mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-14 02:29:41 +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):
|
||||
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
|
||||
def admins(cls):
|
||||
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()
|
||||
|
||||
# Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
|
||||
|
||||
|
||||
@register.filter(needs_autoescape=True)
|
||||
@register.filter(needs_autoescape=True) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
|
||||
def get_field(obj, field, autoescape=True):
|
||||
value = getattr(obj, field)
|
||||
if(isinstance(value, bool)):
|
||||
@@ -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}
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
@register.simple_tag # TODO Can these be done with annotation/aggregation?
|
||||
def invoices_waiting():
|
||||
return len(models.Event.objects.waiting_invoices())
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import urllib.parse
|
||||
|
||||
|
||||
class AssetIDConverter: # Forces lowercase to uppercase
|
||||
regex = '[^/]+'
|
||||
|
||||
@@ -6,3 +9,16 @@ class AssetIDConverter: # Forces lowercase to uppercase
|
||||
|
||||
def to_url(self, value):
|
||||
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)
|
||||
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
|
||||
status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False)
|
||||
is_cable = forms.BooleanField(required=False)
|
||||
|
||||
|
||||
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) {
|
||||
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({
|
||||
url: url,
|
||||
success: function(){
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% load paginator from filters %}
|
||||
{% load button from filters %}
|
||||
{% load ids_from_objects from asset_tags %}
|
||||
{% load widget_tweaks %}
|
||||
{% load static %}
|
||||
|
||||
@@ -74,6 +75,10 @@
|
||||
<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" %}
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
@@ -81,6 +86,9 @@
|
||||
<div class="row my-2">
|
||||
<div class="col text-right px-0">
|
||||
{% 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 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 '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>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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
|
||||
|
||||
register_converter(converters.AssetIDConverter, 'asset')
|
||||
register_converter(converters.ListConverter, 'list')
|
||||
|
||||
urlpatterns = [
|
||||
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')
|
||||
(views.AssetDuplicate.as_view()), name='asset_duplicate'),
|
||||
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/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import simplejson
|
||||
import random
|
||||
import base64
|
||||
from io import BytesIO
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core import serializers
|
||||
@@ -11,10 +14,13 @@ from django.utils.decorators import method_decorator
|
||||
from django.views import generic
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
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 barcode import Code39
|
||||
from barcode.writer import ImageWriter
|
||||
from z3c.rml import rml2pdf
|
||||
|
||||
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \
|
||||
is_ajax, OEmbedView
|
||||
@@ -52,6 +58,9 @@ class AssetList(LoginRequiredMixin, generic.ListView):
|
||||
else:
|
||||
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']:
|
||||
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})
|
||||
|
||||
|
||||
class GenerateLabel(generic.View):
|
||||
def get(self, request, pk):
|
||||
black = (0, 0, 0)
|
||||
white = (255, 255, 255)
|
||||
size = (700, 200)
|
||||
font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20)
|
||||
obj = get_object_or_404(models.Asset, asset_id=pk)
|
||||
def generate_label(pk):
|
||||
black = (0, 0, 0)
|
||||
white = (255, 255, 255)
|
||||
size = (700, 200)
|
||||
font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20)
|
||||
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)
|
||||
csa = "CSA: {}mm²".format(obj.csa)
|
||||
|
||||
image = Image.new("RGB", size, white)
|
||||
logo = Image.open("static/imgs/square_logo.png")
|
||||
draw = ImageDraw.Draw(image)
|
||||
image = Image.new("RGB", size, white)
|
||||
logo = Image.open("static/imgs/square_logo.png")
|
||||
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((350, 170), csa, fill=black, font=font)
|
||||
draw.multiline_text((500, 140), "TEC PA & Lighting\n(0115) 84 68720", 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)
|
||||
|
||||
barcode = Code39(str(obj.asset_id), writer=ImageWriter())
|
||||
barcode = Code39(str(obj.asset_id), writer=ImageWriter())
|
||||
|
||||
logo_size = (200, 200)
|
||||
image.paste(logo.resize(logo_size, Image.ANTIALIAS))
|
||||
barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False})
|
||||
width, height = barcode_image.size
|
||||
image.paste(barcode_image.crop((0, 0, width, 135)), (int(((size[0] + logo_size[0]) - width) / 2), 0))
|
||||
logo_size = (200, 200)
|
||||
image.paste(logo.resize(logo_size, Image.ANTIALIAS))
|
||||
barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False})
|
||||
width, height = barcode_image.size
|
||||
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")
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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.db import transaction
|
||||
from django.utils import timezone
|
||||
@@ -31,7 +32,7 @@ class Command(BaseCommand):
|
||||
self.setup_categories()
|
||||
self.setup_items()
|
||||
self.setup_levels()
|
||||
self.setup_supervisor()
|
||||
# call_command('generate_sample_training_users')
|
||||
print("Done generating training data")
|
||||
|
||||
def setup_categories(self):
|
||||
@@ -143,7 +144,13 @@ class Command(BaseCommand):
|
||||
"Wiki Editing"]
|
||||
|
||||
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)
|
||||
|
||||
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])
|
||||
self.levels.append(technician)
|
||||
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:
|
||||
depths = [("Training_Started", models.TrainingItemQualification.STARTED),
|
||||
("Training_Complete", models.TrainingItemQualification.COMPLETE),
|
||||
("Competency_Assessed", models.TrainingItemQualification.PASSED_OUT),]
|
||||
("Competency_Assessed", models.TrainingItemQualification.PASSED_OUT), ]
|
||||
|
||||
for (depth, depth_index) in depths:
|
||||
if child.find('{}_Date'.format(depth)) is not None:
|
||||
|
||||
@@ -19,10 +19,9 @@ class Trainee(Profile, RevisionMixin):
|
||||
@property
|
||||
def is_technician(self):
|
||||
return self.level_qualifications.exclude(confirmed_on=None).select_related('level') \
|
||||
.filter(level__level=TrainingLevel.TECHNICIAN) \
|
||||
.exclude(level__department=TrainingLevel.HAULAGE) \
|
||||
.exclude(level__department__isnull=True).exists()
|
||||
|
||||
.filter(level__level=TrainingLevel.TECHNICIAN) \
|
||||
.exclude(level__department=TrainingLevel.HAULAGE) \
|
||||
.exclude(level__department__isnull=True).exists()
|
||||
|
||||
@property
|
||||
def is_supervisor(self):
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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">
|
||||
<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
|
||||
@@ -78,9 +78,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.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.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.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.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.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.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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
{% for level in object_list %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
|
||||
@@ -43,9 +43,11 @@
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12 text-right">
|
||||
{% include 'partials/add_qualification.html' %}
|
||||
<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 class="btn-group">
|
||||
{% include 'partials/add_qualification.html' %}
|
||||
<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 class="row mb-3">
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<th>Date</th>
|
||||
<th>Supervisor</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>
|
||||
{% endif %}
|
||||
</tr>
|
||||
@@ -32,8 +32,8 @@
|
||||
<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 %}
|
||||
<td>{% button 'edit' 'edit_qualification' object.pk %}</td>
|
||||
{% if request.user.as_trainee.is_supervisor or perms.training.change_trainingitemqualification %}
|
||||
<td>{% button 'edit' 'edit_qualification' trainee.pk %}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
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 versioning.views import VersionHistory
|
||||
@@ -10,12 +10,12 @@ urlpatterns = [
|
||||
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/<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'),
|
||||
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/', login_required(views.AddQualification.as_view()),
|
||||
path('trainee/<int:pk>/history', has_perm_or_supervisor('RIGS.view_profile')(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
|
||||
path('trainee/<int:pk>/add_qualification/', has_perm_or_supervisor('training.add_trainingitemqualificaiton')(views.AddQualification.as_view()),
|
||||
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'),
|
||||
|
||||
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):
|
||||
context = super().get_context_data(**kwargs)
|
||||
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)
|
||||
return context
|
||||
|
||||
|
||||
Reference in New Issue
Block a user