Compare commits

...

10 Commits

Author SHA1 Message Date
9c1c88a8d6 Specify version of reportlab
Should fix CI - looks like I went a bit too ham-handed in my requirements.txt purge last time...
2020-04-13 15:35:05 +01:00
2a403d9940 Fix migration 2020-04-13 01:36:59 +01:00
e2c495b037 Merge branch 'master' into assets_cabletypes 2020-04-13 01:23:20 +01:00
9cd90d0d52 Move cabletype menu links into 'Assets' dropdown 2020-02-27 10:39:25 +00:00
a707fe847e FIX: Update tests to match new UX 2020-02-27 10:20:50 +00:00
09d1b42728 FIX: Meta migrations... :> 2020-02-18 18:40:27 +00:00
e7324e2d10 FIX: pep8/remove debug print 2020-02-18 18:37:54 +00:00
d1ccb561f5 FIX: Prevent creating duplicate cable types 2020-02-18 18:35:39 +00:00
c60771e613 CRUD and ordering 2020-02-18 18:02:04 +00:00
386fec1f01 Move relevant fields and create migration to autogen cable types 2020-02-18 17:00:46 +00:00
18 changed files with 349 additions and 91 deletions

View File

@@ -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']

View File

@@ -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

View File

@@ -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:

View File

@@ -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'),
),
]

View File

@@ -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)
]

View File

@@ -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',
),
]

View File

@@ -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']},
),
]

View File

@@ -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)

View File

@@ -0,0 +1,61 @@
{% extends 'base_assets.html' %}
{% load widget_tweaks %}
{% block title %}Cable Type{% endblock %}
{% block content %}
<div class="page-header">
<h1>
{% if create %}Create{% elif edit %}Edit{% endif %} Cable Type
</h1>
</div>
{% if create %}
<form method="POST" action="{% url 'cable_type_create'%}">
{% elif edit %}
<form method="POST" action="{% url 'cable_type_update' object.id %}">
{% endif %}
{% include 'form_errors.html' %}
{% csrf_token %}
<input type="hidden" name="id" value="{{ object.id|default:0 }}" hidden=true>
<div class="row">
<div class="col-sm-12">
{% if create or edit %}
<div class="form-group">
<label for="{{ form.plug.id_for_label }}">Plug</label>
{% render_field form.plug|add_class:'form-control'%}
</div>
<div class="form-group">
<label for="{{ form.socket.id_for_label }}">Socket</label>
{% render_field form.socket|add_class:'form-control'%}
</div>
<div class="form-group">
<label for="{{ form.circuits.id_for_label }}">Circuits</label>
{% render_field form.circuits|add_class:'form-control' value=object.circuits %}
</div>
<div class="form-group">
<label for="{{ form.cores.id_for_label }}">Cores</label>
{% render_field form.cores|add_class:'form-control' value=object.cores %}
</div>
<div class="pull-left">
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
<br>
<button type="reset" class="btn btn-link">Cancel</button>
</div>
{% else %}
<dl>
<dt>Socket</dt>
<dd>{{ object.socket|default_if_none:'-' }}</dd>
<dt>Plug</dt>
<dd>{{ object.plug|default_if_none:'-' }}</dd>
<dt>Circuits</dt>
<dd>{{ object.circuits|default_if_none:'-' }}</dd>
<dt>Cores</dt>
<dd>{{ object.cores|default_if_none:'-' }}</dd>
</dl>
{% endif %}
</div>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,41 @@
{% extends 'base_assets.html' %}
{% block title %}Supplier List{% endblock %}
{% load paginator from filters %}
{% load widget_tweaks %}
{% block content %}
<div class="page-header">
<h1>Cable Type List</h1>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Cable Type</th>
<th>Circuits</th>
<th>Cores</th>
<th>Quick Links</th>
</tr>
</thead>
<tbody>
{% for item in object_list %}
<tr>
<td>{{ item }}</td>
<td>{{ item.circuits }}</td>
<td>{{ item.cores }}</td>
<td>
<a href="{% url 'cable_type_detail' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-eye-open"></i> View</a>
<a href="{% url 'cable_type_update' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if is_paginated %}
<div class="text-center">
{% paginator %}
</div>
{% endif %}
{% endblock %}

View File

@@ -23,7 +23,6 @@
<label for="{{ form.category.id_for_label }}" >Category</label>
{% render_field form.category|add_class:'form-control'%}
</div>
{% render_field form.is_cable|attr:'onchange=checkIfCableHidden()' %} <label for="{{ form.is_cable.id_for_label }}">Cable?</label>
<div class="form-group">
<label for="{{ form.status.id_for_label }}" >Status</label>
{% render_field form.status|add_class:'form-control'%}
@@ -32,6 +31,10 @@
<label for="{{ form.serial_number.id_for_label }}">Serial Number</label>
{% render_field form.serial_number|add_class:'form-control' value=object.serial_number %}
</div>
<div class="form-group">
<label for="{{ form.is_cable.id_for_label }}">Cable?</label>
{% render_field form.is_cable|attr:'onchange=checkIfCableHidden()' %}
</div>
<!---TODO: Lower default number of lines in comments box-->
<div class="form-group">
<label for="{{ form.comments.id_for_label }}">Comments</label>

View File

@@ -6,12 +6,10 @@
<div class="panel-body">
{% if create or edit or duplicate %}
<div class="form-group">
<label for="{{ form.plug.id_for_label }}">Plug</label>
{% render_field form.plug|add_class:'form-control'%}
</div>
<div class="form-group">
<label for="{{ form.socket.id_for_label }}">Socket</label>
{% render_field form.socket|add_class:'form-control'%}
<label for="{{ form.cable_type.id_for_label }}">Cable Type</label>
<div class="input-group">
{% render_field form.cable_type|add_class:'form-control' %}
</div>
</div>
<div class="form-group">
<label for="{{ form.length.id_for_label }}">Length</label>
@@ -27,33 +25,16 @@
<span class="input-group-addon">{{ form.csa.help_text }}</span>
</div>
</div>
<div class="form-group">
<label for="{{ form.circuits.id_for_label }}">Circuits</label>
{% render_field form.circuits|add_class:'form-control' value=object.circuits %}
</div>
<div class="form-group">
<label for="{{ form.cores.id_for_label }}">Cores</label>
{% render_field form.cores|add_class:'form-control' value=object.cores %}
</div>
{% else %}
<dl>
<dt>Socket</dt>
<dd>{{ object.socket|default_if_none:'-' }}</dd>
<dt>Plug</dt>
<dd>{{ object.plug|default_if_none:'-' }}</dd>
<dt>Cable Type</dt>
<dd>{{ object.cable_type|default_if_none:'-' }}</dd>
<dt>Length</dt>
<dd>{{ object.length|default_if_none:'-' }}m</dd>
<dt>Cross Sectional Area</dt>
<dd>{{ object.csa|default_if_none:'-' }}m^2</dd>
<dt>Circuits</dt>
<dd>{{ object.circuits|default_if_none:'-' }}</dd>
<dt>Cores</dt>
<dd>{{ object.cores|default_if_none:'-' }}</dd>
<dd>{{ object.csa|default_if_none:'-' }}m</dd>
</dl>
{% endif %}
</div>

View File

@@ -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

View File

@@ -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):

View File

@@ -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/<int:pk>/update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'),
path('cabletype/<int:pk>/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/<str:pk>/embed/',
xframe_options_exempt(

View File

@@ -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})

View File

@@ -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

View File

@@ -10,29 +10,31 @@
{% endblock %}
{% block titleelements %}
{# % if perms.assets.view_asset % #}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Assets<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{% url 'asset_list' %}"><span class="glyphicon glyphicon-list"></span> List Assets</a></li>
{% if perms.assets.add_asset %}
<li><a href="{% url 'asset_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Asset</a></li>
{% endif %}
</ul>
</li>
{# % endif % #}
{# % if perms.assets.view_supplier % #}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> Suppliers<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{% url 'supplier_list' %}"><span class="glyphicon glyphicon-list"></span>
List Suppliers</a></li>
{% if perms.assets.add_supplier %}
<li><a href="{% url 'supplier_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Supplier</a></li>
{% endif %}
</ul>
</li>
{# % endif % #}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Assets<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{% url 'asset_list' %}"><span class="glyphicon glyphicon-list"></span> List Assets</a></li>
{% if perms.assets.add_asset %}
<li><a href="{% url 'asset_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Asset</a></li>
{% endif %}
<li role="separator" class="divider"></li>
<li><a href="{% url 'cable_type_list' %}"><span class="glyphicon glyphicon-list"></span>
List Cable Types</a></li>
{% if perms.assets.add_cable_type %}
<li><a href="{% url 'cable_type_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Cable Type</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> Suppliers<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{% url 'supplier_list' %}"><span class="glyphicon glyphicon-list"></span>
List Suppliers</a></li>
{% if perms.assets.add_supplier %}
<li><a href="{% url 'supplier_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Supplier</a></li>
{% endif %}
</ul>
</li>
{% if perms.assets.view_asset %}
<li><a href="{% url 'asset_activity_table' %}">Recent Changes</a></li>
{% endif %}