mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-04-11 04:51:47 +00:00
Compare commits
7 Commits
6b19d0e8b8
...
3bac6050f1
| Author | SHA1 | Date | |
|---|---|---|---|
|
3bac6050f1
|
|||
|
7aa37c225d
|
|||
|
215c51e718
|
|||
|
fbe4d7271f
|
|||
|
2a2f010028
|
|||
|
484f155e43
|
|||
|
fdbdaab52e
|
@@ -56,6 +56,11 @@ class Profile(AbstractUser):
|
|||||||
def latest_events(self):
|
def latest_events(self):
|
||||||
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists')
|
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists')
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def as_trainee(self):
|
||||||
|
from training.models import Trainee
|
||||||
|
return Trainee.objects.get(pk=self.pk)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def admins(cls):
|
def admins(cls):
|
||||||
return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x])
|
return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x])
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
@@ -114,10 +114,8 @@ def orderby(request, field, attr):
|
|||||||
|
|
||||||
return dict_.urlencode()
|
return dict_.urlencode()
|
||||||
|
|
||||||
# Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
|
|
||||||
|
|
||||||
|
@register.filter(needs_autoescape=True) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
|
||||||
@register.filter(needs_autoescape=True)
|
|
||||||
def get_field(obj, field, autoescape=True):
|
def get_field(obj, field, autoescape=True):
|
||||||
value = getattr(obj, field)
|
value = getattr(obj, field)
|
||||||
if(isinstance(value, bool)):
|
if(isinstance(value, bool)):
|
||||||
@@ -221,7 +219,7 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
|
|||||||
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}
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag # TODO Can these be done with annotation/aggregation?
|
||||||
def invoices_waiting():
|
def invoices_waiting():
|
||||||
return len(models.Event.objects.waiting_invoices())
|
return len(models.Event.objects.waiting_invoices())
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
|
||||||
class AssetIDConverter: # Forces lowercase to uppercase
|
class AssetIDConverter: # Forces lowercase to uppercase
|
||||||
regex = '[^/]+'
|
regex = '[^/]+'
|
||||||
|
|
||||||
@@ -6,3 +9,16 @@ class AssetIDConverter: # Forces lowercase to uppercase
|
|||||||
|
|
||||||
def to_url(self, value):
|
def to_url(self, value):
|
||||||
return str(value).upper()
|
return str(value).upper()
|
||||||
|
|
||||||
|
|
||||||
|
class ListConverter:
|
||||||
|
regex = '[^/]+'
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
return value.split(',')
|
||||||
|
|
||||||
|
def to_url(self, value):
|
||||||
|
string = ""
|
||||||
|
for i in value:
|
||||||
|
string += "," + str(i)
|
||||||
|
return string[1:]
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class AssetSearchForm(forms.Form):
|
|||||||
q = forms.CharField(required=False)
|
q = forms.CharField(required=False)
|
||||||
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
|
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
|
||||||
status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False)
|
status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False)
|
||||||
|
is_cable = forms.BooleanField(required=False)
|
||||||
|
|
||||||
|
|
||||||
class SupplierForm(forms.ModelForm):
|
class SupplierForm(forms.ModelForm):
|
||||||
|
|||||||
BIN
assets/static/imgs/square_logo.png
Normal file
BIN
assets/static/imgs/square_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -12,7 +12,7 @@
|
|||||||
});
|
});
|
||||||
$('#searchButton').click(function (e) {
|
$('#searchButton').click(function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var url = "{% url 'asset_audit' None %}".replace('None', $("#{{form.q.id_for_label}}").val();
|
var url = "{% url 'asset_audit' None %}".replace('None', $("#{{form.q.id_for_label}}").val());
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
success: function(){
|
success: function(){
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends 'base_assets.html' %}
|
{% extends 'base_assets.html' %}
|
||||||
{% load paginator from filters %}
|
{% load paginator from filters %}
|
||||||
{% load button from filters %}
|
{% load button from filters %}
|
||||||
|
{% load ids_from_objects from asset_tags %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
@@ -74,6 +75,10 @@
|
|||||||
<label for="status" class="sr-only">Status</label>
|
<label for="status" class="sr-only">Status</label>
|
||||||
{% render_field form.status|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
|
{% render_field form.status|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
{% render_field form.is_cable|add_class:'form-check-input' %}
|
||||||
|
<label class="form-check-label" for="is_cable">Only Cables?</label>
|
||||||
|
</div>
|
||||||
<button id="filter-submit" type="submit" class="btn btn-secondary" style="width: 6em">Filter</button>
|
<button id="filter-submit" type="submit" class="btn btn-secondary" style="width: 6em">Filter</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,6 +86,9 @@
|
|||||||
<div class="row my-2">
|
<div class="row my-2">
|
||||||
<div class="col text-right px-0">
|
<div class="col text-right px-0">
|
||||||
{% button 'new' 'asset_create' style="width: 6em" %}
|
{% button 'new' 'asset_create' style="width: 6em" %}
|
||||||
|
{% if object_list %}
|
||||||
|
<a class="btn btn-primary" href="{% url 'generate_labels' object_list|ids_from_objects %}"><span class="fas fa-barcode"></span> Generate Labels</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-2">
|
<div class="row my-2">
|
||||||
|
|||||||
35
assets/templates/labels_print.xml
Normal file
35
assets/templates/labels_print.xml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE document SYSTEM "rml.dtd">
|
||||||
|
{% load multiply from filters %}
|
||||||
|
{% load index from asset_tags %}
|
||||||
|
<document filename="{{filename}}">
|
||||||
|
<template>
|
||||||
|
<pageTemplate id="main">
|
||||||
|
<pageGraphics>
|
||||||
|
</pageGraphics>
|
||||||
|
<frame id="first" x1="5" y1="-10" width="581" height="842"/>
|
||||||
|
</pageTemplate>
|
||||||
|
</template>
|
||||||
|
<stylesheet>
|
||||||
|
<blockTableStyle id="table">
|
||||||
|
<!-- show a grid: this also comes in handy for debugging your tables.-->
|
||||||
|
<lineStyle kind="GRID" colorName="black" thickness="1" start="0,0" stop="-1,-1" />
|
||||||
|
</blockTableStyle>
|
||||||
|
</stylesheet>
|
||||||
|
<story>
|
||||||
|
<blockTable style="table">
|
||||||
|
{% for i in images0 %}
|
||||||
|
<tr>
|
||||||
|
<td>{% with images0|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
|
||||||
|
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
|
||||||
|
<td>{% with images1|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
|
||||||
|
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
|
||||||
|
<td>{% with images2|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
|
||||||
|
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
|
||||||
|
<td>{% with images3|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
|
||||||
|
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</blockTable>
|
||||||
|
</story>
|
||||||
|
</document>
|
||||||
@@ -12,9 +12,7 @@
|
|||||||
{% button 'edit' url='asset_update' pk=object.asset_id %}
|
{% button 'edit' url='asset_update' pk=object.asset_id %}
|
||||||
{% button 'duplicate' url='asset_duplicate' pk=object.asset_id %}
|
{% button 'duplicate' url='asset_duplicate' pk=object.asset_id %}
|
||||||
<a type="button" class="btn btn-info" href="{% url 'asset_audit' object.asset_id %}"><span class="fas fa-certificate"></span> Audit</a>
|
<a type="button" class="btn btn-info" href="{% url 'asset_audit' object.asset_id %}"><span class="fas fa-certificate"></span> Audit</a>
|
||||||
{% if object.is_cable %}
|
|
||||||
<a type="button" class="btn btn-primary" href="{% url 'generate_label' object.asset_id %}"><span class="fas fa-barcode"></span> Generate Label</a>
|
<a type="button" class="btn btn-primary" href="{% url 'generate_label' object.asset_id %}"><span class="fas fa-barcode"></span> Generate Label</a>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if create or edit or duplicate %}
|
{% if create or edit or duplicate %}
|
||||||
|
|||||||
17
assets/templatetags/asset_tags.py
Normal file
17
assets/templatetags/asset_tags.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from django import template
|
||||||
|
from assets import models
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def ids_from_objects(object_list):
|
||||||
|
id_list = []
|
||||||
|
for obj in object_list:
|
||||||
|
id_list.append(obj.asset_id)
|
||||||
|
return id_list
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def index(indexable, i):
|
||||||
|
return indexable[i] if i < len(indexable) else None
|
||||||
@@ -7,6 +7,7 @@ from PyRIGS.views import OEmbedView
|
|||||||
from . import views, converters
|
from . import views, converters
|
||||||
|
|
||||||
register_converter(converters.AssetIDConverter, 'asset')
|
register_converter(converters.AssetIDConverter, 'asset')
|
||||||
|
register_converter(converters.ListConverter, 'list')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', login_required(views.AssetList.as_view()), name='asset_index'),
|
path('', login_required(views.AssetList.as_view()), name='asset_index'),
|
||||||
@@ -19,6 +20,7 @@ urlpatterns = [
|
|||||||
path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset')
|
path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset')
|
||||||
(views.AssetDuplicate.as_view()), name='asset_duplicate'),
|
(views.AssetDuplicate.as_view()), name='asset_duplicate'),
|
||||||
path('asset/id/<asset:pk>/label', login_required(views.GenerateLabel.as_view()), name='generate_label'),
|
path('asset/id/<asset:pk>/label', login_required(views.GenerateLabel.as_view()), name='generate_label'),
|
||||||
|
path('asset/<list:ids>/list/label', views.GenerateLabels.as_view(), name='generate_labels'),
|
||||||
|
|
||||||
path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'),
|
path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'),
|
||||||
path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'),
|
path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'),
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import simplejson
|
import simplejson
|
||||||
import random
|
import random
|
||||||
|
import base64
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
@@ -11,10 +14,13 @@ from django.utils.decorators import method_decorator
|
|||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.template.loader import get_template
|
||||||
|
|
||||||
|
from PyPDF2 import PdfFileMerger, PdfFileReader
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
from barcode import Code39
|
from barcode import Code39
|
||||||
from barcode.writer import ImageWriter
|
from barcode.writer import ImageWriter
|
||||||
|
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
|
||||||
@@ -52,6 +58,9 @@ class AssetList(LoginRequiredMixin, generic.ListView):
|
|||||||
else:
|
else:
|
||||||
queryset = self.model.objects.filter(Q(asset_id__exact=query_string.upper()))
|
queryset = self.model.objects.filter(Q(asset_id__exact=query_string.upper()))
|
||||||
|
|
||||||
|
if form.cleaned_data['is_cable']:
|
||||||
|
queryset = queryset.filter(is_cable=True)
|
||||||
|
|
||||||
if form.cleaned_data['category']:
|
if form.cleaned_data['category']:
|
||||||
queryset = queryset.filter(category__in=form.cleaned_data['category'])
|
queryset = queryset.filter(category__in=form.cleaned_data['category'])
|
||||||
|
|
||||||
@@ -346,35 +355,82 @@ class CableTypeUpdate(generic.UpdateView):
|
|||||||
return reverse("cable_type_detail", kwargs={"pk": self.object.pk})
|
return reverse("cable_type_detail", kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class GenerateLabel(generic.View):
|
def generate_label(pk):
|
||||||
def get(self, request, pk):
|
black = (0, 0, 0)
|
||||||
black = (0, 0, 0)
|
white = (255, 255, 255)
|
||||||
white = (255, 255, 255)
|
size = (700, 200)
|
||||||
size = (700, 200)
|
font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20)
|
||||||
font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20)
|
obj = get_object_or_404(models.Asset, asset_id=pk)
|
||||||
obj = get_object_or_404(models.Asset, asset_id=pk)
|
|
||||||
|
|
||||||
asset_id = "Asset: {}".format(obj.asset_id)
|
asset_id = "Asset: {}".format(obj.asset_id)
|
||||||
|
if obj.is_cable:
|
||||||
length = "Length: {}m".format(obj.length)
|
length = "Length: {}m".format(obj.length)
|
||||||
csa = "CSA: {}mm²".format(obj.csa)
|
csa = "CSA: {}mm²".format(obj.csa)
|
||||||
|
|
||||||
image = Image.new("RGB", size, white)
|
image = Image.new("RGB", size, white)
|
||||||
logo = Image.open("static/imgs/square_logo.png")
|
logo = Image.open("static/imgs/square_logo.png")
|
||||||
draw = ImageDraw.Draw(image)
|
draw = ImageDraw.Draw(image)
|
||||||
|
|
||||||
draw.text((210, 140), asset_id, fill=black, font=font)
|
draw.text((210, 140), asset_id, fill=black, font=font)
|
||||||
|
if obj.is_cable:
|
||||||
draw.text((210, 170), length, fill=black, font=font)
|
draw.text((210, 170), length, fill=black, font=font)
|
||||||
draw.text((350, 170), csa, fill=black, font=font)
|
draw.text((360, 170), csa, fill=black, font=font)
|
||||||
draw.multiline_text((500, 140), "TEC PA & Lighting\n(0115) 84 68720", fill=black, font=font)
|
draw.multiline_text((500, 140), "TEC PA & Lighting\n(0115) 84 68720", fill=black, font=font)
|
||||||
|
|
||||||
barcode = Code39(str(obj.asset_id), writer=ImageWriter())
|
barcode = Code39(str(obj.asset_id), writer=ImageWriter())
|
||||||
|
|
||||||
logo_size = (200, 200)
|
logo_size = (200, 200)
|
||||||
image.paste(logo.resize(logo_size, Image.ANTIALIAS))
|
image.paste(logo.resize(logo_size, Image.ANTIALIAS))
|
||||||
barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False})
|
barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False})
|
||||||
width, height = barcode_image.size
|
width, height = barcode_image.size
|
||||||
image.paste(barcode_image.crop((0, 0, width, 135)), (int(((size[0] + logo_size[0]) - width) / 2), 0))
|
image.paste(barcode_image.crop((0, 0, width, 135)), (int(((size[0] + logo_size[0]) - width) / 2), 0))
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateLabel(generic.View): # TODO Caching
|
||||||
|
def get(self, request, pk):
|
||||||
response = HttpResponse(content_type="image/png")
|
response = HttpResponse(content_type="image/png")
|
||||||
image.save(response, "PNG")
|
generate_label(pk).save(response, "PNG")
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateLabels(generic.View):
|
||||||
|
def get(self, request, ids):
|
||||||
|
response = HttpResponse(content_type='application/pdf')
|
||||||
|
template = get_template('labels_print.xml')
|
||||||
|
|
||||||
|
images = []
|
||||||
|
|
||||||
|
for asset_id in ids:
|
||||||
|
image = generate_label(asset_id)
|
||||||
|
in_mem_file = BytesIO()
|
||||||
|
image.save(in_mem_file, format="PNG")
|
||||||
|
# reset file pointer to start
|
||||||
|
in_mem_file.seek(0)
|
||||||
|
img_bytes = in_mem_file.read()
|
||||||
|
|
||||||
|
base64_encoded_result_bytes = base64.b64encode(img_bytes)
|
||||||
|
base64_encoded_result_str = base64_encoded_result_bytes.decode('ascii')
|
||||||
|
images.append(base64_encoded_result_str)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'images0': images[::4],
|
||||||
|
'images1': images[1::4],
|
||||||
|
'images2': images[2::4],
|
||||||
|
'images3': images[3::4],
|
||||||
|
'filename': "Asset Label Sheet generated at {}".format(timezone.now())
|
||||||
|
}
|
||||||
|
merger = PdfFileMerger()
|
||||||
|
|
||||||
|
rml = template.render(context)
|
||||||
|
buffer = rml2pdf.parseString(rml)
|
||||||
|
merger.append(PdfFileReader(buffer))
|
||||||
|
buffer.close()
|
||||||
|
|
||||||
|
merged = BytesIO()
|
||||||
|
merger.write(merged)
|
||||||
|
|
||||||
|
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
|
||||||
|
response.write(merged.getvalue())
|
||||||
return response
|
return response
|
||||||
|
|||||||
9656
package-lock.json
generated
9656
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
5
training/decorators.py
Normal file
5
training/decorators.py
Normal file
@@ -0,0 +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, 'as_trainee') and u.as_trainee.is_supervisor) or u.has_perm(perm), login_url=login_url, oembed_view=oembed_view)
|
||||||
@@ -2,6 +2,7 @@ import datetime
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from django.contrib.auth.models import Group, Permission
|
from django.contrib.auth.models import Group, Permission
|
||||||
|
from django.core.management import call_command
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -31,7 +32,7 @@ class Command(BaseCommand):
|
|||||||
self.setup_categories()
|
self.setup_categories()
|
||||||
self.setup_items()
|
self.setup_items()
|
||||||
self.setup_levels()
|
self.setup_levels()
|
||||||
self.setup_supervisor()
|
# call_command('generate_sample_training_users')
|
||||||
print("Done generating training data")
|
print("Done generating training data")
|
||||||
|
|
||||||
def setup_categories(self):
|
def setup_categories(self):
|
||||||
@@ -143,7 +144,13 @@ class Command(BaseCommand):
|
|||||||
"Wiki Editing"]
|
"Wiki Editing"]
|
||||||
|
|
||||||
for i, name in enumerate(names):
|
for i, name in enumerate(names):
|
||||||
item = models.TrainingItem.objects.create(category=random.choice(self.categories), reference_number=random.randint(0, 100), name=name)
|
category = random.choice(self.categories)
|
||||||
|
previous_item = models.TrainingItem.objects.filter(category=category).last()
|
||||||
|
if previous_item is not None:
|
||||||
|
number = previous_item.reference_number + 1
|
||||||
|
else:
|
||||||
|
number = 0
|
||||||
|
item = models.TrainingItem.objects.create(category=category, reference_number=number, name=name)
|
||||||
self.items.append(item)
|
self.items.append(item)
|
||||||
|
|
||||||
def setup_levels(self):
|
def setup_levels(self):
|
||||||
@@ -192,20 +199,3 @@ class Command(BaseCommand):
|
|||||||
models.TrainingLevelRequirement.objects.create(level=supervisor, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
|
models.TrainingLevelRequirement.objects.create(level=supervisor, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
|
||||||
self.levels.append(technician)
|
self.levels.append(technician)
|
||||||
self.levels.append(supervisor)
|
self.levels.append(supervisor)
|
||||||
|
|
||||||
def setup_supervisor(self):
|
|
||||||
supervisor = models.Profile.objects.create(username="supervisor", first_name="Super", last_name="Visor",
|
|
||||||
initials="SV",
|
|
||||||
email="supervisor@example.com", is_active=True,
|
|
||||||
is_staff=True, is_approved=True)
|
|
||||||
supervisor.set_password('supervisor')
|
|
||||||
supervisor.groups.add(Group.objects.get(name="Keyholders"))
|
|
||||||
supervisor.save()
|
|
||||||
models.TrainingLevelQualification.objects.create(
|
|
||||||
trainee=supervisor,
|
|
||||||
level=models.TrainingLevel.objects.filter(
|
|
||||||
level__gte=models.TrainingLevel.SUPERVISOR).exclude(
|
|
||||||
department=models.TrainingLevel.HAULAGE).exclude(
|
|
||||||
department__isnull=True).first(),
|
|
||||||
confirmed_on=timezone.now(),
|
|
||||||
confirmed_by=models.Trainee.objects.first())
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import datetime
|
||||||
|
import random
|
||||||
|
|
||||||
|
from django.contrib.auth.models import Group, Permission
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils import timezone
|
||||||
|
from reversion import revisions as reversion
|
||||||
|
|
||||||
|
from training import models
|
||||||
|
from RIGS.models import Profile
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Adds training users'
|
||||||
|
can_import_settings = True
|
||||||
|
|
||||||
|
profiles = []
|
||||||
|
committee_group = None
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
print("Generating useful training users")
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
if not (settings.DEBUG or settings.STAGING):
|
||||||
|
raise CommandError('You cannot run this command in production')
|
||||||
|
|
||||||
|
random.seed('otherwise it is done by time, which could lead to inconsistent tests')
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
self.setup_groups()
|
||||||
|
self.setup_useful_profiles()
|
||||||
|
print("Done generating useful training users")
|
||||||
|
|
||||||
|
def setup_groups(self):
|
||||||
|
self.committee_group = Group.objects.create(name='Committee')
|
||||||
|
|
||||||
|
perms = [
|
||||||
|
"add_trainingitemqualification",
|
||||||
|
"change_trainingitemqualification",
|
||||||
|
"delete_trainingitemqualification",
|
||||||
|
"add_traininglevelqualification",
|
||||||
|
"change_traininglevelqualification",
|
||||||
|
"delete_traininglevelqualification",
|
||||||
|
"add_traininglevelrequirement",
|
||||||
|
"change_traininglevelrequirement",
|
||||||
|
"delete_traininglevelrequirement"]
|
||||||
|
|
||||||
|
for permId in perms:
|
||||||
|
self.committee_group.permissions.add(Permission.objects.get(codename=permId))
|
||||||
|
|
||||||
|
self.committee_group.save()
|
||||||
|
|
||||||
|
def setup_useful_profiles(self):
|
||||||
|
supervisor = Profile.objects.create(username="supervisor", first_name="Super", last_name="Visor",
|
||||||
|
initials="SV",
|
||||||
|
email="supervisor@example.com", is_active=True,
|
||||||
|
is_staff=True, is_approved=True)
|
||||||
|
supervisor.set_password('supervisor')
|
||||||
|
supervisor.groups.add(Group.objects.get(name="Keyholders"))
|
||||||
|
supervisor.save()
|
||||||
|
models.TrainingLevelQualification.objects.create(
|
||||||
|
trainee=supervisor,
|
||||||
|
level=models.TrainingLevel.objects.filter(
|
||||||
|
level__gte=models.TrainingLevel.SUPERVISOR).exclude(
|
||||||
|
department=models.TrainingLevel.HAULAGE).exclude(
|
||||||
|
department__isnull=True).first(),
|
||||||
|
confirmed_on=timezone.now(),
|
||||||
|
confirmed_by=models.Trainee.objects.first())
|
||||||
|
|
||||||
|
committee_user = Profile.objects.create(username="committee", first_name="Committee", last_name="Member",
|
||||||
|
initials="CM",
|
||||||
|
email="committee@example.com", is_active=True, is_approved=True)
|
||||||
|
committee_user.groups.add(self.committee_group)
|
||||||
|
supervisor.groups.add(Group.objects.get(name="Keyholders"))
|
||||||
|
committee_user.set_password('committee')
|
||||||
|
committee_user.save()
|
||||||
@@ -124,7 +124,7 @@ class Command(BaseCommand):
|
|||||||
for child in root:
|
for child in root:
|
||||||
depths = [("Training_Started", models.TrainingItemQualification.STARTED),
|
depths = [("Training_Started", models.TrainingItemQualification.STARTED),
|
||||||
("Training_Complete", models.TrainingItemQualification.COMPLETE),
|
("Training_Complete", models.TrainingItemQualification.COMPLETE),
|
||||||
("Competency_Assessed", models.TrainingItemQualification.PASSED_OUT),]
|
("Competency_Assessed", models.TrainingItemQualification.PASSED_OUT), ]
|
||||||
|
|
||||||
for (depth, depth_index) in depths:
|
for (depth, depth_index) in depths:
|
||||||
if child.find('{}_Date'.format(depth)) is not None:
|
if child.find('{}_Date'.format(depth)) is not None:
|
||||||
|
|||||||
@@ -19,10 +19,9 @@ class Trainee(Profile, RevisionMixin):
|
|||||||
@property
|
@property
|
||||||
def is_technician(self):
|
def is_technician(self):
|
||||||
return self.level_qualifications.exclude(confirmed_on=None).select_related('level') \
|
return self.level_qualifications.exclude(confirmed_on=None).select_related('level') \
|
||||||
.filter(level__level=TrainingLevel.TECHNICIAN) \
|
.filter(level__level=TrainingLevel.TECHNICIAN) \
|
||||||
.exclude(level__department=TrainingLevel.HAULAGE) \
|
.exclude(level__department=TrainingLevel.HAULAGE) \
|
||||||
.exclude(level__department__isnull=True).exists()
|
.exclude(level__department__isnull=True).exists()
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_supervisor(self):
|
def is_supervisor(self):
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if request.user.is_supervisor or perms.training.change_traininglevel %}
|
{% if request.user.as_trainee.is_supervisor or perms.training.add_traininglevelrequirement %}
|
||||||
<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
|
||||||
@@ -78,9 +78,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.change_traininglevel %}<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.as_trainee.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.change_traininglevel %}<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.as_trainee.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.change_traininglevel %}<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.as_trainee.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% for level in object_list %}
|
{% for level in object_list %}
|
||||||
<div class="card mb-2">
|
<div class="card mb-2">
|
||||||
<div class="card-header">{{level}}</div>
|
<div class="card-header"><a href="{{level.get_absolute_url}}">{{level}}</a></div>
|
||||||
<div class="card-body">{{level.description|markdown}}</div>
|
<div class="card-body">{{level.description|markdown}}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -43,9 +43,11 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12 text-right">
|
<div class="col-sm-12 text-right">
|
||||||
{% include 'partials/add_qualification.html' %}
|
<div class="btn-group">
|
||||||
<a href="{% url 'trainee_item_detail' object.pk %}" class="btn btn-info"><span class="fas fa-info-circle"></span> View Detailed Record</a>
|
{% include 'partials/add_qualification.html' %}
|
||||||
<a href="{% url 'profile_detail' object.pk %}" class="btn btn-primary"><span class="fas fa-eye"></span> View User Profile</a>
|
<a href="{% url 'trainee_item_detail' object.pk %}" class="btn btn-info"><span class="fas fa-info-circle"></span> View Detailed Record</a>
|
||||||
|
<a href="{% url 'profile_detail' object.pk %}" class="btn btn-primary"><span class="fas fa-eye"></span> View User Profile</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
|||||||
@@ -19,7 +19,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.as_trainee.is_supervisor or perms.training.change_trainingitemqualification %}
|
||||||
<th></th>
|
<th></th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -32,8 +32,8 @@
|
|||||||
<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.as_trainee.is_supervisor or perms.training.change_trainingitemqualification %}
|
||||||
<td>{% button 'edit' 'edit_qualification' object.pk %}</td>
|
<td>{% button 'edit' 'edit_qualification' trainee.pk %}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 PyRIGS.decorators import permission_required_with_403
|
from training.decorators import has_perm_or_supervisor
|
||||||
|
|
||||||
from training import views, models
|
from training import views, models
|
||||||
from versioning.views import VersionHistory
|
from versioning.views import VersionHistory
|
||||||
@@ -10,12 +10,12 @@ urlpatterns = [
|
|||||||
path('items/', login_required(views.ItemList.as_view()), name='item_list'),
|
path('items/', login_required(views.ItemList.as_view()), name='item_list'),
|
||||||
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>/',
|
||||||
permission_required_with_403('RIGS.view_profile')(views.TraineeDetail.as_view()),
|
has_perm_or_supervisor('RIGS.view_profile')(views.TraineeDetail.as_view()),
|
||||||
name='trainee_detail'),
|
name='trainee_detail'),
|
||||||
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>/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/', login_required(views.AddQualification.as_view()),
|
path('trainee/<int:pk>/add_qualification/', has_perm_or_supervisor('training.add_trainingitemqualificaiton')(views.AddQualification.as_view()),
|
||||||
name='add_qualification'),
|
name='add_qualification'),
|
||||||
path('trainee/<int:pk>/edit_qualification/', permission_required_with_403('training.change_trainingitemqualification')(views.EditQualification.as_view()),
|
path('trainee/<int:pk>/edit_qualification/', has_perm_or_supervisor('training.change_trainingitemqualification')(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'),
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ class TraineeItemDetail(generic.ListView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
trainee = models.Trainee.objects.get(pk=self.kwargs['pk'])
|
trainee = models.Trainee.objects.get(pk=self.kwargs['pk'])
|
||||||
|
context["trainee"] = models.Trainee.objects.get(pk=self.kwargs['pk'])
|
||||||
context["page_title"] = "Detailed Training Record for <a href='{}'>{}</a>".format(trainee.get_absolute_url(), trainee)
|
context["page_title"] = "Detailed Training Record for <a href='{}'>{}</a>".format(trainee.get_absolute_url(), trainee)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user