From a0491891e9f8a7c776a5da548286497133a92616 Mon Sep 17 00:00:00 2001 From: Arona Jones Date: Mon, 13 Apr 2020 15:54:43 +0100 Subject: [PATCH] Add 'CableTypes' (#406) * Move relevant fields and create migration to autogen cable types * CRUD and ordering * FIX: Prevent creating duplicate cable types * FIX: pep8/remove debug print * FIX: Meta migrations... :> * FIX: Update tests to match new UX * Move cabletype menu links into 'Assets' dropdown * Fix migration * Specify version of reportlab Should fix CI - looks like I went a bit too ham-handed in my requirements.txt purge last time... --- assets/admin.py | 7 ++- assets/forms.py | 15 +++++ .../commands/generateSampleAssetsData.py | 8 +-- assets/migrations/0011_auto_20200218_1617.py | 29 +++++++++ assets/migrations/0012_auto_20200218_1627.py | 26 ++++++++ assets/migrations/0013_auto_20200218_1639.py | 29 +++++++++ assets/migrations/0014_auto_20200218_1840.py | 17 ++++++ assets/models.py | 42 ++++++++----- assets/templates/cable_type_form.html | 61 +++++++++++++++++++ assets/templates/cable_type_list.html | 41 +++++++++++++ assets/templates/partials/asset_form.html | 5 +- assets/templates/partials/cable_form.html | 33 +++------- assets/tests/pages.py | 5 +- assets/tests/test_assets.py | 26 +++----- assets/urls.py | 5 ++ assets/views.py | 42 +++++++++++++ requirements.txt | 1 + templates/base_assets.html | 48 ++++++++------- 18 files changed, 349 insertions(+), 91 deletions(-) create mode 100644 assets/migrations/0011_auto_20200218_1617.py create mode 100644 assets/migrations/0012_auto_20200218_1627.py create mode 100644 assets/migrations/0013_auto_20200218_1639.py create mode 100644 assets/migrations/0014_auto_20200218_1840.py create mode 100644 assets/templates/cable_type_form.html create mode 100644 assets/templates/cable_type_list.html diff --git a/assets/admin.py b/assets/admin.py index 3e6c9d58..ca39ca3b 100644 --- a/assets/admin.py +++ b/assets/admin.py @@ -23,10 +23,15 @@ class SupplierAdmin(admin.ModelAdmin): @admin.register(assets.Asset) class AssetAdmin(admin.ModelAdmin): list_display = ['id', 'asset_id', 'description', 'category', 'status'] - list_filter = ['is_cable', 'category'] + list_filter = ['is_cable', 'category', 'status'] search_fields = ['id', 'asset_id', 'description'] +@admin.register(assets.CableType) +class CableTypeAdmin(admin.ModelAdmin): + list_display = ['id', '__str__', 'plug', 'socket', 'cores', 'circuits'] + + @admin.register(assets.Connector) class ConnectorAdmin(admin.ModelAdmin): list_display = ['id', '__str__', 'current_rating', 'voltage_rating', 'num_pins'] diff --git a/assets/forms.py b/assets/forms.py index ea05efd3..8fd07179 100644 --- a/assets/forms.py +++ b/assets/forms.py @@ -1,6 +1,7 @@ from django import forms from assets import models +from django.db.models import Q class AssetForm(forms.ModelForm): @@ -34,3 +35,17 @@ class SupplierForm(forms.ModelForm): class SupplierSearchForm(forms.Form): query = forms.CharField(required=False) + + +class CableTypeForm(forms.ModelForm): + class Meta: + model = models.CableType + fields = '__all__' + + def clean(self): + form_data = self.cleaned_data + queryset = models.CableType.objects.filter(Q(plug=form_data['plug']) & Q(socket=form_data['socket']) & Q(circuits=form_data['circuits']) & Q(cores=form_data['cores'])) + # Being identical to itself shouldn't count... + if queryset.exists() and self.instance.pk != queryset[0].pk: + raise forms.ValidationError("A cable type that exactly matches this one already exists, please use that instead.", code="notunique") + return form_data diff --git a/assets/management/commands/generateSampleAssetsData.py b/assets/management/commands/generateSampleAssetsData.py index 258551c2..67b8cc9d 100644 --- a/assets/management/commands/generateSampleAssetsData.py +++ b/assets/management/commands/generateSampleAssetsData.py @@ -78,6 +78,9 @@ class Command(BaseCommand): suppliers = models.Supplier.objects.all() connectors = models.Connector.objects.all() + for i in range(len(connectors)): + models.CableType.objects.create(plug=random.choice(connectors), socket=random.choice(connectors), circuits=random.choice(circuits), cores=random.choice(cores)) + for i in range(100): asset = models.Asset( asset_id='{}'.format(models.Asset.get_available_asset_id()), @@ -87,12 +90,9 @@ class Command(BaseCommand): date_acquired=timezone.now().date(), is_cable=True, - plug=random.choice(connectors), - socket=random.choice(connectors), + cable_type=random.choice(models.CableType.objects.all()), csa=random.choice(csas), length=random.choice(lengths), - circuits=random.choice(circuits), - cores=random.choice(circuits) ) if i % 5 == 0: diff --git a/assets/migrations/0011_auto_20200218_1617.py b/assets/migrations/0011_auto_20200218_1617.py new file mode 100644 index 00000000..2debea8c --- /dev/null +++ b/assets/migrations/0011_auto_20200218_1617.py @@ -0,0 +1,29 @@ +# Generated by Django 2.0.13 on 2020-02-18 16:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0010_auto_20200219_1444'), + ] + + operations = [ + migrations.CreateModel( + name='CableType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('circuits', models.IntegerField(blank=True, null=True)), + ('cores', models.IntegerField(blank=True, null=True)), + ('plug', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plug', to='assets.Connector')), + ('socket', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='socket', to='assets.Connector')), + ], + ), + migrations.AddField( + model_name='asset', + name='cable_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.CableType'), + ), + ] diff --git a/assets/migrations/0012_auto_20200218_1627.py b/assets/migrations/0012_auto_20200218_1627.py new file mode 100644 index 00000000..bf61b845 --- /dev/null +++ b/assets/migrations/0012_auto_20200218_1627.py @@ -0,0 +1,26 @@ +# Generated by Django 2.0.13 on 2020-02-18 16:27 + +from django.db import migrations +from django.db.models import Q + + +def move_cable_type_data(apps, schema_editor): + Asset = apps.get_model('assets', 'Asset') + CableType = apps.get_model('assets', 'CableType') + for asset in Asset.objects.filter(is_cable=True): + # Only create one type per...well...type + if(not CableType.objects.filter(Q(plug=asset.plug) & Q(socket=asset.socket))): + cabletype = CableType.objects.create(plug=asset.plug, socket=asset.socket, circuits=asset.circuits, cores=asset.cores) + asset.save() + cabletype.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0011_auto_20200218_1617'), + ] + + operations = [ + migrations.RunPython(move_cable_type_data) + ] diff --git a/assets/migrations/0013_auto_20200218_1639.py b/assets/migrations/0013_auto_20200218_1639.py new file mode 100644 index 00000000..c7cebec1 --- /dev/null +++ b/assets/migrations/0013_auto_20200218_1639.py @@ -0,0 +1,29 @@ +# Generated by Django 2.0.13 on 2020-02-18 16:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0012_auto_20200218_1627'), + ] + + operations = [ + migrations.RemoveField( + model_name='asset', + name='circuits', + ), + migrations.RemoveField( + model_name='asset', + name='cores', + ), + migrations.RemoveField( + model_name='asset', + name='plug', + ), + migrations.RemoveField( + model_name='asset', + name='socket', + ), + ] diff --git a/assets/migrations/0014_auto_20200218_1840.py b/assets/migrations/0014_auto_20200218_1840.py new file mode 100644 index 00000000..a5c36c40 --- /dev/null +++ b/assets/migrations/0014_auto_20200218_1840.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.13 on 2020-02-18 18:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0013_auto_20200218_1639'), + ] + + operations = [ + migrations.AlterModelOptions( + name='cabletype', + options={'ordering': ['plug', 'socket', '-circuits']}, + ), + ] diff --git a/assets/models.py b/assets/models.py index a6c767f1..4f6f96ec 100644 --- a/assets/models.py +++ b/assets/models.py @@ -63,6 +63,21 @@ class Connector(models.Model): return self.description +class CableType(models.Model): + class Meta: + ordering = ['plug', 'socket', '-circuits'] + + circuits = models.IntegerField(blank=True, null=True) + cores = models.IntegerField(blank=True, null=True) + plug = models.ForeignKey(Connector, on_delete=models.SET_NULL, + related_name='plug', blank=True, null=True) + socket = models.ForeignKey(Connector, on_delete=models.SET_NULL, + related_name='socket', blank=True, null=True) + + def __str__(self): + return "%s → %s" % (self.plug.description, self.socket.description) + + @reversion.register class Asset(models.Model, RevisionMixin): class Meta: @@ -88,16 +103,11 @@ class Asset(models.Model, RevisionMixin): # Cable assets is_cable = models.BooleanField(default=False) - plug = models.ForeignKey(Connector, on_delete=models.SET_NULL, - related_name='plug', blank=True, null=True) - socket = models.ForeignKey(Connector, on_delete=models.SET_NULL, - related_name='socket', blank=True, null=True) + cable_type = models.ForeignKey(to=CableType, blank=True, null=True, on_delete=models.SET_NULL) length = models.DecimalField(decimal_places=1, max_digits=10, blank=True, null=True, help_text='m') csa = models.DecimalField(decimal_places=2, max_digits=10, blank=True, null=True, help_text='mm^2') - circuits = models.IntegerField(blank=True, null=True) - cores = models.IntegerField(blank=True, null=True) # Hidden asset_id components # For example, if asset_id was "C1001" then asset_id_prefix would be "C" and number "1001" @@ -127,7 +137,7 @@ class Asset(models.Model, RevisionMixin): def __str__(self): out = str(self.asset_id) + ' - ' + self.description if self.is_cable: - out += '{} - {}m - {}'.format(self.plug, self.length, self.socket) + out += '{} - {}m - {}'.format(self.cable_type.plug, self.length, self.cable_type.socket) return out def clean(self): @@ -152,14 +162,16 @@ class Asset(models.Model, RevisionMixin): errdict["length"] = ["The length of a cable must be more than 0"] if not self.csa or self.csa <= 0: errdict["csa"] = ["The CSA of a cable must be more than 0"] - if not self.circuits or self.circuits <= 0: - errdict["circuits"] = ["There must be at least one circuit in a cable"] - if not self.cores or self.cores <= 0: - errdict["cores"] = ["There must be at least one core in a cable"] - if self.socket is None: - errdict["socket"] = ["A cable must have a socket"] - if self.plug is None: - errdict["plug"] = ["A cable must have a plug"] + if not self.cable_type: + errdict["cable_type"] = ["A cable must have a type"] + # if not self.circuits or self.circuits <= 0: + # errdict["circuits"] = ["There must be at least one circuit in a cable"] + # if not self.cores or self.cores <= 0: + # errdict["cores"] = ["There must be at least one core in a cable"] + # if self.socket is None: + # errdict["socket"] = ["A cable must have a socket"] + # if self.plug is None: + # errdict["plug"] = ["A cable must have a plug"] if errdict != {}: # If there was an error when validation raise ValidationError(errdict) diff --git a/assets/templates/cable_type_form.html b/assets/templates/cable_type_form.html new file mode 100644 index 00000000..3f44ed71 --- /dev/null +++ b/assets/templates/cable_type_form.html @@ -0,0 +1,61 @@ +{% extends 'base_assets.html' %} +{% load widget_tweaks %} +{% block title %}Cable Type{% endblock %} + +{% block content %} + +{% if create %} +
+{% elif edit %} + +{% endif %} +{% include 'form_errors.html' %} +{% csrf_token %} + +
+
+ {% if create or edit %} +
+ + {% render_field form.plug|add_class:'form-control'%} +
+
+ + {% render_field form.socket|add_class:'form-control'%} +
+
+ + {% render_field form.circuits|add_class:'form-control' value=object.circuits %} +
+
+ + {% render_field form.cores|add_class:'form-control' value=object.cores %} +
+
+ +
+ +
+ {% else %} +
+
Socket
+
{{ object.socket|default_if_none:'-' }}
+ +
Plug
+
{{ object.plug|default_if_none:'-' }}
+ +
Circuits
+
{{ object.circuits|default_if_none:'-' }}
+ +
Cores
+
{{ object.cores|default_if_none:'-' }}
+
+ {% endif %} +
+
+
+{% endblock %} diff --git a/assets/templates/cable_type_list.html b/assets/templates/cable_type_list.html new file mode 100644 index 00000000..74e97407 --- /dev/null +++ b/assets/templates/cable_type_list.html @@ -0,0 +1,41 @@ +{% extends 'base_assets.html' %} +{% block title %}Supplier List{% endblock %} +{% load paginator from filters %} +{% load widget_tweaks %} + +{% block content %} + + + + + + + + + + + + + {% for item in object_list %} + + + + + + + {% endfor %} + +
Cable TypeCircuitsCoresQuick Links
{{ item }}{{ item.circuits }}{{ item.cores }} + View + Edit +
+ +{% if is_paginated %} +
+ {% paginator %} +
+{% endif %} + +{% endblock %} diff --git a/assets/templates/partials/asset_form.html b/assets/templates/partials/asset_form.html index 45424992..a1fd4c88 100644 --- a/assets/templates/partials/asset_form.html +++ b/assets/templates/partials/asset_form.html @@ -23,7 +23,6 @@ {% render_field form.category|add_class:'form-control'%} - {% render_field form.is_cable|attr:'onchange=checkIfCableHidden()' %}
{% render_field form.status|add_class:'form-control'%} @@ -32,6 +31,10 @@ {% render_field form.serial_number|add_class:'form-control' value=object.serial_number %}
+
+ + {% render_field form.is_cable|attr:'onchange=checkIfCableHidden()' %} +
diff --git a/assets/templates/partials/cable_form.html b/assets/templates/partials/cable_form.html index e5deb006..e8aa7607 100644 --- a/assets/templates/partials/cable_form.html +++ b/assets/templates/partials/cable_form.html @@ -6,12 +6,10 @@
{% if create or edit or duplicate %}
- - {% render_field form.plug|add_class:'form-control'%} -
-
- - {% render_field form.socket|add_class:'form-control'%} + +
+ {% render_field form.cable_type|add_class:'form-control' %} +
@@ -27,33 +25,16 @@ {{ form.csa.help_text }}
-
- - {% render_field form.circuits|add_class:'form-control' value=object.circuits %} -
-
- - {% render_field form.cores|add_class:'form-control' value=object.cores %} -
{% else %}
-
Socket
-
{{ object.socket|default_if_none:'-' }}
- -
Plug
-
{{ object.plug|default_if_none:'-' }}
+
Cable Type
+
{{ object.cable_type|default_if_none:'-' }}
Length
{{ object.length|default_if_none:'-' }}m
Cross Sectional Area
-
{{ object.csa|default_if_none:'-' }}m^2
- -
Circuits
-
{{ object.circuits|default_if_none:'-' }}
- -
Cores
-
{{ object.cores|default_if_none:'-' }}
+
{{ object.csa|default_if_none:'-' }}mm²
{% endif %}
diff --git a/assets/tests/pages.py b/assets/tests/pages.py index e31859e1..c68d2568 100644 --- a/assets/tests/pages.py +++ b/assets/tests/pages.py @@ -82,12 +82,9 @@ class AssetForm(FormPage): 'category': (regions.SingleSelectPicker, (By.ID, 'id_category')), 'status': (regions.SingleSelectPicker, (By.ID, 'id_status')), - 'plug': (regions.SingleSelectPicker, (By.ID, 'id_plug')), - 'socket': (regions.SingleSelectPicker, (By.ID, 'id_socket')), + 'cable_type': (regions.SingleSelectPicker, (By.ID, 'id_cable_type')), 'length': (regions.TextBox, (By.ID, 'id_length')), 'csa': (regions.TextBox, (By.ID, 'id_csa')), - 'circuits': (regions.TextBox, (By.ID, 'id_circuits')), - 'cores': (regions.TextBox, (By.ID, 'id_cores')) } @property diff --git a/assets/tests/test_assets.py b/assets/tests/test_assets.py index 37cf816a..06d35701 100644 --- a/assets/tests/test_assets.py +++ b/assets/tests/test_assets.py @@ -98,6 +98,7 @@ class TestAssetForm(AutoLoginTest): self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry") self.parent = models.Asset.objects.create(asset_id="9000", description="Shelf", status=self.status, category=self.category, date_acquired=datetime.date(2000, 1, 1)) self.connector = models.Connector.objects.create(description="IEC", current_rating=10, voltage_rating=240, num_pins=3) + self.cable_type = models.CableType.objects.create(plug=self.connector, socket=self.connector, circuits=1, cores=3) self.page = pages.AssetCreate(self.driver, self.live_server_url).open() def test_asset_create(self): @@ -154,12 +155,10 @@ class TestAssetForm(AutoLoginTest): self.page.is_cable = True self.assertTrue(self.driver.find_element_by_id('cable-table').is_displayed()) - self.page.plug = "IEC" + self.page.cable_type = "IEC → IEC" self.page.socket = "IEC" self.page.length = 10 self.page.csa = "1.5" - self.page.circuits = 1 - self.page.cores = 3 self.page.submit() self.assertTrue(self.page.success) @@ -375,7 +374,8 @@ class TestFormValidation(TestCase): cls.status = models.AssetStatus.objects.create(name="Broken", should_show=True) cls.asset = models.Asset.objects.create(asset_id="9999", description="The Office", status=cls.status, category=cls.category, date_acquired=datetime.date(2018, 6, 15)) cls.connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3) - cls.cable_asset = models.Asset.objects.create(asset_id="666", description="125A -> Jack", comments="The cable from Hell...", status=cls.status, category=cls.category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, plug=cls.connector, socket=cls.connector, length=10, csa="1.5", circuits=1, cores=3) + cls.cable_type = models.CableType.objects.create(circuits=11, cores=3, plug=cls.connector, socket=cls.connector) + cls.cable_asset = models.Asset.objects.create(asset_id="666", description="125A -> Jack", comments="The cable from Hell...", status=cls.status, category=cls.category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cls.cable_type, length=10, csa="1.5") def setUp(self): self.profile.set_password('testuser') @@ -399,12 +399,9 @@ class TestFormValidation(TestCase): response = self.client.post(url, {'asset_id': 'X$%A', 'is_cable': True}) self.assertFormError(response, 'form', 'asset_id', 'An Asset ID can only consist of letters and numbers, with a final number') - self.assertFormError(response, 'form', 'plug', 'A cable must have a plug') - self.assertFormError(response, 'form', 'socket', 'A cable must have a socket') + self.assertFormError(response, 'form', 'cable_type', 'A cable must have a type') self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0') self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') - self.assertFormError(response, 'form', 'circuits', 'There must be at least one circuit in a cable') - self.assertFormError(response, 'form', 'cores', 'There must be at least one core in a cable') # Given that validation is done at model level it *shouldn't* need retesting...gonna do it anyway! def test_asset_edit(self): @@ -422,24 +419,19 @@ class TestFormValidation(TestCase): def test_cable_edit(self): url = reverse('asset_update', kwargs={'pk': self.cable_asset.asset_id}) # TODO Why do I have to send is_cable=True here? - response = self.client.post(url, {'is_cable': True, 'length': -3, 'csa': -3, 'circuits': -4, 'cores': -8}) + response = self.client.post(url, {'is_cable': True, 'length': -3, 'csa': -3}) - # Can't figure out how to select the 'none' option... - # self.assertFormError(response, 'form', 'plug', 'A cable must have a plug') - # self.assertFormError(response, 'form', 'socket', 'A cable must have a socket') + # TODO Can't figure out how to select the 'none' option... + # self.assertFormError(response, 'form', 'cable_type', 'A cable must have a type') self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0') self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') - self.assertFormError(response, 'form', 'circuits', 'There must be at least one circuit in a cable') - self.assertFormError(response, 'form', 'cores', 'There must be at least one core in a cable') def test_asset_duplicate(self): url = reverse('asset_duplicate', kwargs={'pk': self.cable_asset.asset_id}) - response = self.client.post(url, {'is_cable': True, 'length': 0, 'csa': 0, 'circuits': 0, 'cores': 0}) + response = self.client.post(url, {'is_cable': True, 'length': 0, 'csa': 0}) self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0') self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') - self.assertFormError(response, 'form', 'circuits', 'There must be at least one circuit in a cable') - self.assertFormError(response, 'form', 'cores', 'There must be at least one core in a cable') class TestSampleDataGenerator(TestCase): diff --git a/assets/urls.py b/assets/urls.py index dc8e021c..8bc3c2a0 100644 --- a/assets/urls.py +++ b/assets/urls.py @@ -22,6 +22,11 @@ urlpatterns = [ path('activity', permission_required_with_403('assets.view_asset') (views.ActivityTable.as_view()), name='asset_activity_table'), + path('cabletype/list/', permission_required_with_403('assets.view_cable_type')(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//update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'), + path('cabletype//detail/', permission_required_with_403('assets.view_cable_type')(views.CableTypeDetail.as_view()), name='cable_type_detail'), + path('asset/search/', views.AssetSearch.as_view(), name='asset_search_json'), path('asset/id//embed/', xframe_options_exempt( diff --git a/assets/views.py b/assets/views.py index 29eaa7aa..29e86872 100644 --- a/assets/views.py +++ b/assets/views.py @@ -252,3 +252,45 @@ class ActivityTable(versioning.ActivityTable): versions = versioning.RIGSVersion.objects.get_for_multiple_models( [models.Asset, models.Supplier]) return versions + + +class CableTypeList(generic.ListView): + model = models.CableType + template_name = 'cable_type_list.html' + paginate_by = 40 + # ordering = ['__str__'] + + +class CableTypeDetail(generic.DetailView): + model = models.CableType + template_name = 'cable_type_form.html' + + +class CableTypeCreate(generic.CreateView): + model = models.CableType + template_name = "cable_type_form.html" + form_class = forms.CableTypeForm + + def get_context_data(self, **kwargs): + context = super(CableTypeCreate, self).get_context_data(**kwargs) + context["create"] = True + + return context + + def get_success_url(self): + return reverse("cable_type_detail", kwargs={"pk": self.object.pk}) + + +class CableTypeUpdate(generic.UpdateView): + model = models.CableType + template_name = "cable_type_form.html" + form_class = forms.CableTypeForm + + def get_context_data(self, **kwargs): + context = super(CableTypeUpdate, self).get_context_data(**kwargs) + context["edit"] = True + + return context + + def get_success_url(self): + return reverse("cable_type_detail", kwargs={"pk": self.object.pk}) diff --git a/requirements.txt b/requirements.txt index e737650a..569e0366 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,4 +21,5 @@ requests==2.23.0 selenium==3.141.0 simplejson==3.17.0 whitenoise==5.0.1 +reportlab==3.4.0 z3c.rml==3.9.1 diff --git a/templates/base_assets.html b/templates/base_assets.html index 636c70df..7f226963 100644 --- a/templates/base_assets.html +++ b/templates/base_assets.html @@ -10,29 +10,31 @@ {% endblock %} {% block titleelements %} - {# % if perms.assets.view_asset % #} - - {# % endif % #} - {# % if perms.assets.view_supplier % #} - - {# % endif % #} + + {% if perms.assets.view_asset %}
  • Recent Changes
  • {% endif %}