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 %}
+
+{% 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 %}
+
+
+
+
+
+ | Cable Type |
+ Circuits |
+ Cores |
+ Quick Links |
+
+
+
+ {% for item in object_list %}
+
+ | {{ item }} |
+ {{ item.circuits }} |
+ {{ item.cores }} |
+
+ View
+ Edit
+ |
+
+ {% endfor %}
+
+
+
+{% 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/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 % #}
-
- Assets
-
-
- {# % endif % #}
- {# % if perms.assets.view_supplier % #}
-
- Suppliers
-
-
- {# % endif % #}
+
+ Assets
+
+
+
+ Suppliers
+
+
{% if perms.assets.view_asset %}
Recent Changes
{% endif %}