mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-12 01:29:42 +00:00
Compare commits
14 Commits
9273ca35cf
...
combine-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b16cf6333 | ||
|
|
7798f5c368 | ||
|
|
5c2e8b391c | ||
|
|
548bc1df81 | ||
|
|
c1d2bce8fb | ||
|
c71beab278
|
|||
|
259932a548
|
|||
|
7526485837
|
|||
| 39ed5aefb4 | |||
|
e7e760de2e
|
|||
| 9091197639 | |||
|
4f4baa62c1
|
|||
|
b9f8621e1a
|
|||
|
4b1dc37a7f
|
18
RIGS/migrations/0045_alter_profile_is_approved.py
Normal file
18
RIGS/migrations/0045_alter_profile_is_approved.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.12 on 2022-10-20 23:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0044_profile_is_supervisor'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='is_approved',
|
||||
field=models.BooleanField(default=False, help_text='Designates whether a staff member has approved this user.', verbose_name='Approval Status'),
|
||||
),
|
||||
]
|
||||
@@ -36,7 +36,7 @@ class Profile(AbstractUser):
|
||||
initials = models.CharField(max_length=5, null=True, blank=False)
|
||||
phone = models.CharField(max_length=13, blank=True, default='')
|
||||
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
|
||||
is_approved = models.BooleanField(default=False)
|
||||
is_approved = models.BooleanField(default=False, verbose_name="Approval Status", help_text="Designates whether a staff member has approved this user.")
|
||||
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
||||
last_emailed = models.DateTimeField(blank=True, null=True)
|
||||
dark_theme = models.BooleanField(default=False)
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
bulletFontSize="10"/>
|
||||
</stylesheet>
|
||||
|
||||
<template > {# Note: page is 595x842 points (1 point=1/72in) #}
|
||||
<template title="{{filename}}"> {# Note: page is 595x842 points (1 point=1/72in) #}
|
||||
<pageTemplate id="Headed" >
|
||||
<pageGraphics>
|
||||
<image file="static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
|
||||
|
||||
@@ -5,21 +5,6 @@
|
||||
|
||||
{% block title %}Request Authorisation{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||
<script src="{% static 'js/popover.js' %}"></script>
|
||||
<script src="{% static 'js/clipboard.min.js' %}"></script>
|
||||
<script>
|
||||
var clipboard = new ClipboardJS('.btn');
|
||||
|
||||
clipboard.on('success', function(e) {
|
||||
$(e.trigger).popover('show');
|
||||
window.setTimeout(function () {$(e.trigger).popover('hide')}, 3000);
|
||||
e.clearSelection();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
@@ -33,11 +18,11 @@
|
||||
<dl class="dl-horizontal">
|
||||
{% if object.person.email %}
|
||||
<dt>Person Email</dt>
|
||||
<dd><span id="person-email">{{ object.person.email }}</span>{% button 'copy' id='#person-email' %}</dd>
|
||||
<dd><span id="person-email" class="pr-1">{{ object.person.email }}</span> {% button 'copy' id='#person-email' %}</dd>
|
||||
{% endif %}
|
||||
{% if object.organisation.email %}
|
||||
<dt>Organisation Email</dt>
|
||||
<dd><span id="org-email">{{ object.organisation.email }}</span>{% button 'copy' id='#org-email' %}</dd>
|
||||
<dd><span id="org-email" class="pr-1">{{ object.organisation.email }}</span> {% button 'copy' id='#org-email' %}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% else %}
|
||||
@@ -57,11 +42,20 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||
<script src="{% static 'js/popover.js' %}"></script>
|
||||
<script src="{% static 'js/clipboard.min.js' %}"></script>
|
||||
<script>
|
||||
$('#auth-request-form').on('submit', function () {
|
||||
$('#auth-request-form button').attr('disabled', true);
|
||||
});
|
||||
var clipboard = new ClipboardJS('.btn');
|
||||
|
||||
clipboard.on('success', function(e) {
|
||||
$(e.trigger).popover('show');
|
||||
window.setTimeout(function () {$(e.trigger).popover('hide')}, 3000);
|
||||
e.clearSelection();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -216,6 +216,8 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
|
||||
return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': style}
|
||||
elif type == 'submit':
|
||||
return {'submit': True, 'class': 'btn-primary', 'icon': 'fa-save', 'text': 'Save', 'id': id, 'style': style}
|
||||
elif type == 'today':
|
||||
return {'today': True, 'id': id}
|
||||
return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
def validate_positive(value):
|
||||
if value < 0:
|
||||
raise ValidationError("A price cannot be negative")
|
||||
|
||||
|
||||
@reversion.register
|
||||
class Asset(models.Model, RevisionMixin):
|
||||
parent = models.ForeignKey(to='self', related_name='asset_parent',
|
||||
@@ -117,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")
|
||||
date_acquired = models.DateField()
|
||||
date_sold = models.DateField(blank=True, null=True)
|
||||
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
|
||||
salvage_value = models.DecimalField(null=True, decimal_places=2, max_digits=10)
|
||||
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
||||
replacement_cost = models.DecimalField(null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
# Audit
|
||||
@@ -165,12 +170,6 @@ class Asset(models.Model, RevisionMixin):
|
||||
errdict["asset_id"] = [
|
||||
"An Asset ID can only consist of letters and numbers, with a final number"]
|
||||
|
||||
if self.purchase_price and self.purchase_price < 0:
|
||||
errdict["purchase_price"] = ["A price cannot be negative"]
|
||||
|
||||
if self.salvage_value and self.salvage_value < 0:
|
||||
errdict["salvage_value"] = ["A price cannot be negative"]
|
||||
|
||||
if self.is_cable:
|
||||
if not self.length or self.length <= 0:
|
||||
errdict["length"] = ["The length of a cable must be more than 0"]
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
date = new Date();
|
||||
}
|
||||
$('#id_date_acquired').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'));
|
||||
return false;
|
||||
}
|
||||
function setFieldValue(ID, CSA) {
|
||||
$('#' + String(ID)).val(CSA);
|
||||
return false;
|
||||
}
|
||||
function checkIfCableHidden() {
|
||||
document.getElementById("cable-table").hidden = !document.getElementById("id_is_cable").checked;
|
||||
@@ -39,16 +41,16 @@
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.date_acquired col="col-6" %}
|
||||
<div class="col-sm-4">
|
||||
<button class="btn btn-info" onclick="setAcquired(true);" tabindex="-1">Today</button>
|
||||
<button class="btn btn-warning" onclick="setAcquired(false);" tabindex="-1">Unknown</button>
|
||||
<div class="col-sm-2">
|
||||
<button class="btn btn-info" onclick="return setAcquired(true);" tabindex="-1">Today</button>
|
||||
<button class="btn btn-warning" onclick="return setAcquired(false);" tabindex="-1">Unknown</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.date_sold col="col-6" %}
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.salvage_value col="col-6" prepend="£" %}
|
||||
{% include 'partials/form_field.html' with field=form.replacement_cost col="col-6" prepend="£" %}
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group form-row">
|
||||
@@ -64,16 +66,16 @@
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.length append=form.length.help_text col="col-6" %}
|
||||
<div class="col-4">
|
||||
<button class="btn btn-danger" onclick="setFieldValue('{{ form.length.id_for_label }}','5');" tabindex="-1" type="button">5{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-success" onclick="setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1" type="button">10{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-info" onclick="setFieldValue('{{ form.length.id_for_label }}','20');" tabindex="-1" type="button">20{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-danger" onclick="return setFieldValue('{{ form.length.id_for_label }}','5');" tabindex="-1" type="button">5{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-success" onclick="return setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1" type="button">10{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-info" onclick="return setFieldValue('{{ form.length.id_for_label }}','20');" tabindex="-1" type="button">20{{ form.length.help_text }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.csa append=form.csa.help_text title='CSA' col="col-6" %}
|
||||
<div class="col-4">
|
||||
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '1.5');" tabindex="-1" type="button">1.5{{ form.csa.help_text }}</button>
|
||||
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '2.5');" tabindex="-1" type="button">2.5{{ form.csa.help_text }}</button>
|
||||
<button class="btn btn-secondary" onclick="return setFieldValue('{{ form.csa.id_for_label }}', '1.5');" tabindex="-1" type="button">1.5{{ form.csa.help_text }}</button>
|
||||
<button class="btn btn-secondary" onclick="return setFieldValue('{{ form.csa.id_for_label }}', '2.5');" tabindex="-1" type="button">2.5{{ form.csa.help_text }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load title_spaced from filters %}
|
||||
{% spaceless %}
|
||||
<label for="{{ field.id_for_label }}" {% if col %}class="col-2 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label>
|
||||
<label for="{{ field.id_for_label }}" {% if col %}class="col-4 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label>
|
||||
{% if append or prepend %}
|
||||
<div class="input-group {{col}}">
|
||||
{% if prepend %}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
|
||||
<select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
|
||||
{% if object.purchased_from %}
|
||||
<option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option>
|
||||
{% endif %}
|
||||
@@ -39,10 +39,10 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.salvage_value.id_for_label }}">Salvage Value</label>
|
||||
<label for="{{ form.salvage_value.id_for_label }}">Replacement Cost</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
||||
{% render_field form.salvage_value|add_class:'form-control' value=object.salvage_value %}
|
||||
{% render_field form.replacement_cost|add_class:'form-control' value=object.replacement_cost %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -70,8 +70,8 @@
|
||||
<dd>{% if object.purchased_from %}<a href="{{object.purchased_from.get_absolute_url}}">{{ object.purchased_from }}</a>{%else%}-{%endif%}</dd>
|
||||
<dt>Purchase Price</dt>
|
||||
<dd>£{{ object.purchase_price|default_if_none:'-' }}</dd>
|
||||
<dt>Salvage Value</dt>
|
||||
<dd>£{{ object.salvage_value|default_if_none:'-' }}</dd>
|
||||
<dt>Replacement Cost</dt>
|
||||
<dd>£{{ object.replacement_cost|default_if_none:'-' }}</dd>
|
||||
<dt>Date Acquired</dt>
|
||||
<dd>{{ object.date_acquired|default_if_none:'-' }}</dd>
|
||||
{% if object.date_sold %}
|
||||
|
||||
@@ -28,13 +28,13 @@ def cable_type(db):
|
||||
|
||||
@pytest.fixture
|
||||
def test_cable(db, category, status, cable_type):
|
||||
cable = models.Asset.objects.create(asset_id="9666", description="125A -> Jack", comments="The cable from Hell...", status=status, category=category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cable_type, length=10, csa="1.5", salvage_value=50)
|
||||
cable = models.Asset.objects.create(asset_id="9666", description="125A -> Jack", comments="The cable from Hell...", status=status, category=category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cable_type, length=10, csa="1.5", replacement_cost=50)
|
||||
yield cable
|
||||
cable.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_asset(db, category, status):
|
||||
asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), salvage_value=100)
|
||||
asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), replacement_cost=100)
|
||||
yield asset
|
||||
asset.delete()
|
||||
|
||||
@@ -79,7 +79,7 @@ class AssetForm(FormPage):
|
||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||
'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
|
||||
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
|
||||
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
|
||||
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
|
||||
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||
'date_sold': (regions.DatePicker, (By.ID, 'id_date_sold')),
|
||||
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
||||
@@ -221,7 +221,7 @@ class AssetAuditList(AssetList):
|
||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
|
||||
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
|
||||
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
||||
'status': (regions.SingleSelectPicker, (By.ID, 'id_status')),
|
||||
|
||||
@@ -102,7 +102,7 @@ def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, ca
|
||||
page.status = status.name
|
||||
page.serial_number = "MELON-MELON-MELON"
|
||||
page.comments = "You might need that"
|
||||
page.salvage_value = "666"
|
||||
page.replacement_cost = "666"
|
||||
page.is_cable = True
|
||||
|
||||
assert logged_in_browser.driver.find_element(By.ID, 'cable-table').is_displayed()
|
||||
@@ -179,7 +179,7 @@ class TestAssetForm(AutoLoginTest):
|
||||
self.page.comments = comments = "This is actually a sledgehammer, not a cable..."
|
||||
|
||||
self.page.purchase_price = "12.99"
|
||||
self.page.salvage_value = "99.12"
|
||||
self.page.replacement_cost = "99.12"
|
||||
self.page.date_acquired = acquired = datetime.date(2020, 5, 2)
|
||||
self.page.purchased_from_selector.toggle()
|
||||
self.assertTrue(self.page.purchased_from_selector.is_open)
|
||||
@@ -320,14 +320,14 @@ class TestAssetAudit(AutoLoginTest):
|
||||
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1,
|
||||
voltage_rating=40, num_pins=13)
|
||||
models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status,
|
||||
category=self.category, date_acquired=datetime.date(2020, 2, 1), salvage_value=10)
|
||||
category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||
models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status,
|
||||
category=self.category, date_acquired=datetime.date(2020, 2, 1), salvage_value=10)
|
||||
category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||
models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category,
|
||||
date_acquired=datetime.date(2020, 2, 1), salvage_value=10)
|
||||
date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||
self.asset = models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status,
|
||||
category=self.category,
|
||||
date_acquired=datetime.date(2020, 2, 1), salvage_value=10)
|
||||
date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||
self.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
|
||||
self.wait = WebDriverWait(self.driver, 20)
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ def test_oembed(client, test_asset):
|
||||
|
||||
|
||||
def test_asset_create(admin_client):
|
||||
response = admin_client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'})
|
||||
response = admin_client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'replacement_cost': '-30'})
|
||||
assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
||||
assert_asset_form_errors(response)
|
||||
|
||||
@@ -99,7 +99,7 @@ def test_cable_create(admin_client):
|
||||
|
||||
def test_asset_edit(admin_client, test_asset):
|
||||
url = reverse('asset_update', kwargs={'pk': test_asset.asset_id})
|
||||
response = admin_client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'salvage_value': '-50', 'description': "", 'status': "", 'category': ""})
|
||||
response = admin_client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'replacement_cost': '-50', 'description': "", 'status': "", 'category': ""})
|
||||
assert_asset_form_errors(response)
|
||||
|
||||
|
||||
@@ -127,4 +127,4 @@ def assert_asset_form_errors(response):
|
||||
assertFormError(response, 'form', 'category', 'This field is required.')
|
||||
assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
|
||||
assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
|
||||
assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
|
||||
assertFormError(response, 'form', 'replacement_cost', 'A price cannot be negative')
|
||||
|
||||
@@ -28,6 +28,7 @@ def admin_user(admin_user):
|
||||
admin_user.last_name = "Test"
|
||||
admin_user.initials = "ETU"
|
||||
admin_user.is_approved = True
|
||||
admin_user.is_supervisor = True
|
||||
admin_user.save()
|
||||
return admin_user
|
||||
|
||||
|
||||
1284
package-lock.json
generated
1284
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,14 +27,14 @@
|
||||
"html5sortable": "^0.13.3",
|
||||
"jquery": "^3.6.0",
|
||||
"konami": "^1.6.3",
|
||||
"moment": "^2.29.2",
|
||||
"moment": "^2.29.4",
|
||||
"node-sass": "^7.0.0",
|
||||
"popper.js": "^1.16.1",
|
||||
"postcss": "^8.4.5",
|
||||
"uglify-js": "^3.14.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browser-sync": "^2.27.7"
|
||||
"browser-sync": "^2.27.10"
|
||||
},
|
||||
"scripts": {
|
||||
"gulp": "gulp",
|
||||
|
||||
@@ -47,14 +47,16 @@ function initPicker(obj) {
|
||||
//log: 3,
|
||||
preprocessData: function (data) {
|
||||
var i, l = data.length, array = [];
|
||||
array.push({
|
||||
text: clearSelectionLabel,
|
||||
value: '',
|
||||
data:{
|
||||
update_url: '',
|
||||
subtext:''
|
||||
}
|
||||
});
|
||||
if (!obj.data('noclear')) {
|
||||
array.push({
|
||||
text: clearSelectionLabel,
|
||||
value: '',
|
||||
data:{
|
||||
update_url: '',
|
||||
subtext:''
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (l) {
|
||||
for(i = 0; i < l; i++){
|
||||
@@ -71,11 +73,13 @@ function initPicker(obj) {
|
||||
return array;
|
||||
}
|
||||
};
|
||||
|
||||
obj.prepend($("<option></option>")
|
||||
.attr("value",'')
|
||||
.text(clearSelectionLabel)
|
||||
.data('update_url','')); //Add "clear selection" option
|
||||
console.log(obj.data);
|
||||
if (!obj.data('noclear')) {
|
||||
obj.prepend($("<option></option>")
|
||||
.attr("value",'')
|
||||
.text(clearSelectionLabel)
|
||||
.data('update_url','')); //Add "clear selection" option
|
||||
}
|
||||
|
||||
|
||||
obj.selectpicker().ajaxSelectPicker(options); //Initiaise selectPicker
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{% load nice_errors from filters %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger alert-dismissable">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<div class="alert alert-danger mb-0">
|
||||
<dl>
|
||||
{% with form|nice_errors as qq %}
|
||||
{% for error_name,desc in qq.items %}
|
||||
<span class="row">
|
||||
<dt class="col-4">{{error_name}}</dt>
|
||||
<dd class="col-8">{{desc}}</dd>
|
||||
<dt class="col-3">{{error_name}}</dt>
|
||||
<dd class="col-9">{{desc}}</dd>
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<a href="{% url target pk %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%} {% if text == 'Print' %}target="_blank"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a>
|
||||
{% elif copy %}
|
||||
<button class="btn btn-secondary btn-sm mr-1" data-clipboard-target="{{id}}" data-content="Copied to clipboard!"><span class="fas fa-copy"></span></button>
|
||||
{% elif today %}
|
||||
<button class="btn btn-info col-sm-2" onclick="var date = new Date(); $('#{{id}}').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'))" tabindex="-1" type="button">Today</button>
|
||||
{% else %}
|
||||
<a href="{% url target %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{% load widget_tweaks %}
|
||||
{% include 'form_errors.html' %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-info">
|
||||
<p><strong>Please note:</strong> If it has been more than a year since you last logged in, your account will have been automatically deactivated. Contact <a href="mailto:it@nottinghamtec.co.uk">it@nottinghamtec.co.uk</a> for assistance.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-6 offset-sm-3 col-lg-4 offset-lg-4">
|
||||
<form action="{% url 'login' %}" method="post" role="form" target="_self">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from PyRIGS.decorators import user_passes_test_with_403
|
||||
|
||||
|
||||
def has_perm_or_supervisor(perm, login_url=None, oembed_view=None):
|
||||
return user_passes_test_with_403(lambda u: (hasattr(u, 'is_supervisor') and u.is_supervisor) or u.has_perm(perm), login_url=login_url, oembed_view=oembed_view)
|
||||
def is_supervisor(login_url=None, oembed_view=None):
|
||||
return user_passes_test_with_403(lambda u: (hasattr(u, 'is_supervisor') and u.is_supervisor))
|
||||
|
||||
@@ -201,7 +201,7 @@ class TrainingItemQualification(models.Model, RevisionMixin):
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return f"{self.trainee} {self.get_depth_display().lower()} {self.get_depth_display()} in {self.item}"
|
||||
return f"{self.trainee} {self.get_depth_display().lower()} in {self.item}"
|
||||
|
||||
@classmethod
|
||||
def get_colour_from_depth(cls, depth):
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<a class="dropdown-item" href="{% url 'item_list' %}"><span class="fas fa-sitemap"></span> Item List</a>
|
||||
</div>
|
||||
</li>
|
||||
{% if perms.training.add_trainingitemqualification or request.user.is_supervisor %}
|
||||
{% if request.user.is_supervisor %}
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'session_log' %}"><span class="fas fa-users"></span> Log Session</a></li>
|
||||
{% endif %}
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'training_activity_table' %}"><span class="fas fa-random"></span> Recent Changes</a></li>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
{% render_field form.date|add_class:'form-control'|attr:'type="date"' value=training_date %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<button class="btn btn-info col-sm-2" onclick="var date = new Date(); $('#id_date').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'))" tabindex="-1" type="button">Today</button>
|
||||
{% button 'today' id='id_date' %}
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
<label for="id_notes" class="col-sm-2 col-form-label">Notes</label>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if request.user.is_supervisor or perms.training.add_traininglevelrequirement %}
|
||||
{% if request.user.is_supervisor %}
|
||||
<div class="col-sm-12 text-right pr-0">
|
||||
<a type="button" class="btn btn-success mb-3" href="{% url 'add_requirement' pk=object.pk %}" id="requirement_button">
|
||||
<span class="fas fa-plus"></span> Add New Requirement
|
||||
@@ -79,9 +79,9 @@
|
||||
{% endfor %}
|
||||
<tr><th colspan="3" class="text-center">{{object}}</th></tr>
|
||||
<tr>
|
||||
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% if request.user.is_supervisor or perms.training.add_trainingitemqualification %}
|
||||
{% if request.user.is_supervisor %}
|
||||
<a type="button" class="btn btn-success" href="{% url 'add_qualification' object.pk %}" id="add_record">
|
||||
<span class="fas fa-plus"></span> Add New Training Record
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label>
|
||||
<select name="supervisor" id="supervisor_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required>
|
||||
<select name="supervisor" id="supervisor_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required data-noclear="true">
|
||||
{% if supervisor %}
|
||||
<option value="{{form.supervisor.value}}" selected>{{ supervisor }}</option>
|
||||
{% endif %}
|
||||
|
||||
@@ -28,25 +28,26 @@
|
||||
{% include 'form_errors.html' %}
|
||||
{% csrf_token %}
|
||||
<h3>People</h3>
|
||||
<div class="form-group row">
|
||||
<div class="form-group row" id="supervisor_group">
|
||||
{% include 'partials/supervisor_field.html' %}
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="form-group row" id="trainees_group">
|
||||
<label for="trainees_id" class="col-sm-2">Select Attendees</label>
|
||||
<select multiple name="trainees" id="trainees_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
||||
<select multiple name="trainees" id="trainees_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" data-noclear="true">
|
||||
</select>
|
||||
</div>
|
||||
<h3>Training Items</h3>
|
||||
{% for depth in depths %}
|
||||
<div class="form-group row">
|
||||
<div class="form-group row" id="{{depth.0}}">
|
||||
<label for="selectpicker" class="col-sm-2 text-{% colour_from_depth depth.0 %} py-1">{{ depth.1 }} Items</label>
|
||||
<select multiple name="items_{{depth.0}}" id="items_{{depth.0}}_id" class="selectpicker col-sm-10 px-0" data-live-search="true" data-sourceurl="{% url 'api_secure' model='training_item' %}?fields=display_id,description&filters=active">
|
||||
<select multiple name="items_{{depth.0}}" id="items_{{depth.0}}_id" class="selectpicker col-sm-10 px-0" data-live-search="true" data-sourceurl="{% url 'api_secure' model='training_item' %}?fields=display_id,description&filters=active" data-noclear="true">
|
||||
</select>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<h3>Session Information</h3>
|
||||
<div class="form-group">
|
||||
{% include 'partials/form_field.html' with field=form.date %}
|
||||
<div class="form-group row">
|
||||
{% include 'partials/form_field.html' with field=form.date col='col-sm-6' %}
|
||||
{% button 'today' id='id_date' %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{% include 'partials/form_field.html' with field=form.notes %}
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<th>Date</th>
|
||||
<th>Supervisor</th>
|
||||
<th>Notes</th>
|
||||
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
|
||||
{% if request.user.is_supervisor %}
|
||||
<th></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
@@ -67,7 +67,7 @@
|
||||
<td>{{ object.date }}</td>
|
||||
<td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td>
|
||||
<td>{{ object.notes }}</td>
|
||||
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
|
||||
{% if request.user.is_supervisor %}
|
||||
<td>{% button 'edit' 'edit_qualification' object.pk id="edit" %}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
|
||||
@@ -30,6 +30,15 @@ def training_item(db):
|
||||
training_item.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def training_item_2(db):
|
||||
training_category = models.TrainingCategory.objects.create(reference_number=2, name="Sound")
|
||||
training_item = models.TrainingItem.objects.create(category=training_category, reference_number=1, description="Fundamentals of Audio")
|
||||
yield training_item
|
||||
training_category.delete()
|
||||
training_item.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def level(db):
|
||||
level = models.TrainingLevel.objects.create(description="There is no description.", level=models.TrainingLevel.TECHNICIAN)
|
||||
|
||||
@@ -40,3 +40,42 @@ class AddQualification(FormPage):
|
||||
@property
|
||||
def success(self):
|
||||
return 'add' not in self.driver.current_url
|
||||
|
||||
|
||||
class SessionLog(FormPage):
|
||||
URL_TEMPLATE = 'training/session_log'
|
||||
|
||||
_supervisor_selector = (By.CSS_SELECTOR, 'div#supervisor_group>div.bootstrap-select')
|
||||
_trainees_selector = (By.CSS_SELECTOR, 'div#trainees_group>div.bootstrap-select')
|
||||
_training_started_selector = (By.XPATH, '//div[1]/div/div/form/div[4]/div')
|
||||
_training_complete_selector = (By.XPATH, '//div[1]/div/div/form/div[4]/div')
|
||||
_competency_assessed_selector = (By.XPATH, '//div[1]/div/div/form/div[5]/div')
|
||||
|
||||
form_items = {
|
||||
'date': (regions.DatePicker, (By.ID, 'id_date')),
|
||||
'notes': (regions.TextBox, (By.ID, 'id_notes')),
|
||||
}
|
||||
|
||||
@property
|
||||
def supervisor_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._supervisor_selector))
|
||||
|
||||
@property
|
||||
def trainees_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._trainees_selector))
|
||||
|
||||
@property
|
||||
def training_started_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._training_started_selector))
|
||||
|
||||
@property
|
||||
def training_complete_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._training_complete_selector))
|
||||
|
||||
@property
|
||||
def competency_assessed_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._competency_assessed_selector))
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return 'log' not in self.driver.current_url
|
||||
|
||||
@@ -12,6 +12,15 @@ from training import models
|
||||
from training.tests import pages
|
||||
|
||||
|
||||
def select_super(page, supervisor):
|
||||
page.supervisor_selector.toggle()
|
||||
assert page.supervisor_selector.is_open
|
||||
page.supervisor_selector.search(supervisor.name[:-6])
|
||||
time.sleep(2) # Slow down for javascript
|
||||
assert page.supervisor_selector.options[0].selected
|
||||
page.supervisor_selector.toggle()
|
||||
|
||||
|
||||
def test_add_qualification(logged_in_browser, live_server, trainee, supervisor, training_item):
|
||||
page = pages.AddQualification(logged_in_browser.driver, live_server.url, pk=trainee.pk).open()
|
||||
# assert page.name in str(trainee)
|
||||
@@ -30,12 +39,7 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
|
||||
assert page.item_selector.options[0].selected
|
||||
page.item_selector.toggle()
|
||||
|
||||
page.supervisor_selector.toggle()
|
||||
assert page.supervisor_selector.is_open
|
||||
page.supervisor_selector.search(supervisor.name[:-6])
|
||||
time.sleep(2) # Slow down for javascript
|
||||
assert page.supervisor_selector.options[0].selected
|
||||
page.supervisor_selector.toggle()
|
||||
select_super(page, supervisor)
|
||||
|
||||
page.submit()
|
||||
assert page.success
|
||||
@@ -44,3 +48,32 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
|
||||
assert qualification.date == date
|
||||
assert qualification.notes == "A note"
|
||||
assert qualification.depth == models.TrainingItemQualification.STARTED
|
||||
|
||||
|
||||
def test_session_log(logged_in_browser, live_server, trainee, supervisor, training_item, training_item_2):
|
||||
page = pages.SessionLog(logged_in_browser.driver, live_server.url).open()
|
||||
|
||||
page.date = date = datetime.date(2001, 1, 10)
|
||||
page.notes = note = "A general note"
|
||||
|
||||
time.sleep(2) # Slow down for javascript
|
||||
|
||||
select_super(page, supervisor)
|
||||
|
||||
page.trainees_selector.toggle()
|
||||
assert page.trainees_selector.is_open
|
||||
page.trainees_selector.search(trainee.first_name)
|
||||
time.sleep(2) # Slow down for javascript
|
||||
page.trainees_selector.set_option(trainee.name, True)
|
||||
# assert page.trainees_selector.options[0].selected
|
||||
page.trainees_selector.toggle()
|
||||
|
||||
page.training_started_selector.toggle()
|
||||
assert page.training_started_selector.is_open
|
||||
page.training_started_selector.search(training_item.description[:-2])
|
||||
time.sleep(2) # Slow down for javascript
|
||||
# assert page.training_started_selector.options[0].selected
|
||||
page.training_started_selector.toggle()
|
||||
|
||||
page.submit()
|
||||
assert page.success
|
||||
|
||||
@@ -16,7 +16,7 @@ def test_add_qualification(admin_client, trainee, admin_user, training_item):
|
||||
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': trainee.pk, 'item': training_item.pk})
|
||||
assertFormError(response, 'form', 'date', 'Qualification date may not be in the future')
|
||||
assertFormError(response, 'form', 'supervisor', 'One may not supervise oneself...')
|
||||
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': admin_user.pk, 'item': training_item.pk})
|
||||
response = admin_client.post(url, {'date': date, 'trainee': admin_user.pk, 'supervisor': trainee.pk, 'item': training_item.pk})
|
||||
print(response.content)
|
||||
assertFormError(response, 'form', 'supervisor', 'Selected supervisor must actually *be* a supervisor...')
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from django.urls import path
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from training.decorators import has_perm_or_supervisor
|
||||
from training.decorators import is_supervisor
|
||||
from PyRIGS.decorators import permission_required_with_403
|
||||
|
||||
from training import views, models
|
||||
from versioning.views import VersionHistory
|
||||
@@ -12,22 +13,22 @@ urlpatterns = [
|
||||
|
||||
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
|
||||
path('trainee/<int:pk>/',
|
||||
has_perm_or_supervisor('RIGS.view_profile')(views.TraineeDetail.as_view()),
|
||||
permission_required_with_403('RIGS.view_profile')(views.TraineeDetail.as_view()),
|
||||
name='trainee_detail'),
|
||||
path('trainee/<int:pk>/history', has_perm_or_supervisor('RIGS.view_profile')(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
|
||||
path('trainee/<int:pk>/add_qualification/', has_perm_or_supervisor('training.add_trainingitemqualification')(views.AddQualification.as_view()),
|
||||
path('trainee/<int:pk>/history', 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/', is_supervisor()(views.AddQualification.as_view()),
|
||||
name='add_qualification'),
|
||||
path('trainee/edit_qualification/<int:pk>/', has_perm_or_supervisor('training.change_trainingitemqualification')(views.EditQualification.as_view()),
|
||||
path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()),
|
||||
name='edit_qualification'),
|
||||
|
||||
path('levels/', login_required(views.LevelList.as_view()), name='level_list'),
|
||||
path('level/<int:pk>/', login_required(views.LevelDetail.as_view()), name='level_detail'),
|
||||
path('level/<int:pk>/user/<int:u>/', login_required(views.LevelDetail.as_view()), name='level_detail'),
|
||||
path('level/<int:pk>/add_requirement/', login_required(views.AddLevelRequirement.as_view()), name='add_requirement'),
|
||||
path('level/remove_requirement/<int:pk>/', login_required(views.RemoveRequirement.as_view()), name='remove_requirement'),
|
||||
path('level/<int:pk>/add_requirement/', is_supervisor()(views.AddLevelRequirement.as_view()), name='add_requirement'),
|
||||
path('level/remove_requirement/<int:pk>/', is_supervisor()(views.RemoveRequirement.as_view()), name='remove_requirement'),
|
||||
|
||||
path('trainee/<int:pk>/level/<int:level_pk>/confirm', login_required(views.ConfirmLevel.as_view()), name='confirm_level'),
|
||||
path('trainee/<int:pk>/level/<int:level_pk>/confirm', is_supervisor()(views.ConfirmLevel.as_view()), name='confirm_level'),
|
||||
path('trainee/<int:pk>/item_record', login_required(views.TraineeItemDetail.as_view()), name='trainee_item_detail'),
|
||||
|
||||
path('session_log', has_perm_or_supervisor('training.add_trainingitemqualification')(views.SessionLog.as_view()), name='session_log'),
|
||||
path('session_log', is_supervisor()(views.SessionLog.as_view()), name='session_log'),
|
||||
]
|
||||
|
||||
26
users/management/commands/usercleanup.py
Normal file
26
users/management/commands/usercleanup.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from datetime import datetime, timedelta
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils import timezone
|
||||
|
||||
from RIGS.models import Profile
|
||||
from training.models import TrainingLevel
|
||||
|
||||
|
||||
# This is triggered nightly by Heroku Scheduler
|
||||
class Command(BaseCommand):
|
||||
help = 'Performs perodic user maintenance tasks'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for person in Profile.objects.all():
|
||||
# Inactivate users that have not logged in for a year (or have never logged in)
|
||||
if person.last_login is None or (timezone.now() - person.last_login).days > 365:
|
||||
person.is_active = False
|
||||
person.is_approved = False
|
||||
person.save()
|
||||
# Ensure everyone with a supervisor level has the flag correctly set in the database
|
||||
if person.level_qualifications.exclude(confirmed_on=None).select_related('level') \
|
||||
.filter(level__level__gte=TrainingLevel.SUPERVISOR) \
|
||||
.exclude(level__department=TrainingLevel.HAULAGE) \
|
||||
.exclude(level__department__isnull=True).exists():
|
||||
person.is_supervisor = True
|
||||
person.save()
|
||||
Reference in New Issue
Block a user