mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-03-07 04:28:23 +00:00
Merge branch 'master' into markdown
# Conflicts: # RIGS/static/scss/screen.scss # RIGS/templates/RIGS/event_detail.html # RIGS/templates/RIGS/event_form.html # RIGS/templates/RIGS/event_table.html # RIGS/templates/RIGS/item_row.html # RIGS/templates/RIGS/item_table.html # RIGS/templates/RIGS/organisation_detail.html # RIGS/templates/RIGS/organisation_form.html # RIGS/templates/RIGS/person_detail.html # RIGS/templates/RIGS/person_form.html # RIGS/templates/RIGS/rigboard.html # RIGS/templates/RIGS/venue_detail.html # RIGS/templates/RIGS/venue_form.html # RIGS/templates/event_print.xml # RIGS/templates/event_print_page.xml # RIGS/templates/item_modal.html # RIGS/tests/test_unit.py # assets/templates/asset_create.html # assets/templates/asset_update.html # assets/templates/partials/asset_detail_form.html # requirements.txt # templates/base.html
This commit is contained in:
@@ -0,0 +1 @@
|
||||
default_app_config = 'assets.apps.AssetsAppConfig'
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from assets import models as assets
|
||||
|
||||
|
||||
@@ -15,18 +17,23 @@ class AssetStatusAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(assets.Supplier)
|
||||
class SupplierAdmin(admin.ModelAdmin):
|
||||
class SupplierAdmin(VersionAdmin):
|
||||
list_display = ['id', 'name']
|
||||
ordering = ['id']
|
||||
|
||||
|
||||
@admin.register(assets.Asset)
|
||||
class AssetAdmin(admin.ModelAdmin):
|
||||
class AssetAdmin(VersionAdmin):
|
||||
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']
|
||||
|
||||
8
assets/apps.py
Normal file
8
assets/apps.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AssetsAppConfig(AppConfig):
|
||||
name = 'assets'
|
||||
|
||||
def ready(self):
|
||||
import assets.signals
|
||||
8
assets/converters.py
Normal file
8
assets/converters.py
Normal file
@@ -0,0 +1,8 @@
|
||||
class AssetIDConverter: # Forces lowercase to uppercase
|
||||
regex = '[^/]+'
|
||||
|
||||
def to_python(self, value):
|
||||
return str(value).upper()
|
||||
|
||||
def to_url(self, value):
|
||||
return str(value).upper()
|
||||
@@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
|
||||
from assets import models
|
||||
|
||||
@@ -12,7 +13,7 @@ class AssetForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = models.Asset
|
||||
fields = '__all__'
|
||||
exclude = ['asset_id_prefix', 'asset_id_number']
|
||||
exclude = ['asset_id_prefix', 'asset_id_number', 'last_audited_at', 'last_audited_by']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -20,8 +21,15 @@ class AssetForm(forms.ModelForm):
|
||||
self.fields['date_acquired'].widget.format = '%Y-%m-%d'
|
||||
|
||||
|
||||
class AssetAuditForm(AssetForm):
|
||||
class Meta(AssetForm.Meta):
|
||||
# Prevents assets losing existing data that isn't included in the audit form
|
||||
exclude = ['asset_id_prefix', 'asset_id_number', 'last_audited_at', 'last_audited_by',
|
||||
'parent', 'purchased_from', 'purchase_price', 'comments']
|
||||
|
||||
|
||||
class AssetSearchForm(forms.Form):
|
||||
query = forms.CharField(required=False)
|
||||
q = forms.CharField(required=False)
|
||||
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
|
||||
status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False)
|
||||
|
||||
@@ -32,5 +40,15 @@ class SupplierForm(forms.ModelForm):
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from assets import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Deletes testing sample data'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
from django.conf import settings
|
||||
|
||||
if not (settings.DEBUG):
|
||||
raise CommandError('You cannot run this command in production')
|
||||
|
||||
self.delete_objects(models.AssetCategory)
|
||||
self.delete_objects(models.AssetStatus)
|
||||
self.delete_objects(models.Supplier)
|
||||
self.delete_objects(models.Connector)
|
||||
self.delete_objects(models.Asset)
|
||||
|
||||
def delete_objects(self, model):
|
||||
for object in model.objects.all():
|
||||
object.delete()
|
||||
@@ -1,12 +1,24 @@
|
||||
import random
|
||||
|
||||
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 RIGS import models as rigsmodels
|
||||
from assets import models
|
||||
from assets.models import get_available_asset_id
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Creates some sample data for testing'
|
||||
|
||||
categories = []
|
||||
statuses = []
|
||||
suppliers = []
|
||||
connectors = []
|
||||
assets = []
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
from django.conf import settings
|
||||
|
||||
@@ -15,98 +27,53 @@ class Command(BaseCommand):
|
||||
|
||||
random.seed('Some object to see the random number generator')
|
||||
|
||||
self.create_categories()
|
||||
self.create_statuses()
|
||||
self.create_suppliers()
|
||||
self.create_assets()
|
||||
self.create_connectors()
|
||||
self.create_cables()
|
||||
with transaction.atomic():
|
||||
self.create_categories()
|
||||
self.create_statuses()
|
||||
self.create_suppliers()
|
||||
self.create_assets()
|
||||
self.create_connectors()
|
||||
self.create_cables()
|
||||
|
||||
def create_categories(self):
|
||||
categories = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging']
|
||||
|
||||
for cat in categories:
|
||||
models.AssetCategory.objects.create(name=cat)
|
||||
choices = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging']
|
||||
for cat in choices:
|
||||
self.categories.append(models.AssetCategory.objects.create(name=cat))
|
||||
|
||||
def create_statuses(self):
|
||||
statuses = [('In Service', True), ('Lost', False), ('Binned', False), ('Sold', False), ('Broken', False)]
|
||||
|
||||
for stat in statuses:
|
||||
models.AssetStatus.objects.create(name=stat[0], should_show=stat[1])
|
||||
choices = [('In Service', True, 'success'), ('Lost', False, 'warning'), ('Binned', False, 'danger'), ('Sold', False, 'danger'), ('Broken', False, 'warning')]
|
||||
for stat in choices:
|
||||
self.statuses.append(models.AssetStatus.objects.create(name=stat[0], should_show=stat[1], display_class=stat[2]))
|
||||
|
||||
def create_suppliers(self):
|
||||
suppliers = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa
|
||||
choices = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa
|
||||
"Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron", "Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry", "Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.", "Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer", "Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron", "Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company", "Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer", "Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp", "The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute", "Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King", "Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman", "Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande", "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
|
||||
|
||||
for supplier in suppliers:
|
||||
models.Supplier.objects.create(name=supplier)
|
||||
for supplier in choices:
|
||||
self.suppliers.append(models.Supplier.objects.create(name=supplier))
|
||||
|
||||
def create_assets(self):
|
||||
asset_description = ['Large cable', 'Shiny thing', 'New lights', 'Really expensive microphone', 'Box of fuse flaps', 'Expensive tool we didn\'t agree to buy', 'Cable drums', 'Boring amount of tape', 'Video stuff no one knows how to use', 'More amplifiers', 'Heatshrink']
|
||||
|
||||
categories = models.AssetCategory.objects.all()
|
||||
statuses = models.AssetStatus.objects.all()
|
||||
suppliers = models.Supplier.objects.all()
|
||||
|
||||
for i in range(100):
|
||||
asset = models.Asset(
|
||||
asset_id='{}'.format(models.Asset.get_available_asset_id()),
|
||||
description=random.choice(asset_description),
|
||||
category=random.choice(categories),
|
||||
status=random.choice(statuses),
|
||||
date_acquired=timezone.now().date()
|
||||
)
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(rigsmodels.Profile.objects.all()))
|
||||
asset_id = str(get_available_asset_id())
|
||||
asset = models.Asset(
|
||||
asset_id=asset_id,
|
||||
description=random.choice(asset_description),
|
||||
category=random.choice(self.categories),
|
||||
status=random.choice(self.statuses),
|
||||
date_acquired=timezone.now().date()
|
||||
)
|
||||
|
||||
if i % 4 == 0:
|
||||
asset.parent = models.Asset.objects.order_by('?').first()
|
||||
if i % 4 == 0:
|
||||
asset.parent = models.Asset.objects.order_by('?').first()
|
||||
|
||||
if i % 3 == 0:
|
||||
asset.purchased_from = random.choice(suppliers)
|
||||
asset.clean()
|
||||
asset.save()
|
||||
if i % 3 == 0:
|
||||
asset.purchased_from = random.choice(self.suppliers)
|
||||
|
||||
def create_cables(self):
|
||||
asset_description = ['The worm', 'Harting without a cap', 'Heavy cable', 'Extension lead', 'IEC cable that we should remember to prep']
|
||||
asset_prefixes = ["C", "C4P", "CBNC", "CDMX", "CDV", "CRCD", "CSOCA", "CXLR"]
|
||||
|
||||
csas = [0.75, 1.00, 1.25, 2.5, 4]
|
||||
lengths = [1, 2, 5, 10, 15, 20, 25, 30, 50, 100]
|
||||
cores = [3, 5]
|
||||
circuits = [1, 2, 3, 6]
|
||||
categories = models.AssetCategory.objects.all()
|
||||
statuses = models.AssetStatus.objects.all()
|
||||
suppliers = models.Supplier.objects.all()
|
||||
connectors = models.Connector.objects.all()
|
||||
|
||||
for i in range(100):
|
||||
asset = models.Asset(
|
||||
asset_id='{}'.format(models.Asset.get_available_asset_id()),
|
||||
description=random.choice(asset_description),
|
||||
category=random.choice(categories),
|
||||
status=random.choice(statuses),
|
||||
date_acquired=timezone.now().date(),
|
||||
|
||||
is_cable=True,
|
||||
plug=random.choice(connectors),
|
||||
socket=random.choice(connectors),
|
||||
csa=random.choice(csas),
|
||||
length=random.choice(lengths),
|
||||
circuits=random.choice(circuits),
|
||||
cores=random.choice(circuits)
|
||||
)
|
||||
|
||||
if i % 5 == 0:
|
||||
prefix = random.choice(asset_prefixes)
|
||||
asset.asset_id = prefix + str(models.Asset.get_available_asset_id(wanted_prefix=prefix))
|
||||
|
||||
if i % 4 == 0:
|
||||
asset.parent = models.Asset.objects.order_by('?').first()
|
||||
|
||||
if i % 3 == 0:
|
||||
asset.purchased_from = random.choice(suppliers)
|
||||
|
||||
asset.clean()
|
||||
asset.save()
|
||||
asset.clean()
|
||||
asset.save()
|
||||
|
||||
def create_connectors(self):
|
||||
connectors = [
|
||||
@@ -118,3 +85,43 @@ class Command(BaseCommand):
|
||||
for connector in connectors:
|
||||
conn = models.Connector.objects.create(** connector)
|
||||
conn.save()
|
||||
self.connectors.append(conn)
|
||||
|
||||
def create_cables(self):
|
||||
asset_description = ['The worm', 'Harting without a cap', 'Heavy cable', 'Extension lead', 'IEC cable that we should remember to prep']
|
||||
asset_prefixes = ["C", "C4P", "CBNC", "CDMX", "CDV", "CRCD", "CSOCA", "CXLR"]
|
||||
|
||||
csas = [0.75, 1.00, 1.25, 2.5, 4]
|
||||
lengths = [1, 2, 5, 10, 15, 20, 25, 30, 50, 100]
|
||||
cores = [3, 5]
|
||||
circuits = [1, 2, 3, 6]
|
||||
types = []
|
||||
|
||||
for i in range(len(self.connectors)):
|
||||
types.append(models.CableType.objects.create(plug=random.choice(self.connectors), socket=random.choice(self.connectors), circuits=random.choice(circuits), cores=random.choice(cores)))
|
||||
|
||||
for i in range(100):
|
||||
prefix = random.choice(asset_prefixes)
|
||||
asset_id = str(get_available_asset_id(wanted_prefix=prefix))
|
||||
asset_id = prefix + asset_id
|
||||
asset = models.Asset(
|
||||
asset_id=asset_id,
|
||||
description=random.choice(asset_description),
|
||||
category=random.choice(self.categories),
|
||||
status=random.choice(self.statuses),
|
||||
date_acquired=timezone.now().date(),
|
||||
|
||||
is_cable=True,
|
||||
cable_type=random.choice(types),
|
||||
csa=random.choice(csas),
|
||||
length=random.choice(lengths),
|
||||
)
|
||||
|
||||
if i % 4 == 0:
|
||||
asset.parent = models.Asset.objects.order_by('?').first()
|
||||
|
||||
if i % 3 == 0:
|
||||
asset.purchased_from = random.choice(self.suppliers)
|
||||
|
||||
asset.clean()
|
||||
asset.save()
|
||||
|
||||
@@ -57,7 +57,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=None, related_name='asset_parent', to='assets.Asset'),
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_parent', to='assets.Asset'),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
@@ -85,7 +85,7 @@ class Migration(migrations.Migration):
|
||||
('circuits', models.IntegerField(blank=True, null=True)),
|
||||
('cores', models.IntegerField(blank=True, null=True)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.AssetCategory')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=None, related_name='asset_parent', to='assets.Cable')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_parent', to='assets.Cable')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 2.0.13 on 2020-02-07 17:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0009_auto_20200103_2215'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='supplier',
|
||||
options={'ordering': ['name'], 'permissions': (('view_supplier', 'Can view a supplier'),)},
|
||||
),
|
||||
]
|
||||
21
assets/migrations/0010_auto_20200219_1444.py
Normal file
21
assets/migrations/0010_auto_20200219_1444.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.0.3 on 2020-02-19 14:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0009_auto_20200103_2215'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'ordering': ['asset_id_prefix', 'asset_id_number'], 'permissions': [('asset_finance', 'Can see financial data for assets')]},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='supplier',
|
||||
options={'ordering': ['name']},
|
||||
),
|
||||
]
|
||||
29
assets/migrations/0011_auto_20200218_1617.py
Normal file
29
assets/migrations/0011_auto_20200218_1617.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
26
assets/migrations/0012_auto_20200218_1627.py
Normal file
26
assets/migrations/0012_auto_20200218_1627.py
Normal 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)
|
||||
]
|
||||
29
assets/migrations/0013_auto_20200218_1639.py
Normal file
29
assets/migrations/0013_auto_20200218_1639.py
Normal 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',
|
||||
),
|
||||
]
|
||||
17
assets/migrations/0014_auto_20200218_1840.py
Normal file
17
assets/migrations/0014_auto_20200218_1840.py
Normal 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']},
|
||||
),
|
||||
]
|
||||
17
assets/migrations/0015_remove_asset_next_sched_maint.py
Normal file
17
assets/migrations/0015_remove_asset_next_sched_maint.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.0.3 on 2020-04-13 15:13
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0014_auto_20200218_1840'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='next_sched_maint',
|
||||
),
|
||||
]
|
||||
34
assets/migrations/0016_auto_20200413_1632.py
Normal file
34
assets/migrations/0016_auto_20200413_1632.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 3.0.3 on 2020-04-13 15:32
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0015_remove_asset_next_sched_maint'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='cabletype',
|
||||
name='circuits',
|
||||
field=models.IntegerField(default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cabletype',
|
||||
name='cores',
|
||||
field=models.IntegerField(default=3),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cabletype',
|
||||
name='plug',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='plug', to='assets.Connector'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cabletype',
|
||||
name='socket',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='socket', to='assets.Connector'),
|
||||
),
|
||||
]
|
||||
31
assets/migrations/0017_add_audit.py
Normal file
31
assets/migrations/0017_add_audit.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 3.0.3 on 2020-04-13 00:06
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('assets', '0016_auto_20200413_1632'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='last_audited_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='last_audited_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='audited_by', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='csa',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='mm²', max_digits=10, null=True),
|
||||
),
|
||||
]
|
||||
33
assets/migrations/0018_auto_20200415_1940.py
Normal file
33
assets/migrations/0018_auto_20200415_1940.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 3.0.3 on 2020-04-15 18:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0017_add_audit'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='supplier',
|
||||
name='address',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='supplier',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, max_length=254, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='supplier',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='supplier',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, max_length=15, null=True),
|
||||
),
|
||||
]
|
||||
25
assets/migrations/0019_fix_cabletype.py
Normal file
25
assets/migrations/0019_fix_cabletype.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.1.5 on 2021-02-08 16:02
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def add_default(apps, schema_editor):
|
||||
CableType = apps.get_model('assets', 'CableType')
|
||||
Connector = apps.get_model('assets', 'Connector')
|
||||
for cable_type in CableType.objects.all():
|
||||
if cable_type.plug is None:
|
||||
cable_type.plug = Connector.objects.first()
|
||||
if cable_type.socket is None:
|
||||
cable_type.socket = Connector.objects.first()
|
||||
cable_type.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0018_auto_20200415_1940'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_default, migrations.RunPython.noop)
|
||||
]
|
||||
23
assets/migrations/0020_auto_20210302_1201.py
Normal file
23
assets/migrations/0020_auto_20210302_1201.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-02 12:01
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def postgres_migration_prep(apps, schema_editor):
|
||||
model = apps.get_model("assets", "Supplier")
|
||||
fields = ["address", "email", "notes", "phone"]
|
||||
for field in fields:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0019_fix_cabletype'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(postgres_migration_prep, migrations.RunPython.noop)
|
||||
]
|
||||
50
assets/migrations/0021_auto_20210302_1204.py
Normal file
50
assets/migrations/0021_auto_20210302_1204.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-02 12:04
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0020_auto_20210302_1201'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='assetstatus',
|
||||
name='display_class',
|
||||
field=models.CharField(blank=True, default='', help_text='HTML class to be appended to alter display of assets with this status, such as in the list.', max_length=80),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cabletype',
|
||||
name='plug',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plug', to='assets.connector'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cabletype',
|
||||
name='socket',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='socket', to='assets.connector'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplier',
|
||||
name='address',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplier',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, default='', max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplier',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplier',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, default='', max_length=15),
|
||||
),
|
||||
]
|
||||
149
assets/models.py
149
assets/models.py
@@ -1,40 +1,37 @@
|
||||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models, connection
|
||||
from django.urls import reverse
|
||||
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch.dispatcher import receiver
|
||||
|
||||
from reversion import revisions as reversion
|
||||
from reversion.models import Version
|
||||
|
||||
from RIGS.models import RevisionMixin
|
||||
from RIGS.models import RevisionMixin, Profile
|
||||
|
||||
|
||||
class AssetCategory(models.Model):
|
||||
name = models.CharField(max_length=80)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Asset Category'
|
||||
verbose_name_plural = 'Asset Categories'
|
||||
ordering = ['name']
|
||||
|
||||
name = models.CharField(max_length=80)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class AssetStatus(models.Model):
|
||||
name = models.CharField(max_length=80)
|
||||
should_show = models.BooleanField(
|
||||
default=True, help_text="Should this be shown by default in the asset list.")
|
||||
display_class = models.CharField(max_length=80, blank=True, help_text="HTML class to be appended to alter display of assets with this status, such as in the list.")
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Asset Status'
|
||||
verbose_name_plural = 'Asset Statuses'
|
||||
ordering = ['name']
|
||||
|
||||
name = models.CharField(max_length=80)
|
||||
should_show = models.BooleanField(
|
||||
default=True, help_text="Should this be shown by default in the asset list.")
|
||||
display_class = models.CharField(max_length=80, blank=True, null=True, help_text="HTML class to be appended to alter display of assets with this status, such as in the list.")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -42,15 +39,17 @@ class AssetStatus(models.Model):
|
||||
@reversion.register
|
||||
class Supplier(models.Model, RevisionMixin):
|
||||
name = models.CharField(max_length=80)
|
||||
phone = models.CharField(max_length=15, blank=True, default="")
|
||||
email = models.EmailField(blank=True, default="")
|
||||
address = models.TextField(blank=True, default="")
|
||||
|
||||
notes = models.TextField(blank=True, default="")
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
permissions = (
|
||||
('view_supplier', 'Can view a supplier'),
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('supplier_list')
|
||||
return reverse('supplier_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -66,15 +65,48 @@ class Connector(models.Model):
|
||||
return self.description
|
||||
|
||||
|
||||
class CableType(models.Model):
|
||||
circuits = models.IntegerField(default=1)
|
||||
cores = models.IntegerField(default=3)
|
||||
plug = models.ForeignKey(Connector, on_delete=models.CASCADE,
|
||||
related_name='plug')
|
||||
socket = models.ForeignKey(Connector, on_delete=models.CASCADE,
|
||||
related_name='socket')
|
||||
|
||||
class Meta:
|
||||
ordering = ['plug', 'socket', '-circuits']
|
||||
|
||||
def __str__(self):
|
||||
if self.plug and self.socket:
|
||||
return "%s → %s" % (self.plug.description, self.socket.description)
|
||||
else:
|
||||
return "Unknown"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('cable_type_detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
def get_available_asset_id(wanted_prefix=""):
|
||||
sql = """
|
||||
SELECT a.asset_id_number+1
|
||||
FROM assets_asset a
|
||||
LEFT OUTER JOIN assets_asset b ON
|
||||
(a.asset_id_number + 1 = b.asset_id_number AND
|
||||
a.asset_id_prefix = b.asset_id_prefix)
|
||||
WHERE b.asset_id IS NULL AND a.asset_id_number >= %s AND a.asset_id_prefix = %s;
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql, [9000, wanted_prefix])
|
||||
row = cursor.fetchone()
|
||||
if row is None or row[0] is None:
|
||||
return 9000
|
||||
else:
|
||||
return row[0]
|
||||
cursor.close()
|
||||
|
||||
|
||||
@reversion.register
|
||||
class Asset(models.Model, RevisionMixin):
|
||||
class Meta:
|
||||
ordering = ['asset_id_prefix', 'asset_id_number']
|
||||
permissions = (
|
||||
('asset_finance', 'Can see financial data for assets'),
|
||||
('view_asset', 'Can view an asset')
|
||||
)
|
||||
|
||||
parent = models.ForeignKey(to='self', related_name='asset_parent',
|
||||
blank=True, null=True, on_delete=models.SET_NULL)
|
||||
asset_id = models.CharField(max_length=15, unique=True)
|
||||
@@ -88,52 +120,38 @@ class Asset(models.Model, RevisionMixin):
|
||||
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
|
||||
salvage_value = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
|
||||
comments = models.TextField(blank=True)
|
||||
next_sched_maint = models.DateField(blank=True, null=True)
|
||||
|
||||
# Audit
|
||||
last_audited_at = models.DateTimeField(blank=True, null=True)
|
||||
last_audited_by = models.ForeignKey(Profile, on_delete=models.SET_NULL, related_name='audited_by', blank=True, null=True)
|
||||
|
||||
# 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)
|
||||
blank=True, null=True, help_text='mm²')
|
||||
|
||||
# Hidden asset_id components
|
||||
# For example, if asset_id was "C1001" then asset_id_prefix would be "C" and number "1001"
|
||||
asset_id_prefix = models.CharField(max_length=8, default="")
|
||||
asset_id_number = models.IntegerField(default=1)
|
||||
|
||||
def get_available_asset_id(wanted_prefix=""):
|
||||
sql = """
|
||||
SELECT a.asset_id_number+1
|
||||
FROM assets_asset a
|
||||
LEFT OUTER JOIN assets_asset b ON
|
||||
(a.asset_id_number + 1 = b.asset_id_number AND
|
||||
a.asset_id_prefix = b.asset_id_prefix)
|
||||
WHERE b.asset_id IS NULL AND a.asset_id_number >= %s AND a.asset_id_prefix = %s;
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql, [9000, wanted_prefix])
|
||||
row = cursor.fetchone()
|
||||
if row is None or row[0] is None:
|
||||
return 9000
|
||||
else:
|
||||
return row[0]
|
||||
reversion_perm = 'assets.asset_finance'
|
||||
|
||||
class Meta:
|
||||
ordering = ['asset_id_prefix', 'asset_id_number']
|
||||
permissions = [
|
||||
('asset_finance', 'Can see financial data for assets')
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return "{} | {}".format(self.asset_id, self.description)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('asset_detail', kwargs={'pk': self.asset_id})
|
||||
|
||||
def __str__(self):
|
||||
out = str(self.asset_id) + ' - ' + self.description
|
||||
if self.is_cable:
|
||||
out += '{} - {}m - {}'.format(self.plug, self.length, self.socket)
|
||||
return out
|
||||
|
||||
def clean(self):
|
||||
errdict = {}
|
||||
if self.date_sold and self.date_acquired > self.date_sold:
|
||||
@@ -156,25 +174,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 errdict != {}: # If there was an error when validation
|
||||
raise ValidationError(errdict)
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return str(self)
|
||||
|
||||
@receiver(pre_save, sender=Asset)
|
||||
def pre_save_asset(sender, instance, **kwargs):
|
||||
"""Automatically fills in hidden members on database access"""
|
||||
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
|
||||
if asset_search is None:
|
||||
instance.asset_id += "1"
|
||||
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
|
||||
instance.asset_id_prefix = asset_search.group(1)
|
||||
instance.asset_id_number = int(asset_search.group(2))
|
||||
@property
|
||||
def display_id(self):
|
||||
return str(self.asset_id)
|
||||
|
||||
15
assets/signals.py
Normal file
15
assets/signals.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import re
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch.dispatcher import receiver
|
||||
from .models import Asset
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Asset)
|
||||
def pre_save_asset(sender, instance, **kwargs):
|
||||
"""Automatically fills in hidden members on database access"""
|
||||
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
|
||||
if asset_search is None:
|
||||
instance.asset_id += "1"
|
||||
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
|
||||
instance.asset_id_prefix = asset_search.group(1)
|
||||
instance.asset_id_number = int(asset_search.group(2))
|
||||
@@ -1,23 +0,0 @@
|
||||
$.ajaxSetup({
|
||||
beforeSend: function(xhr, settings) {
|
||||
function getCookie(name) {
|
||||
var cookieValue = null;
|
||||
if (document.cookie && document.cookie != '') {
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0; i < cookies.length; i++) {
|
||||
var cookie = jQuery.trim(cookies[i]);
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) == (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) {
|
||||
// Only send the token to relative URLs i.e. locally.
|
||||
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_assets.html" %}
|
||||
{% load static %}
|
||||
{% load paginator from filters %}
|
||||
{% load to_class_name from filters %}
|
||||
|
||||
{% block title %}Asset Activity Stream{% endblock %}
|
||||
|
||||
{# TODO: Find a way to reduce code duplication...can't just include the content because of the IDs... #}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static "js/tooltip.js" %}"></script>
|
||||
<script src="{% static "js/popover.js" %}"></script>
|
||||
<script src="{% static "js/moment.min.js" %}"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('[data-toggle="popover"]').popover().click(function(){
|
||||
if($(this).attr('href')){
|
||||
window.location.href = $(this).attr('href');
|
||||
}
|
||||
});
|
||||
|
||||
// This keeps timeago values correct, but uses an insane amount of resources
|
||||
// $(function () {
|
||||
// setInterval(function() {
|
||||
// $('.date').each(function (index, dateElem) {
|
||||
// var $dateElem = $(dateElem);
|
||||
// var formatted = moment($dateElem.attr('data-date')).fromNow();
|
||||
// $dateElem.text(formatted);
|
||||
// })
|
||||
// });
|
||||
// }, 10000);
|
||||
|
||||
|
||||
$('.date').each(function (index, dateElem) {
|
||||
var $dateElem = $(dateElem);
|
||||
var formatted = moment($dateElem.attr('data-date')).fromNow();
|
||||
$dateElem.text(formatted);
|
||||
});
|
||||
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-sm-12">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h3>Asset Activity Stream</h3>
|
||||
</div>
|
||||
<div class="text-right col-sm-12">{% paginator %}</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>Object</td>
|
||||
<td>Version ID</td>
|
||||
<td>User</td>
|
||||
<td>Changes</td>
|
||||
<td>Comment</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for version in object_list %}
|
||||
|
||||
<tr>
|
||||
<td>{{ version.revision.date_created }}</td>
|
||||
<td><a href="{{ version.changes.new.get_absolute_url }}">{{version.changes.new|to_class_name}} {{ version.changes.new.asset_id|default:version.changes.new.pk }}</a></td>
|
||||
<td>{{ version.pk }}|{{ version.revision.pk }}</td>
|
||||
<td>{{ version.revision.user.name }}</td>
|
||||
<td>
|
||||
{% if version.changes.old == None %}
|
||||
{{version.changes.new|to_class_name}} Created
|
||||
{% else %}
|
||||
{% include 'RIGS/version_changes.html' %}
|
||||
{% endif %} </td>
|
||||
<td>{{ version.changes.revision.comment }}</td>
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="align-right">{% paginator %}</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
92
assets/templates/asset_audit.html
Normal file
92
assets/templates/asset_audit.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends request.is_ajax|yesno:'base_ajax.html,base_assets.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block content %}
|
||||
<script>
|
||||
function setAcquired(today) {
|
||||
var date = new Date(1970, 0, 1);
|
||||
if(today) {
|
||||
date = new Date();
|
||||
}
|
||||
$('#id_date_acquired').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'));
|
||||
}
|
||||
function setFieldValue(ID, CSA) {
|
||||
$('#' + String(ID)).val(CSA);
|
||||
}
|
||||
function checkIfCableHidden() {
|
||||
document.getElementById("cable-table").hidden = !document.getElementById("id_is_cable").checked;
|
||||
}
|
||||
checkIfCableHidden();
|
||||
</script>
|
||||
<form method="POST" id="asset_audit_form" action="{{ form.action|default:request.path }}">
|
||||
{% include 'form_errors.html' %}
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ object.id|default:0 }}" hidden=true>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.asset_id col="col-6" %}
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.description col="col-6" %}
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.category col="col-6" %}
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.status col="col-6" %}
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.serial_number col="col-6" %}
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.date_acquired col="col-6" %}
|
||||
<div class="col-sm-4">
|
||||
<button class="btn btn-info" onclick="setAcquired(true);" tabindex="-1">Today</button>
|
||||
<button class="btn btn-warning" onclick="setAcquired(false);" tabindex="-1">Unknown</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.date_sold col="col-6" %}
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.salvage_value col="col-6" prepend="£" %}
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group form-row">
|
||||
<label for="{{ form.is_cable.id_for_label }}" class="col-2">Cable?</label>
|
||||
<div class="col-6">
|
||||
{% render_field form.is_cable|attr:'onchange=checkIfCableHidden()' %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="cable-table">
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.cable_type col="col-6" %}
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.length append=form.length.help_text col="col-6" %}
|
||||
<div class="col-4">
|
||||
<button class="btn btn-danger" onclick="setFieldValue('{{ form.length.id_for_label }}','5');" tabindex="-1">5{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-success" onclick="setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1">10{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-info" onclick="setFieldValue('{{ form.length.id_for_label }}','20');" tabindex="-1">20{{ form.length.help_text }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.csa append=form.csa.help_text title='CSA' col="col-6" %}
|
||||
<div class="col-4">
|
||||
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '1.5');" tabindex="-1">1.5{{ form.csa.help_text }}</button>
|
||||
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '2.5');" tabindex="-1">2.5{{ form.csa.help_text }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if not request.is_ajax %}
|
||||
<div class="form-group form-row pull-right">
|
||||
<button class="btn btn-success" type="submit" form="asset_audit_form" id="id_mark_audited">Mark Audited</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
<div class="form-group form-row">
|
||||
<button class="btn btn-success pull-right" type="submit" form="asset_audit_form" onclick="onAuditClick({{form.asset_id.value}});" id="id_mark_audited">Mark Audited</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
70
assets/templates/asset_audit_list.html
Normal file
70
assets/templates/asset_audit_list.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% load static %}
|
||||
{% load paginator from filters %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
$('document').ready(function(){
|
||||
$('#asset-search-form').submit(function () {
|
||||
$('#searchButton').focus().click();
|
||||
return false;
|
||||
});
|
||||
$('#searchButton').click(function (e) {
|
||||
e.preventDefault();
|
||||
var url = "{% url 'asset_audit' None %}";
|
||||
var id = $("#{{form.q.id_for_label}}").val();
|
||||
url = url.replace('None', id);
|
||||
$.ajax({
|
||||
url: url,
|
||||
success: function(){
|
||||
$link = $(this);
|
||||
// Anti modal inception
|
||||
if ($link.parents('#modal').length === 0) {
|
||||
modaltarget = $link.data('target');
|
||||
modalobject = "";
|
||||
$('#modal').load(url, function (e) {
|
||||
$('#modal').modal();
|
||||
});
|
||||
}
|
||||
},
|
||||
error:function(){
|
||||
$("#error404").attr("hidden", false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
function onAuditClick(assetID) {
|
||||
$('#' + assetID).remove();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="error404" class="alert alert-danger alert-dismissable" hidden=true>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<span>Asset with that ID does not exist!</span>
|
||||
</div>
|
||||
|
||||
<form id="asset-search-form" class="mb-3" method="GET">
|
||||
<div class="form-group form-row">
|
||||
<h3>Audit Asset:</h3>
|
||||
<div class="input-group input-group-lg">
|
||||
{% render_field form.q|add_class:'form-control' placeholder='Enter Asset ID' autofocus="true" %}
|
||||
<div class="input-group-append">
|
||||
<label for="q" class="sr-only">Asset ID:</label>
|
||||
<a id="searchButton" class="btn btn-primary" class="submit" type="submit">Search</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h3>Assets Requiring Audit:</h3>
|
||||
{% include 'partials/asset_list_table.html' with audit="true" %}
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="text-center">
|
||||
{% paginator %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,74 +0,0 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% load static %}
|
||||
{% load widget_tweaks %}
|
||||
{% block title %}Asset {{ object.asset_id }}{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
<script src="//cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
|
||||
<script src="{% static 'js/interaction.js' %}"></script>
|
||||
<script>
|
||||
function checkIfCableHidden() {
|
||||
if (document.getElementById("id_is_cable").checked) {
|
||||
document.getElementById("cable-table").hidden = false;
|
||||
} else {
|
||||
document.getElementById("cable-table").hidden = true;
|
||||
}
|
||||
}
|
||||
checkIfCableHidden();
|
||||
|
||||
$(document).ready(function () {
|
||||
setupMDE('#id_comments');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{% if duplicate %}
|
||||
Duplication of Asset: {{ previous_asset_id }}
|
||||
{% else %}
|
||||
Create Asset
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
{% if duplicate %}
|
||||
<form method="post" id="asset_update_form" action="{% url 'asset_duplicate' pk=previous_asset_id%}">
|
||||
{% else %}
|
||||
<form method="post" id="asset_update_form" action="{% url 'asset_create'%}">
|
||||
{% 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">
|
||||
{% include 'partials/asset_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% include 'partials/purchasedetails_form.html' %}
|
||||
</div>
|
||||
<div class="col-md-6" hidden="true" id="cable-table">
|
||||
{% include 'partials/cable_form.html' %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% include 'partials/parent_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'partials/asset_buttons.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
35
assets/templates/asset_detail.html
Normal file
35
assets/templates/asset_detail.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-end">
|
||||
{% include 'partials/asset_buttons.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
{% include 'partials/asset_detail_form.html' %}
|
||||
</div>
|
||||
{% if perms.assets.asset_finance %}
|
||||
<div class="col-md-6 mb-3">
|
||||
{% include 'partials/purchasedetails_form.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-md-4 mb-3" {% if not object.is_cable %}hidden="true"{% endif %} id="cable-table">
|
||||
{% include 'partials/cable_form.html' %}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
{% include 'partials/parent_form.html' %}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
{% include 'partials/audit_details.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.assets.view_asset %}
|
||||
<div class="row justify-content-end">
|
||||
{% include 'partials/asset_buttons.html' %}
|
||||
</div>
|
||||
<div class="row justify-content-end">
|
||||
{% include 'partials/last_edited.html' with target="asset_history" id=object.asset_id %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,40 +1,36 @@
|
||||
{% extends 'base_embed.html' %}
|
||||
{% load static from staticfiles %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<a href="/assets">
|
||||
<span class="source"> TEC Asset Database</span>
|
||||
</a>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<a href="/assets">
|
||||
<span class="source"> TEC Asset Database</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<h3><a href="{% url 'asset_detail' object.asset_id %}">Asset: {{ object.asset_id }} | {{ object.description }} </a></h3>
|
||||
<h4>
|
||||
<span class="label label-default">
|
||||
<strong>Category:</strong>
|
||||
{{ object.category }}
|
||||
</span>
|
||||
|
||||
<span class="label label-{{ object.status.display_class|default:'default' }}">
|
||||
<strong>Status:</strong>
|
||||
{{ object.status }}
|
||||
</span>
|
||||
</h4>
|
||||
<dl>
|
||||
{% if object.serial_number %}
|
||||
<dt>Serial Number: </dt>
|
||||
<dd>{{ object.serial_number }}</dd>
|
||||
{% endif %}
|
||||
{% if object.comments %}
|
||||
<dt>Comments: </dt>
|
||||
<dd class="dont-break-out">{{ object.comments|linebreaksbr }}<dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<h3><a href="{% url 'asset_detail' object.asset_id %}">Asset: {{ object.asset_id }} | {{ object.description }} </a></h3>
|
||||
<h4>
|
||||
<span class="label label-default">
|
||||
<strong>Category:</strong>
|
||||
{{ object.category }}
|
||||
</span>
|
||||
|
||||
<span class="label label-{{ object.status.display_class|default:'default' }}">
|
||||
<strong>Status:</strong>
|
||||
{{ object.status }}
|
||||
</span>
|
||||
</h4>
|
||||
{% if object.serial_number %}
|
||||
<dt>Serial Number: </dt>
|
||||
<dd>{{ object.serial_number }}</dd>
|
||||
{% endif %}
|
||||
{% if object.comments %}
|
||||
<dt>Comments: </dt>
|
||||
<dd class="dont-break-out">{{ object.comments|linebreaksbr }}<dd>
|
||||
{% endif %}
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
110
assets/templates/asset_form.html
Normal file
110
assets/templates/asset_form.html
Normal file
@@ -0,0 +1,110 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% load static %}
|
||||
|
||||
{% block css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/selects.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/autocompleter.js' %}"></script>
|
||||
<script>
|
||||
const matches = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.matchMedia("(update: slow)").matches;
|
||||
dur = matches ? 0 : 500;
|
||||
function checkIfCableHidden() {
|
||||
if ($("#id_is_cable").prop('checked')) {
|
||||
$("#cable-table").slideDown(dur);
|
||||
} else {
|
||||
$("#cable-table").slideUp(dur);
|
||||
}
|
||||
}
|
||||
checkIfCableHidden();
|
||||
</script>
|
||||
<script>
|
||||
$('#parent_id')
|
||||
.selectpicker({
|
||||
liveSearch: true
|
||||
})
|
||||
.ajaxSelectPicker({
|
||||
ajax: {
|
||||
url: "{% url 'asset_search_json' %}",
|
||||
type: "GET",
|
||||
data: function () {
|
||||
let params = {
|
||||
{% verbatim %}query: '{{{q}}}'{% endverbatim %}
|
||||
};
|
||||
return params;
|
||||
}
|
||||
},
|
||||
locale: {
|
||||
emptyTitle: 'Search for item...'
|
||||
},
|
||||
preprocessData: function(data){
|
||||
var assets = [];
|
||||
if(data.length){
|
||||
var len = data.length;
|
||||
for(var i = 0; i < len; i++){
|
||||
var curr = data[i];
|
||||
assets.push(
|
||||
{
|
||||
'value': curr.id,
|
||||
'text': curr.label,
|
||||
'disabled': false
|
||||
}
|
||||
);
|
||||
}
|
||||
assets.push(
|
||||
{
|
||||
'value': null,
|
||||
'text': "No parent"
|
||||
});
|
||||
}
|
||||
|
||||
return assets;
|
||||
},
|
||||
preserveSelected: false
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if duplicate %}
|
||||
<form method="POST" id="asset_update_form" action="{% url 'asset_duplicate' pk=previous_asset_id %}">
|
||||
{% elif edit %}
|
||||
<form method="POST" id="asset_update_form" action="{% url 'asset_update' pk=object.asset_id %}">
|
||||
{% else %}
|
||||
<form method="POST" id="asset_update_form" action="{% url 'asset_create' %}">
|
||||
{% endif %}
|
||||
{% include 'form_errors.html' %}
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ object.id|default:0 }}" hidden=true>
|
||||
<div class="row pt-2">
|
||||
<div class="col-sm-12">
|
||||
{% include 'partials/asset_detail_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pt-2">
|
||||
<div class="col-12 col-sm">
|
||||
{% include 'partials/purchasedetails_form.html' %}
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-4" id="cable-table">
|
||||
{% include 'partials/cable_form.html' %}
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
{% include 'partials/parent_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{% include 'partials/asset_buttons.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -1,65 +1,102 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% block title %}Asset List{% endblock %}
|
||||
{% load paginator from filters %}
|
||||
{% load button from filters %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="text-center">Asset List</h1>
|
||||
</div>
|
||||
|
||||
<form id="asset-search-form" method="get" class="form-inline pull-right">
|
||||
<div class="input-group pull-right" style="width: auto;">
|
||||
{% render_field form.query|add_class:'form-control' placeholder='Search by Asset ID/Desc/Serial' style="width: 250px"%}
|
||||
<label for="query" class="sr-only">Asset ID/Description/Serial Number:</label>
|
||||
<span class="input-group-btn"><button type="submit" class="btn btn-default">Search</button></span>
|
||||
</div>
|
||||
<br>
|
||||
<div style="margin-top: 1em;" class="pull-right">
|
||||
<div id="category-group" class="form-group">
|
||||
<label for="category" class="sr-only">Category</label>
|
||||
{% render_field form.category|attr:'multiple'|add_class:'form-control selectpicker' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %}
|
||||
</div>
|
||||
<div id="status-group" class="form-group">
|
||||
<label for="status" class="sr-only">Status</label>
|
||||
{% render_field form.status|attr:'multiple'|add_class:'form-control selectpicker' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
|
||||
</div>
|
||||
<!---TODO: Auto filter whenever an option is selected, instead of using a button -->
|
||||
<button id="filter-submit" type="submit" class="btn btn-default">Filter</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Asset ID</th>
|
||||
<th>Description</th>
|
||||
<th>Category</th>
|
||||
<th>Status</th>
|
||||
<th class="hidden-xs">Quick Links</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="asset_table_body">
|
||||
{% include 'partials/asset_list_table_body.html' %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="text-center">
|
||||
{% paginator %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="{% static "css/bootstrap-select.min.css" %}"/>
|
||||
<link rel="stylesheet" href="{% static "css/ajax-bootstrap-select.css" %}"/>
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
<script src="{% static "js/bootstrap-select.js" %}"></script>
|
||||
<script src="{% static "js/ajax-bootstrap-select.js" %}"></script>
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/selects.js' %}" async></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ block.super }}
|
||||
<script>
|
||||
//Get querystring value
|
||||
function getParameterByName(name) {
|
||||
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
|
||||
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
||||
results = regex.exec(location.search);
|
||||
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
||||
}
|
||||
//Function used to remove querystring
|
||||
function removeQString(key) {
|
||||
var urlValue=document.location.href;
|
||||
|
||||
//Get query string value
|
||||
var searchUrl=location.search;
|
||||
|
||||
if(key!=="") {
|
||||
oldValue = getParameterByName(key);
|
||||
removeVal=key+"="+oldValue;
|
||||
if(searchUrl.indexOf('?'+removeVal+'&')!== "-1") {
|
||||
urlValue=urlValue.replace('?'+removeVal+'&','?');
|
||||
}
|
||||
else if(searchUrl.indexOf('&'+removeVal+'&')!== "-1") {
|
||||
urlValue=urlValue.replace('&'+removeVal+'&','&');
|
||||
}
|
||||
else if(searchUrl.indexOf('?'+removeVal)!== "-1") {
|
||||
urlValue=urlValue.replace('?'+removeVal,'');
|
||||
}
|
||||
else if(searchUrl.indexOf('&'+removeVal)!== "-1") {
|
||||
urlValue=urlValue.replace('&'+removeVal,'');
|
||||
}
|
||||
}
|
||||
else {
|
||||
var searchUrl=location.search;
|
||||
urlValue=urlValue.replace(searchUrl,'');
|
||||
}
|
||||
history.pushState({state:1, rand: Math.random()}, '', urlValue);
|
||||
window.location.reload(true);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col px-0">
|
||||
<form id="asset-search-form" method="GET" class="form-inline justify-content-end">
|
||||
<div class="input-group px-1 mb-2 mb-sm-0 flex-nowrap">
|
||||
{% render_field form.q|add_class:'form-control' placeholder='Enter Asset ID/Desc/Serial' %}
|
||||
<label for="q" class="sr-only">Asset ID/Description/Serial Number:</label>
|
||||
<span class="input-group-append">{% button 'search' id="id_search" %}</span>
|
||||
</div>
|
||||
<div id="category-group" class="form-group px-1" style="margin-bottom: 0;">
|
||||
<label for="category" class="sr-only">Category</label>
|
||||
{% render_field form.category|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %}
|
||||
</div>
|
||||
<div id="status-group" class="form-group px-1" style="margin-bottom: 0;">
|
||||
<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>
|
||||
<button id="filter-submit" type="submit" class="btn btn-secondary" style="width: 6em">Filter</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-2">
|
||||
<div class="col text-right px-0">
|
||||
{% button 'new' 'asset_create' style="width: 6em" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-2">
|
||||
<div class="col bg-dark text-white rounded pt-3">
|
||||
{# TODO Gotta be a cleaner way to do this... #}
|
||||
<p><span class="ml-2">Active Filters: </span> {% for filter in category_filters %}<span class="badge badge-info mx-1 ">{{filter}}<button type="button" class="btn btn-link p-0 ml-1 align-baseline">
|
||||
<span aria-hidden="true" class="fas fa-times" onclick="removeQString('category', '{{filter.id}}')"></span>
|
||||
</button></span>{%endfor%}{% for filter in status_filters %}<span class="badge badge-info mx-1 ">{{filter}}<button type="button" class="btn btn-link p-0 ml-1 align-baseline">
|
||||
<span aria-hidden="true" class="fas fa-times" onclick="removeQString('status', '{{filter.id}}')"></span>
|
||||
</button></span>{%endfor%}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col px-0">
|
||||
{% include 'partials/asset_list_table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% paginator %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% load static %}
|
||||
{% load widget_tweaks %}
|
||||
{% block title %}Asset {{ object.asset_id }}{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
<script src="//cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{% if edit %}
|
||||
<script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
|
||||
<script src="{% static 'js/interaction.js' %}"></script>
|
||||
<script>
|
||||
function checkIfCableHidden() {
|
||||
if (document.getElementById("id_is_cable").checked) {
|
||||
document.getElementById("cable-table").hidden = false;
|
||||
} else {
|
||||
document.getElementById("cable-table").hidden = true;
|
||||
}
|
||||
}
|
||||
checkIfCableHidden();
|
||||
|
||||
$(document).ready(function () {
|
||||
setupMDE('#id_comments');
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{% if edit and object %}
|
||||
Edit Asset: {{ object.asset_id }}
|
||||
{% else %}
|
||||
Asset: {{ object.asset_id }}
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
<form method="post" id="asset_update_form" action="{% url 'asset_update' pk=object.asset_id%}">
|
||||
{% 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">
|
||||
{% include 'partials/asset_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% if perms.assets.asset_finance %}
|
||||
<div class="col-md-6">
|
||||
{% include 'partials/purchasedetails_form.html' %}
|
||||
</div>
|
||||
{%endif%}
|
||||
<div class="col-md-6"
|
||||
{% if not object.is_cable %} hidden="true" {% endif %} id="cable-table">
|
||||
{% include 'partials/cable_form.html' %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% include 'partials/parent_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'partials/asset_buttons.html' %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if not edit and perms.assets.view_asset %}
|
||||
<div class="col-sm-12 text-right">
|
||||
<div>
|
||||
<a href="{% url 'asset_history' object.asset_id %}" title="View Revision History">
|
||||
Last edited at {{ object.last_edited_at|default:'never' }} by {{ object.last_edited_by.name|default:'nobody' }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,68 +0,0 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_assets.html" %}
|
||||
{% load to_class_name from filters %}
|
||||
{% load paginator from filters %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{object|to_class_name}} {{ object.asset_id }} - Revision History{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static "js/tooltip.js" %}"></script>
|
||||
<script src="{% static "js/popover.js" %}"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('[data-toggle="popover"]').popover().click(function(){
|
||||
if($(this).attr('href')){
|
||||
window.location.href = $(this).attr('href');
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-sm-12">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h3><a href="{{ object.get_absolute_url }}">{{object|to_class_name}} {{ object.asset_id|default:object.pk }}</a> - Revision History</h3>
|
||||
</div>
|
||||
<div class="text-right col-sm-12">{% paginator %}</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>Version ID</td>
|
||||
<td>User</td>
|
||||
<td>Changes</td>
|
||||
<td>Comment</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for version in object_list %}
|
||||
|
||||
<tr>
|
||||
<td>{{ version.revision.date_created }}</td>
|
||||
<td>{{ version.pk }}|{{ version.revision.pk }}</td>
|
||||
<td>{{ version.revision.user.name }}</td>
|
||||
<td>
|
||||
{% if version.changes.old is None %}
|
||||
{{object|to_class_name}} Created
|
||||
{% else %}
|
||||
{% include 'RIGS/version_changes.html' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ version.revision.comment }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="align-right">{% paginator %}</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
45
assets/templates/base_assets.html
Normal file
45
assets/templates/base_assets.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block extrahead %}
|
||||
<meta name="google" content="notranslate">
|
||||
{% endblock %}
|
||||
|
||||
{% block titleheader %}
|
||||
<a class="nav navbar-brand" href="{% url 'asset_index' %}">Assets</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block titleelements %}
|
||||
<li class="nav-item"><a class="nav-link" href="/">Home</a></li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Assets</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="{% url 'asset_list' %}"><span class="fas fa-list"></span> List Assets</a>
|
||||
{% if perms.assets.add_asset %}
|
||||
<a class="dropdown-item" href="{% url 'asset_create' %}"><span class="fas fa-plus"></span> Create Asset</a>
|
||||
{% endif %}
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="{% url 'cable_type_list' %}"><span class="fas fa-list"></span> List Cable Types</a>
|
||||
{% if perms.assets.add_cabletype %}
|
||||
<a class="dropdown-item" href="{% url 'cable_type_create' %}"><span class="fas fa-plus"></span> Create Cable Type</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
<div class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> Suppliers</a>
|
||||
<ul class="dropdown-menu">
|
||||
<a class="dropdown-item" href="{% url 'supplier_list' %}"><span class="fas fa-list"></span> List Suppliers</a>
|
||||
{% if perms.assets.add_supplier %}
|
||||
<a class="dropdown-item" href="{% url 'supplier_create' %}"><span class="fas fa-plus"></span> Create Supplier</a>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if perms.assets.view_asset %}
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'assets_activity_table' %}">Recent Changes</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'asset_audit_list' %}">Audit</a></li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block titleelements_right %}
|
||||
{% include 'partials/search.html' %}
|
||||
{% include 'partials/navbar_user.html' %}
|
||||
{% endblock %}
|
||||
40
assets/templates/cable_type_detail.html
Normal file
40
assets/templates/cable_type_detail.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% load button from filters %}
|
||||
{% load cache %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<dl class="card-body row">
|
||||
<dt class="col-6">Socket</dt>
|
||||
<dd>{{ object.socket|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt class="col-6">Plug</dt>
|
||||
<dd>{{ object.plug|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt class="col-6">Circuits</dt>
|
||||
<dd>{{ object.circuits|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt class="col-6">Cores</dt>
|
||||
<dd>{{ object.cores|default_if_none:'-' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mt-2 text-right">
|
||||
{% button 'edit' url='cable_type_update' pk=object.id %}
|
||||
</div>
|
||||
{% cache None cable_type_assets object %}
|
||||
<div class="col mt-2">
|
||||
<div class="card">
|
||||
<div class="card-header">Associated Assets</div>
|
||||
{% with object.asset_set.all as object_list %}
|
||||
{% include 'partials/asset_list_table.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
{% endcache %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
28
assets/templates/cable_type_form.html
Normal file
28
assets/templates/cable_type_form.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% load button from filters %}
|
||||
{% load cache %}
|
||||
|
||||
{% block content %}
|
||||
{% 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="">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% for field in form %}
|
||||
<div class="form-group">
|
||||
{% include 'partials/form_field.html' with field=field %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="text-right">
|
||||
{% button 'submit' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
44
assets/templates/cable_type_list.html
Normal file
44
assets/templates/cable_type_list.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% load paginator from filters %}
|
||||
{% load button from filters %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-2">
|
||||
<div class="col text-right pr-0">
|
||||
{% button 'new' 'cable_type_create' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Cable Type</th>
|
||||
<th scope="col">Circuits</th>
|
||||
<th scope="col">Cores</th>
|
||||
<th scope="col">Quick Links</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<th scope="row">{{ item }}</th>
|
||||
<td>{{ item.circuits }}</td>
|
||||
<td>{{ item.cores }}</td>
|
||||
<td>
|
||||
<a href="{% url 'cable_type_detail' item.pk %}" class="btn btn-primary"><span class="fas fa-eye"></span> View</a>
|
||||
<a href="{% url 'cable_type_update' item.pk %}" class="btn btn-warning"><span class="fas fa-edit"></span> Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% if is_paginated %}
|
||||
<div class="text-center">
|
||||
{% paginator %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,25 +1,29 @@
|
||||
{% if edit and object %}
|
||||
<!--edit-->
|
||||
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
|
||||
<a class="btn btn-default" href="{% url 'asset_duplicate' object.pk %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
|
||||
{% elif duplicate %}
|
||||
<!--duplicate-->
|
||||
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-ok-sign"></i> Create Duplicate</button>
|
||||
{% elif create %}
|
||||
<!--create-->
|
||||
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
|
||||
{% else %}
|
||||
<!--detail view-->
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'asset_update' object.asset_id %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a>
|
||||
<a class="btn btn-default" href="{% url 'asset_duplicate' object.asset_id %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
|
||||
{% load button from filters %}
|
||||
<div class="text-right py-2">
|
||||
{% if create or edit and object %}
|
||||
<!--edit-->
|
||||
{% button 'submit' %}
|
||||
{% elif duplicate %}
|
||||
<!--duplicate-->
|
||||
<button type="submit" class="btn btn-success"><span class="fas fa-check"></span> Create Duplicate</button>
|
||||
{% else %}
|
||||
<!--detail view-->
|
||||
<div class="btn-group">
|
||||
{% 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 %}
|
||||
<br>
|
||||
<button type="reset" class="btn btn-link" onclick="
|
||||
{% if duplicate %}
|
||||
{% url 'asset_detail' previous_asset_id %}
|
||||
{% else %}
|
||||
history.back()
|
||||
{% endif %}">Cancel</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if create or edit or duplicate %}
|
||||
<br>
|
||||
<button type="reset" class="btn btn-link" onclick="
|
||||
{%if duplicate%}
|
||||
{% url 'asset_detail' previous_asset_id %}
|
||||
{%else%}
|
||||
history.back(){%endif%}">Cancel</button>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load markdown_tags %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Asset Details
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="card-body">
|
||||
{% if create or edit or duplicate %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.asset_id.id_for_label }}">Asset ID</label>
|
||||
@@ -24,7 +25,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'%}
|
||||
@@ -33,6 +33,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>
|
||||
43
assets/templates/partials/asset_list_table.html
Normal file
43
assets/templates/partials/asset_list_table.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% load button from filters %}
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col">Asset ID</th>
|
||||
<th scope="col">Description</th>
|
||||
<th scope="col">Category</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col" class="d-none d-sm-table-cell">Quick Links</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="asset_table_body">
|
||||
{% for item in object_list %}
|
||||
<tr class="table-{{ item.status.display_class|default:'' }} assetRow">
|
||||
<th scope="row" class="align-middle"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></th>
|
||||
<td class="assetDesc"><span class="text-truncate d-inline-block align-middle">{{ item.description }}</span></td>
|
||||
<td class="assetCategory align-middle">{{ item.category }}</td>
|
||||
<td class="assetStatus align-middle">{{ item.status }}</td>
|
||||
<td class="d-none d-sm-table-cell">
|
||||
{% if audit %}
|
||||
<a type="button" class="btn btn-info btn-sm modal-href" href="{% url 'asset_audit' item.asset_id %}"><i class="fas fa-certificate"></i> Audit</a>
|
||||
{% else %}
|
||||
<div class="btn-group" role="group">
|
||||
{% button 'view' url='asset_detail' pk=item.asset_id clazz="btn-sm" %}
|
||||
{% if perms.assets.change_asset %}
|
||||
{% button 'edit' url='asset_update' pk=item.asset_id clazz="btn-sm" %}
|
||||
{% endif %}
|
||||
{% if perms.assets.add_asset %}
|
||||
{% button 'duplicate' url='asset_duplicate' pk=item.asset_id clazz="btn-sm" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6">Nothing found</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,19 +0,0 @@
|
||||
{% for item in object_list %}
|
||||
{# <li><a href="{% url 'asset_detail' item.pk %}">{{ item.asset_id }} - {{ item.description }}</a></li>#}
|
||||
<!---TODO: When the ability to filter the list is added, remove the colours from the filter - specifically, stop greying out sold/binned stuff if it is being searched for-->
|
||||
<tr class="{{ item.status.display_class|default:'' }} assetRow">
|
||||
<td style="vertical-align: middle;"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></td>
|
||||
<td class="assetDesc" style="vertical-align: middle; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; max-width: 25vw">{{ item.description }}</td>
|
||||
<td class="assetCategory" style="vertical-align: middle;">{{ item.category }}</td>
|
||||
<td class="assetStatus" style="vertical-align: middle;">{{ item.status }}</td>
|
||||
<td class="hidden-xs">
|
||||
<div class="btn-group" role="group">
|
||||
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_detail' item.asset_id %}"><i class="glyphicon glyphicon-eye-open"></i> View</a>
|
||||
{% if perms.assets.change_asset %}
|
||||
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_update' item.asset_id %}"><i class="glyphicon glyphicon-edit"></i> Edit</a>
|
||||
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_duplicate' item.asset_id %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -1,65 +0,0 @@
|
||||
<select name="parent" id="parent_id" class="selectpicker">
|
||||
{% if object.parent%}
|
||||
<option value="{{object.parent.pk}}" selected>{{object.parent.description}}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
|
||||
{% load static %}
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="{% static "css/bootstrap-select.min.css" %}"/>
|
||||
<link rel="stylesheet" href="{% static "css/ajax-bootstrap-select.css" %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
<script src="{% static "js/bootstrap-select.js" %}"></script>
|
||||
<script src="{% static "js/ajax-bootstrap-select.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ js.super }}
|
||||
<script>
|
||||
$('#parent_id')
|
||||
.selectpicker({
|
||||
liveSearch: true
|
||||
})
|
||||
.ajaxSelectPicker({
|
||||
ajax: {
|
||||
url: '{% url 'asset_search_json'%}',
|
||||
type: "get",
|
||||
data: function () {
|
||||
var params = {
|
||||
{% verbatim %}query: '{{{q}}}'{% endverbatim %}
|
||||
};
|
||||
return params;
|
||||
}
|
||||
},
|
||||
locale: {
|
||||
emptyTitle: 'Search for item...'
|
||||
},
|
||||
preprocessData: function(data){
|
||||
var assets = [];
|
||||
if(data.length){
|
||||
var len = data.length;
|
||||
for(var i = 0; i < len; i++){
|
||||
var curr = data[i];
|
||||
assets.push(
|
||||
{
|
||||
'value': curr.id,
|
||||
'text': curr.label,
|
||||
'disabled': false
|
||||
}
|
||||
);
|
||||
}
|
||||
assets.push(
|
||||
{
|
||||
'value': null,
|
||||
'text': "No parent"
|
||||
});
|
||||
}
|
||||
|
||||
return assets;
|
||||
},
|
||||
preserveSelected: false
|
||||
});
|
||||
</script>
|
||||
{% endblock js %}
|
||||
8
assets/templates/partials/associated_assets.html
Normal file
8
assets/templates/partials/associated_assets.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="col-12 pt-2">
|
||||
<div class="card">
|
||||
<div class="card-header">Associated Assets</div>
|
||||
{% with object.assets.all as object_list %}
|
||||
{% include 'partials/asset_list_table.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
8
assets/templates/partials/audit_details.html
Normal file
8
assets/templates/partials/audit_details.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="card {% if object.last_audited_at is not None %}border-success{% else %}border-warning{% endif %}">
|
||||
<div class="card-header">
|
||||
Audit Details
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Audited at <span class="badge badge-secondary">{{ object.last_audited_at|default_if_none:'-' }}</span> by <span class="badge badge-info">{{ object.last_audited_by|default_if_none:'-' }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,59 +1,27 @@
|
||||
{% load widget_tweaks %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="card mb-2">
|
||||
<div class="card-header">
|
||||
Cable Details
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="card-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'%}
|
||||
{% include 'partials/form_field.html' with field=form.cable_type %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.socket.id_for_label }}">Socket</label>
|
||||
{% render_field form.socket|add_class:'form-control'%}
|
||||
{% include 'partials/form_field.html' with field=form.length append=form.length.help_text %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.length.id_for_label }}">Length</label>
|
||||
<div class="input-group">
|
||||
{% render_field form.length|add_class:'form-control' %}
|
||||
<span class="input-group-addon">{{ form.length.help_text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.csa.id_for_label }}">Cross Sectional Area</label>
|
||||
<div class="input-group">
|
||||
{% render_field form.csa|add_class:'form-control' value=object.csa %}
|
||||
<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 %}
|
||||
{% include 'partials/form_field.html' with field=form.csa append=form.csa.help_text %}
|
||||
</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>{% if object.cable_type %}<a href="{{object.cable_type.get_absolute_url}}">{{ object.cable_type }}</a>{%else%}-{%endif%}</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:'-' }}mm²</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
22
assets/templates/partials/form_field.html
Normal file
22
assets/templates/partials/form_field.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load title_spaced from filters %}
|
||||
{% spaceless %}
|
||||
<label for="{{ field.id_for_label }}" {% if col %}class="col-2 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label>
|
||||
{% if append or prepend %}
|
||||
<div class="input-group {{col}}">
|
||||
{% if prepend %}
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">{{ prepend }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% render_field field|add_class:'form-control' %}
|
||||
{% if append %}
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">{{ append }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% render_field field|add_class:'form-control' class+=col %}
|
||||
{% endif %}
|
||||
{% endspaceless %}
|
||||
@@ -1,13 +1,17 @@
|
||||
{% load widget_tweaks %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="card mb-2">
|
||||
<div class="card-header">
|
||||
Collection Details
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="card-body">
|
||||
{% if create or edit or duplicate %}
|
||||
<div class="form-group" id="parent-group">
|
||||
<label for="selectpicker">Set Parent</label>
|
||||
{% include 'partials/asset_picker.html' %}
|
||||
<select name="parent" id="parent_id" class="form-control selectpicker" data-live-search="true">
|
||||
{% if object.parent %}
|
||||
<option value="{{object.parent.pk}}" selected>{{object.parent.description}}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
{% else %}
|
||||
<dl>
|
||||
@@ -24,13 +28,13 @@
|
||||
|
||||
<dt>Children</dt>
|
||||
{% if object.asset_parent.all %}
|
||||
<div style="max-height: 200px; overflow-y: auto; -webkit-overflow-scrolling: touch; ">
|
||||
{% for child in object.asset_parent.all %}
|
||||
<dd>
|
||||
<a href="{% url 'asset_detail' child.asset_id %}">
|
||||
{{ child.asset_id }} - {{ child.description }}
|
||||
</a>
|
||||
<a href="{% url 'asset_detail' child.asset_id %}">{{ child }}</a>
|
||||
</dd>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<dd><span>-</span></dd>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load static %}
|
||||
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="{% static "css/bootstrap-select.min.css" %}"/>
|
||||
<link rel="stylesheet" href="{% static "css/ajax-bootstrap-select.css" %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
<script src="{% static "js/bootstrap-select.js" %}"></script>
|
||||
<script src="{% static "js/ajax-bootstrap-select.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static "js/autocompleter.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% load linkornone from filters %}
|
||||
<div class="card mb-2">
|
||||
<div class="card-header">
|
||||
Purchase Details
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="card-body">
|
||||
{% if create or edit or duplicate %}
|
||||
<div class="form-group" id="purchased-from-group">
|
||||
<label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
|
||||
<select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
|
||||
{% if object.purchased_from %}
|
||||
<option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
|
||||
{% if object.purchased_from %}
|
||||
<option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col align-right">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'supplier_create' %}" class="btn btn-success modal-href"
|
||||
data-target="#{{ form.purchased_from.id_for_label }}">
|
||||
<span class="fas fa-plus"></span>
|
||||
</a>
|
||||
<a {% if form.supplier.value %}href="{% url 'supplier_update' form.purchased_from.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.purchased_from.id_for_label }}-update" data-target="#{{ form.purchased_from.id_for_label }}">
|
||||
<span class="fas fa-edit"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.purchase_price.id_for_label }}">Purchase Price</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">£</span>
|
||||
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
||||
{% render_field form.purchase_price|add_class:'form-control' value=object.purchase_price %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +41,7 @@
|
||||
<div class="form-group">
|
||||
<label for="{{ form.salvage_value.id_for_label }}">Salvage Value</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">£</span>
|
||||
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
||||
{% render_field form.salvage_value|add_class:'form-control' value=object.salvage_value %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,7 +53,7 @@
|
||||
{% render_field form.date_acquired|add_class:'form-control'|attr:'type="date"' value=date_acq %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<input type="date" name="date_acquired" value="{% now "Y-m-d" %}"
|
||||
<input type="date" name="date_acquired" value="{% now 'Y-m-d' %}"
|
||||
class="form-control" id="id_date_acquired">
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -68,14 +67,11 @@
|
||||
{% else %}
|
||||
<dl>
|
||||
<dt>Purchased From</dt>
|
||||
<dd>{{ object.purchased_from|default_if_none:'-' }}</dd>
|
||||
|
||||
<dd>{% if object.purchased_from %}<a href="{{object.purchased_from.get_absolute_url}}">{{ object.purchased_from }}</a>{%else%}-{%endif%}</dd>
|
||||
<dt>Purchase Price</dt>
|
||||
<dd>£{{ object.purchase_price|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Salvage Value</dt>
|
||||
<dd>£{{ object.salvage_value|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Date Acquired</dt>
|
||||
<dd>{{ object.date_acquired|default_if_none:'-' }}</dd>
|
||||
{% if object.date_sold %}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{% load widget_tweaks %}
|
||||
|
||||
<!---TODO: Assign form-control class in here--->
|
||||
<div class="form-group">
|
||||
<label for="{{ field.id_for_label }}">{{ label|default:field.label }}</label>
|
||||
{% if css %}
|
||||
{% render_field field|add_class:css %}
|
||||
{% elif disable_if_filled and field.value %}
|
||||
{% render_field field|attr:'disabled' %}
|
||||
{% elif css and disable_if_filled %}
|
||||
{% render_field field|add_class:css|attr:'disabled' %}
|
||||
{% else %}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
<span class="helper-text" data-error="{{ field.errors.text }}"></span>
|
||||
</div>
|
||||
@@ -1,73 +0,0 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% block title %}Supplier | {{ object.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% if not request.is_ajax %}
|
||||
<div class="col-sm-12">
|
||||
<h1>Supplier | {{ object.name }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 text-right">
|
||||
<div class="btn-group btn-page">
|
||||
<a href="{% url 'supplier_update' object.pk %}" class="btn btn-default"><span
|
||||
class="glyphicon glyphicon-pencil"></span> Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-6">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">Supplier Details</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Name</dt>
|
||||
<dd>{{ object.name }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Associated Assets</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Asset ID</th>
|
||||
<th>Description</th>
|
||||
<th>Category</th>
|
||||
<th>Status</th>
|
||||
<th class="hidden-xs">Quick Links</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="asset_table_body">
|
||||
{% with object.assets.all as object_list %}
|
||||
{% include 'partials/asset_list_table_body.html' %}
|
||||
{% endwith %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% if not request.is_ajax %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12 text-right">
|
||||
<div class="btn-group btn-page">
|
||||
<a href="{% url 'supplier_update' object.pk %}" class="btn btn-default"><span
|
||||
class="glyphicon glyphicon-pencil"></span> Edit</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'supplier_update' object.pk %}" title="View Revision History">
|
||||
Last edited {{ object.last_edited_at }} by {{ object.last_edited_by.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,47 +0,0 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% block title %}Supplier List{% endblock %}
|
||||
{% load paginator from filters %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Supplier List</h1>
|
||||
</div>
|
||||
|
||||
<form id="supplier-search-form" method="get" class="form-inline pull-right">
|
||||
{% csrf_token %}
|
||||
<div class="input-group pull-right" style="width: auto;">
|
||||
{% render_field form.query|add_class:'form-control' placeholder='Search by Name' style="width: 250px"%}
|
||||
<label for="query" class="sr-only">Name:</label>
|
||||
<span class="input-group-btn"><button type="submit" class="btn btn-default" id="id_search">Search</button></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Supplier</th>
|
||||
<th>Quick Links</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="asset_table_body">
|
||||
{% for item in object_list %}
|
||||
<tr class="supplierRow">
|
||||
<td class="supplierName">{{ item.name }}</td>
|
||||
<td>
|
||||
<a href="{% url 'supplier_detail' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-eye-open"></i> View</a>
|
||||
<a href="{% url 'supplier_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 %}
|
||||
@@ -1,20 +0,0 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% block title %}Edit{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Supplier
|
||||
{% if object %}
|
||||
Edit: {{ object.name }}
|
||||
{% else %}
|
||||
Create
|
||||
{% endif %}</h1>
|
||||
</div>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'form_errors.html' %}
|
||||
{{ form }}
|
||||
<input type="submit" value="Save" class="btn btn-success">
|
||||
</form>
|
||||
{% endblock %}
|
||||
35
assets/tests/conftest.py
Normal file
35
assets/tests/conftest.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import pytest
|
||||
from assets import models
|
||||
import datetime
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def category(db):
|
||||
category = models.AssetCategory.objects.create(name="Sound")
|
||||
yield category
|
||||
category.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def status(db):
|
||||
status = models.AssetStatus.objects.create(name="Broken", should_show=True)
|
||||
yield status
|
||||
status.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_cable(db, category, status):
|
||||
connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3)
|
||||
cable_type = models.CableType.objects.create(circuits=11, cores=3, plug=connector, socket=connector)
|
||||
cable = models.Asset.objects.create(asset_id="9666", description="125A -> Jack", comments="The cable from Hell...", status=status, category=category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cable_type, length=10, csa="1.5")
|
||||
yield cable
|
||||
connector.delete()
|
||||
cable_type.delete()
|
||||
cable.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_asset(db, category, status):
|
||||
asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26))
|
||||
yield asset
|
||||
asset.delete()
|
||||
@@ -1,22 +1,23 @@
|
||||
# Collection of page object models for use within tests.
|
||||
from pypom import Page, Region
|
||||
from django.urls import reverse
|
||||
from pypom import Region
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions
|
||||
from selenium.webdriver import Chrome
|
||||
from django.urls import reverse
|
||||
|
||||
from PyRIGS.tests import regions
|
||||
from PyRIGS.tests.pages import BasePage, FormPage
|
||||
import pdb
|
||||
from PyRIGS.tests.pages import BasePage, FormPage, animation_is_finished
|
||||
|
||||
|
||||
class AssetList(BasePage):
|
||||
URL_TEMPLATE = '/assets/asset/list'
|
||||
|
||||
_asset_item_locator = (By.CLASS_NAME, 'assetRow')
|
||||
_search_text_locator = (By.ID, 'id_query')
|
||||
_search_text_locator = (By.ID, 'id_q')
|
||||
_status_select_locator = (By.CSS_SELECTOR, 'div#status-group>div.bootstrap-select')
|
||||
_category_select_locator = (By.CSS_SELECTOR, 'div#category-group>div.bootstrap-select')
|
||||
_go_button_locator = (By.ID, 'filter-submit')
|
||||
_go_button_locator = (By.ID, 'id_search')
|
||||
_filter_button_locator = (By.ID, 'filter-submit')
|
||||
|
||||
class AssetListRow(Region):
|
||||
_asset_id_locator = (By.CLASS_NAME, "assetID")
|
||||
@@ -56,6 +57,9 @@ class AssetList(BasePage):
|
||||
def search(self):
|
||||
self.find_element(*self._go_button_locator).click()
|
||||
|
||||
def filter(self):
|
||||
self.find_element(*self._filter_button_locator).click()
|
||||
|
||||
@property
|
||||
def status_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._status_select_locator))
|
||||
@@ -66,9 +70,8 @@ class AssetList(BasePage):
|
||||
|
||||
|
||||
class AssetForm(FormPage):
|
||||
_purchased_from_select_locator = (By.CSS_SELECTOR, 'div#purchased-from-group>div.bootstrap-select')
|
||||
_purchased_from_select_locator = (By.XPATH, '//div[@id="purchased-from-group"]/div/div/div')
|
||||
_parent_select_locator = (By.CSS_SELECTOR, 'div#parent-group>div.bootstrap-select')
|
||||
_submit_locator = (By.CLASS_NAME, 'btn-success')
|
||||
form_items = {
|
||||
'asset_id': (regions.TextBox, (By.ID, 'id_asset_id')),
|
||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||
@@ -82,12 +85,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
|
||||
@@ -98,11 +98,6 @@ class AssetForm(FormPage):
|
||||
def parent_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._parent_select_locator))
|
||||
|
||||
def submit(self):
|
||||
previous_errors = self.errors
|
||||
self.find_element(*self._submit_locator).click()
|
||||
self.wait.until(lambda x: self.errors != previous_errors or self.success)
|
||||
|
||||
|
||||
class AssetEdit(AssetForm):
|
||||
URL_TEMPLATE = '/assets/asset/id/{asset_id}/edit/'
|
||||
@@ -122,6 +117,7 @@ class AssetCreate(AssetForm):
|
||||
|
||||
class AssetDuplicate(AssetForm):
|
||||
URL_TEMPLATE = '/assets/asset/id/{asset_id}/duplicate'
|
||||
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Duplicate')]")
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
@@ -131,12 +127,12 @@ class AssetDuplicate(AssetForm):
|
||||
class SupplierList(BasePage):
|
||||
URL_TEMPLATE = reverse('supplier_list')
|
||||
|
||||
_supplier_item_locator = (By.CLASS_NAME, 'supplierRow')
|
||||
_search_text_locator = (By.ID, 'id_query')
|
||||
_supplier_item_locator = (By.ID, 'row_item')
|
||||
_search_text_locator = (By.ID, 'id_search_text')
|
||||
_go_button_locator = (By.ID, 'id_search')
|
||||
|
||||
class SupplierListRow(Region):
|
||||
_name_locator = (By.CLASS_NAME, "supplierName")
|
||||
_name_locator = (By.ID, "cell_name")
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -160,16 +156,10 @@ class SupplierList(BasePage):
|
||||
|
||||
|
||||
class SupplierForm(FormPage):
|
||||
_submit_locator = (By.CLASS_NAME, 'btn-success')
|
||||
form_items = {
|
||||
'name': (regions.TextBox, (By.ID, 'id_name')),
|
||||
}
|
||||
|
||||
def submit(self):
|
||||
previous_errors = self.errors
|
||||
self.find_element(*self._submit_locator).click()
|
||||
self.wait.until(lambda x: self.errors != previous_errors or self.success)
|
||||
|
||||
|
||||
class SupplierCreate(SupplierForm):
|
||||
URL_TEMPLATE = reverse('supplier_create')
|
||||
@@ -186,3 +176,76 @@ class SupplierEdit(SupplierForm):
|
||||
@property
|
||||
def success(self):
|
||||
return '/edit' not in self.driver.current_url
|
||||
|
||||
|
||||
class AssetAuditList(AssetList):
|
||||
URL_TEMPLATE = reverse('asset_audit_list')
|
||||
|
||||
_search_text_locator = (By.ID, 'id_q')
|
||||
_go_button_locator = (By.ID, 'searchButton')
|
||||
_modal_locator = (By.ID, 'modal')
|
||||
_errors_selector = (By.CLASS_NAME, "alert-danger")
|
||||
|
||||
@property
|
||||
def modal(self):
|
||||
return self.AssetAuditModal(self, self.find_element(*self._modal_locator))
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
return self.find_element(*self._search_text_locator).text
|
||||
|
||||
def set_query(self, query):
|
||||
element = self.find_element(*self._search_text_locator)
|
||||
element.clear()
|
||||
element.send_keys(query)
|
||||
|
||||
def search(self):
|
||||
self.find_element(*self._go_button_locator).click()
|
||||
self.wait.until(animation_is_finished())
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
try:
|
||||
return self.find_element(*self._errors_selector)
|
||||
except NoSuchElementException:
|
||||
return None
|
||||
|
||||
class AssetAuditModal(regions.Modal):
|
||||
_errors_selector = (By.CLASS_NAME, "alert-danger")
|
||||
# Don't use the usual success selector - that tries and fails to hit the '10m long cable' helper button...
|
||||
_submit_locator = (By.ID, "id_mark_audited")
|
||||
_close_selector = (By.XPATH, "//button[@data-dismiss='modal']")
|
||||
|
||||
form_items = {
|
||||
'asset_id': (regions.TextBox, (By.ID, 'id_asset_id')),
|
||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
|
||||
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||
'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')),
|
||||
'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
|
||||
def errors(self):
|
||||
try:
|
||||
error_page = regions.ErrorPage(self, self.find_element(*self._errors_selector))
|
||||
return error_page.errors
|
||||
except NoSuchElementException:
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
self.page.find_element(*self._close_selector).click()
|
||||
self.wait.until(expected_conditions.invisibility_of_element_located((By.ID, 'modal')))
|
||||
|
||||
def remove_all_required(self):
|
||||
self.driver.execute_script("Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
|
||||
self.driver.execute_script("Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
|
||||
|
||||
@@ -1,583 +0,0 @@
|
||||
from . import pages
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from assets import models
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from urllib.parse import urlparse
|
||||
from RIGS import models as rigsmodels
|
||||
from PyRIGS.tests.base import BaseTest, AutoLoginTest
|
||||
from assets import models, urls
|
||||
from reversion import revisions as reversion
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
import datetime
|
||||
|
||||
|
||||
class TestAssetList(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
sound = models.AssetCategory.objects.create(name="Sound")
|
||||
lighting = models.AssetCategory.objects.create(name="Lighting")
|
||||
|
||||
working = models.AssetStatus.objects.create(name="Working", should_show=True)
|
||||
broken = models.AssetStatus.objects.create(name="Broken", should_show=False)
|
||||
|
||||
models.Asset.objects.create(asset_id="1", description="Broken XLR", status=broken, category=sound, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="10", description="Working Mic", status=working, category=sound, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="2", description="A light", status=working, category=lighting, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="C1", description="The pearl", status=broken, category=lighting, date_acquired=datetime.date(2020, 2, 1))
|
||||
self.page = pages.AssetList(self.driver, self.live_server_url).open()
|
||||
|
||||
def test_default_statuses_applied(self):
|
||||
# Only the working stuff should be shown initially
|
||||
assetDescriptions = list(map(lambda x: x.description, self.page.assets))
|
||||
self.assertEqual(2, len(assetDescriptions))
|
||||
self.assertIn("A light", assetDescriptions)
|
||||
self.assertIn("Working Mic", assetDescriptions)
|
||||
|
||||
def test_asset_order(self):
|
||||
# Only the working stuff should be shown initially
|
||||
self.page.status_selector.open()
|
||||
self.page.status_selector.set_option("Broken", True)
|
||||
self.page.status_selector.close()
|
||||
|
||||
self.page.search()
|
||||
|
||||
assetIDs = list(map(lambda x: x.id, self.page.assets))
|
||||
self.assertEqual("1", assetIDs[0])
|
||||
self.assertEqual("2", assetIDs[1])
|
||||
self.assertEqual("10", assetIDs[2])
|
||||
self.assertEqual("C1", assetIDs[3])
|
||||
|
||||
def test_search(self):
|
||||
self.page.set_query("10")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 1)
|
||||
self.assertEqual("Working Mic", self.page.assets[0].description)
|
||||
self.assertEqual("10", self.page.assets[0].id)
|
||||
|
||||
self.page.set_query("light")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 1)
|
||||
self.assertEqual("A light", self.page.assets[0].description)
|
||||
|
||||
self.page.set_query("Random string")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 0)
|
||||
|
||||
self.page.set_query("")
|
||||
self.page.search()
|
||||
# Only working stuff shown by default
|
||||
self.assertTrue(len(self.page.assets) == 2)
|
||||
|
||||
self.page.status_selector.toggle()
|
||||
self.assertTrue(self.page.status_selector.is_open)
|
||||
self.page.status_selector.select_all()
|
||||
self.page.status_selector.toggle()
|
||||
self.assertFalse(self.page.status_selector.is_open)
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 4)
|
||||
|
||||
self.page.category_selector.toggle()
|
||||
self.assertTrue(self.page.category_selector.is_open)
|
||||
self.page.category_selector.set_option("Sound", True)
|
||||
self.page.category_selector.close()
|
||||
self.assertFalse(self.page.category_selector.is_open)
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 2)
|
||||
assetIDs = list(map(lambda x: x.id, self.page.assets))
|
||||
self.assertEqual("1", assetIDs[0])
|
||||
self.assertEqual("10", assetIDs[1])
|
||||
|
||||
|
||||
class TestAssetForm(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.category = models.AssetCategory.objects.create(name="Health & Safety")
|
||||
self.status = models.AssetStatus.objects.create(name="O.K.", should_show=True)
|
||||
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.page = pages.AssetCreate(self.driver, self.live_server_url).open()
|
||||
|
||||
def test_asset_create(self):
|
||||
# Test that ID is automatically assigned and properly incremented
|
||||
self.assertIn(self.page.asset_id, "9001")
|
||||
|
||||
self.page.remove_all_required()
|
||||
self.page.asset_id = "XX$X"
|
||||
self.page.submit()
|
||||
self.assertFalse(self.page.success)
|
||||
self.assertIn("An Asset ID can only consist of letters and numbers, with a final number", self.page.errors["Asset id"])
|
||||
self.assertIn("This field is required.", self.page.errors["Description"])
|
||||
|
||||
self.page.open()
|
||||
|
||||
self.page.description = "Bodge Lead"
|
||||
self.page.category = "Health & Safety"
|
||||
self.page.status = "O.K."
|
||||
self.page.serial_number = "0124567890-SAUSAGE"
|
||||
self.page.comments = "This is actually a sledgehammer, not a cable..."
|
||||
|
||||
self.page.purchased_from_selector.toggle()
|
||||
self.assertTrue(self.page.purchased_from_selector.is_open)
|
||||
self.page.purchased_from_selector.search(self.supplier.name[:-8])
|
||||
self.page.purchased_from_selector.set_option(self.supplier.name, True)
|
||||
self.assertFalse(self.page.purchased_from_selector.is_open)
|
||||
self.page.purchase_price = "12.99"
|
||||
self.page.salvage_value = "99.12"
|
||||
self.date_acquired = "05022020"
|
||||
|
||||
self.page.parent_selector.toggle()
|
||||
self.assertTrue(self.page.parent_selector.is_open)
|
||||
# Searching it by ID autoselects it
|
||||
self.page.parent_selector.search(self.parent.asset_id)
|
||||
# Needed here but not earlier for whatever reason
|
||||
self.driver.implicitly_wait(1)
|
||||
# self.page.parent_selector.set_option(self.parent.asset_id + " | " + self.parent.description, True)
|
||||
# Need to explicitly close as we haven't selected anything to trigger the auto close
|
||||
self.page.parent_selector.search(Keys.ESCAPE)
|
||||
self.assertFalse(self.page.parent_selector.is_open)
|
||||
self.assertTrue(self.page.parent_selector.options[0].selected)
|
||||
|
||||
self.assertFalse(self.driver.find_element_by_id('cable-table').is_displayed())
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
def test_cable_create(self):
|
||||
self.page.description = "IEC -> IEC"
|
||||
self.page.category = "Health & Safety"
|
||||
self.page.status = "O.K."
|
||||
self.page.serial_number = "MELON-MELON-MELON"
|
||||
self.page.comments = "You might need that"
|
||||
self.page.is_cable = True
|
||||
|
||||
self.assertTrue(self.driver.find_element_by_id('cable-table').is_displayed())
|
||||
self.page.plug = "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)
|
||||
|
||||
def test_asset_edit(self):
|
||||
self.page = pages.AssetEdit(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
|
||||
|
||||
self.assertTrue(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly') is not None)
|
||||
|
||||
new_description = "Big Shelf"
|
||||
self.page.description = new_description
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
self.assertEqual(models.Asset.objects.get(asset_id=self.parent.asset_id).description, new_description)
|
||||
|
||||
def test_asset_duplicate(self):
|
||||
self.page = pages.AssetDuplicate(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
|
||||
|
||||
self.assertNotEqual(self.parent.asset_id, self.page.asset_id)
|
||||
self.assertEqual(self.parent.description, self.page.description)
|
||||
self.assertEqual(self.parent.status.name, self.page.status)
|
||||
self.assertEqual(self.parent.category.name, self.page.category)
|
||||
self.assertEqual(self.parent.date_acquired, self.page.date_acquired.date())
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
self.assertEqual(models.Asset.objects.last().description, self.parent.description)
|
||||
|
||||
|
||||
class TestSupplierList(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
models.Supplier.objects.create(name="Fullmetal Heavy Industry")
|
||||
models.Supplier.objects.create(name="Acme.")
|
||||
models.Supplier.objects.create(name="TEC PA & Lighting")
|
||||
models.Supplier.objects.create(name="Caterpillar Inc.")
|
||||
models.Supplier.objects.create(name="N.E.R.D")
|
||||
models.Supplier.objects.create(name="Khumalo")
|
||||
models.Supplier.objects.create(name="1984 Incorporated")
|
||||
self.page = pages.SupplierList(self.driver, self.live_server_url).open()
|
||||
|
||||
# Should be sorted alphabetically
|
||||
def test_order(self):
|
||||
names = list(map(lambda x: x.name, self.page.suppliers))
|
||||
self.assertEqual("1984 Incorporated", names[0])
|
||||
self.assertEqual("Acme.", names[1])
|
||||
self.assertEqual("Caterpillar Inc.", names[2])
|
||||
self.assertEqual("Fullmetal Heavy Industry", names[3])
|
||||
self.assertEqual("Khumalo", names[4])
|
||||
self.assertEqual("N.E.R.D", names[5])
|
||||
self.assertEqual("TEC PA & Lighting", names[6])
|
||||
|
||||
def test_search(self):
|
||||
self.page.set_query("TEC")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.suppliers) == 1)
|
||||
self.assertEqual("TEC PA & Lighting", self.page.suppliers[0].name)
|
||||
|
||||
self.page.set_query("")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.suppliers) == 7)
|
||||
|
||||
self.page.set_query("This is not a supplier")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.suppliers) == 0)
|
||||
|
||||
|
||||
class TestSupplierCreateAndEdit(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry")
|
||||
|
||||
def test_supplier_create(self):
|
||||
self.page = pages.SupplierCreate(self.driver, self.live_server_url).open()
|
||||
|
||||
self.page.remove_all_required()
|
||||
self.page.submit()
|
||||
self.assertFalse(self.page.success)
|
||||
self.assertIn("This field is required.", self.page.errors["Name"])
|
||||
|
||||
self.page.name = "Optican Health Supplies"
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
def test_supplier_edit(self):
|
||||
self.page = pages.SupplierEdit(self.driver, self.live_server_url, supplier_id=self.supplier.pk).open()
|
||||
|
||||
self.assertEquals("Fullmetal Heavy Industry", self.page.name)
|
||||
new_name = "Cyberdyne Systems"
|
||||
self.page.name = new_name
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
|
||||
class TestSupplierValidation(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = rigsmodels.Profile.objects.create(username="SupplierValidationTest", email="SVT@test.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
cls.supplier = models.Supplier.objects.create(name="Gadgetron Corporation")
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
def test_create(self):
|
||||
url = reverse('supplier_create')
|
||||
response = self.client.post(url)
|
||||
self.assertFormError(response, 'form', 'name', 'This field is required.')
|
||||
|
||||
def test_edit(self):
|
||||
url = reverse('supplier_update', kwargs={'pk': self.supplier.pk})
|
||||
response = self.client.post(url, {'name': ""})
|
||||
self.assertFormError(response, 'form', 'name', 'This field is required.')
|
||||
|
||||
|
||||
class Test404(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = rigsmodels.Profile.objects.create(username="404Test", email="404@test.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
def test(self):
|
||||
urls = {'asset_detail', 'asset_update', 'asset_duplicate', 'supplier_detail', 'supplier_update'}
|
||||
for url_name in urls:
|
||||
request_url = reverse(url_name, kwargs={'pk': "0000"})
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
# @tag('slow') TODO: req. Django 3.0
|
||||
class TestAccessLevels(TestCase):
|
||||
@override_settings(DEBUG=True)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# Shortcut to create the levels - bonus side effect of testing the command (hopefully) matches production
|
||||
call_command('generateSampleData')
|
||||
|
||||
# Nothing should be available to the unauthenticated
|
||||
def test_unauthenticated(self):
|
||||
for url in urls.urlpatterns:
|
||||
if url.name is not None:
|
||||
pattern = str(url.pattern)
|
||||
if "json" in url.name or pattern:
|
||||
# TODO
|
||||
pass
|
||||
elif ":pk>" in pattern:
|
||||
request_url = reverse(url.name, kwargs={'pk': 9})
|
||||
else:
|
||||
request_url = reverse(url.name)
|
||||
response = self.client.get(request_url, HTTP_HOST='example.com')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
response = self.client.get(request_url, follow=True, HTTP_HOST='example.com')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'login')
|
||||
|
||||
def test_basic_access(self):
|
||||
self.assertTrue(self.client.login(username="basic", password="basic"))
|
||||
|
||||
url = reverse('asset_list')
|
||||
response = self.client.get(url)
|
||||
# Check edit and duplicate buttons not shown in list
|
||||
self.assertNotContains(response, 'Edit')
|
||||
self.assertNotContains(response, 'Duplicate')
|
||||
|
||||
url = reverse('asset_detail', kwargs={'pk': "9000"})
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, 'Purchase Details')
|
||||
self.assertNotContains(response, 'View Revision History')
|
||||
|
||||
urls = {'asset_history', 'asset_update', 'asset_duplicate'}
|
||||
for url_name in urls:
|
||||
request_url = reverse(url_name, kwargs={'pk': "9000"})
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
request_url = reverse('supplier_create')
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
request_url = reverse('supplier_update', kwargs={'pk': "1"})
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_keyholder_access(self):
|
||||
self.assertTrue(self.client.login(username="keyholder", password="keyholder"))
|
||||
|
||||
url = reverse('asset_list')
|
||||
response = self.client.get(url)
|
||||
# Check edit and duplicate buttons shown in list
|
||||
self.assertContains(response, 'Edit')
|
||||
self.assertContains(response, 'Duplicate')
|
||||
|
||||
url = reverse('asset_detail', kwargs={'pk': "9000"})
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, 'Purchase Details')
|
||||
self.assertContains(response, 'View Revision History')
|
||||
|
||||
# def test_finance_access(self): Level not used in assets currently
|
||||
|
||||
|
||||
class TestFormValidation(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = rigsmodels.Profile.objects.create(username="AssetCreateValidationTest", email="acvt@test.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
cls.category = models.AssetCategory.objects.create(name="Sound")
|
||||
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)
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
def test_asset_create(self):
|
||||
url = reverse('asset_create')
|
||||
response = self.client.post(url, {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'})
|
||||
self.assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'description', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'status', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'category', 'This field is required.')
|
||||
|
||||
self.assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
|
||||
self.assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
|
||||
self.assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
|
||||
|
||||
def test_cable_create(self):
|
||||
url = reverse('asset_create')
|
||||
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', '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):
|
||||
url = reverse('asset_update', kwargs={'pk': self.asset.asset_id})
|
||||
response = self.client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'salvage_value': '-50', 'description': "", 'status': "", 'category': ""})
|
||||
# self.assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'description', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'status', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'category', 'This field is required.')
|
||||
|
||||
self.assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
|
||||
self.assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
|
||||
self.assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
|
||||
|
||||
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})
|
||||
|
||||
# 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')
|
||||
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})
|
||||
|
||||
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):
|
||||
@override_settings(DEBUG=True)
|
||||
def test_generate_sample_data(self):
|
||||
# Run the management command and check there are no exceptions
|
||||
call_command('generateSampleAssetsData')
|
||||
|
||||
# Check there are lots
|
||||
self.assertTrue(models.Asset.objects.all().count() > 50)
|
||||
self.assertTrue(models.Supplier.objects.all().count() > 50)
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
def test_delete_sample_data(self):
|
||||
call_command('deleteSampleData')
|
||||
|
||||
self.assertTrue(models.Asset.objects.all().count() == 0)
|
||||
self.assertTrue(models.Supplier.objects.all().count() == 0)
|
||||
|
||||
def test_production_exception(self):
|
||||
from django.core.management.base import CommandError
|
||||
|
||||
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleAssetsData')
|
||||
self.assertRaisesRegex(CommandError, ".*production", call_command, 'deleteSampleData')
|
||||
|
||||
|
||||
class TestVersioningViews(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = rigsmodels.Profile.objects.create(username="VersionTest", email="version@test.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
|
||||
working = models.AssetStatus.objects.create(name="Working", should_show=True)
|
||||
broken = models.AssetStatus.objects.create(name="Broken", should_show=False)
|
||||
general = models.AssetCategory.objects.create(name="General")
|
||||
lighting = models.AssetCategory.objects.create(name="Lighting")
|
||||
|
||||
cls.assets = {}
|
||||
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(cls.profile)
|
||||
cls.assets[1] = models.Asset.objects.create(asset_id="1991", description="Spaceflower", status=broken, category=lighting, date_acquired=datetime.date(1991, 12, 26))
|
||||
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(cls.profile)
|
||||
cls.assets[2] = models.Asset.objects.create(asset_id="0001", description="Virgil", status=working, category=lighting, date_acquired=datetime.date(2015, 1, 1))
|
||||
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(cls.profile)
|
||||
cls.assets[1].status = working
|
||||
cls.assets[1].save()
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
def test_history_loads_successfully(self):
|
||||
request_url = reverse('asset_history', kwargs={'pk': self.assets[1].asset_id})
|
||||
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_activity_table_loads_successfully(self):
|
||||
request_url = reverse('asset_activity_table')
|
||||
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class TestEmbeddedViews(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = rigsmodels.Profile.objects.create(username="EmbeddedViewsTest", email="embedded@test.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
|
||||
working = models.AssetStatus.objects.create(name="Working", should_show=True)
|
||||
lighting = models.AssetCategory.objects.create(name="Lighting")
|
||||
|
||||
cls.assets = {
|
||||
1: models.Asset.objects.create(asset_id="1991", description="Spaceflower", status=working, category=lighting, date_acquired=datetime.date(1991, 12, 26))
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
|
||||
def testLoginRedirect(self):
|
||||
request_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id})
|
||||
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||
|
||||
# Request the page and check it redirects
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertRedirects(response, expected_url, status_code=302, target_status_code=200)
|
||||
|
||||
# Now login
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
# And check that it no longer redirects
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(len(response.redirect_chain), 0)
|
||||
|
||||
def testLoginCookieWarning(self):
|
||||
login_url = reverse('login_embed')
|
||||
response = self.client.post(login_url, follow=True)
|
||||
self.assertContains(response, "Cookies do not seem to be enabled")
|
||||
|
||||
def testXFrameHeaders(self):
|
||||
asset_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id})
|
||||
login_url = reverse('login_embed')
|
||||
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
response = self.client.get(asset_url, follow=True)
|
||||
with self.assertRaises(KeyError):
|
||||
response._headers["X-Frame-Options"]
|
||||
|
||||
response = self.client.get(login_url, follow=True)
|
||||
with self.assertRaises(KeyError):
|
||||
response._headers["X-Frame-Options"]
|
||||
|
||||
def testOEmbed(self):
|
||||
asset_url = reverse('asset_detail', kwargs={'pk': self.assets[1].asset_id})
|
||||
asset_embed_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id})
|
||||
oembed_url = reverse('asset_oembed', kwargs={'pk': self.assets[1].asset_id})
|
||||
|
||||
alt_oembed_url = reverse('asset_oembed', kwargs={'pk': 999})
|
||||
alt_asset_embed_url = reverse('asset_embed', kwargs={'pk': 999})
|
||||
|
||||
# Test the meta tag is in place
|
||||
response = self.client.get(asset_url, follow=True, HTTP_HOST='example.com')
|
||||
self.assertContains(response, '<link rel="alternate" type="application/json+oembed"')
|
||||
self.assertContains(response, oembed_url)
|
||||
|
||||
# Test that the JSON exists
|
||||
response = self.client.get(oembed_url, follow=True, HTTP_HOST='example.com')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, asset_embed_url)
|
||||
|
||||
# Should also work for non-existant
|
||||
response = self.client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, alt_asset_embed_url)
|
||||
347
assets/tests/test_interaction.py
Normal file
347
assets/tests/test_interaction.py
Normal file
@@ -0,0 +1,347 @@
|
||||
import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
from PyRIGS.tests.base import AutoLoginTest, screenshot_failure_cls, assert_times_almost_equal
|
||||
from PyRIGS.tests.pages import animation_is_finished
|
||||
from assets import models
|
||||
from . import pages
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestAssetList(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
sound = models.AssetCategory.objects.create(name="Sound")
|
||||
lighting = models.AssetCategory.objects.create(name="Lighting")
|
||||
|
||||
working = models.AssetStatus.objects.create(name="Working", should_show=True)
|
||||
broken = models.AssetStatus.objects.create(name="Broken", should_show=False)
|
||||
|
||||
models.Asset.objects.create(asset_id="1", description="Broken XLR", status=broken, category=sound,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="10", description="Working Mic", status=working, category=sound,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="2", description="A light", status=working, category=lighting,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="C1", description="The pearl", status=broken, category=lighting,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
self.page = pages.AssetList(self.driver, self.live_server_url).open()
|
||||
|
||||
def test_default_statuses_applied(self):
|
||||
# Only the working stuff should be shown initially
|
||||
asset_descriptions = list(map(lambda x: x.description, self.page.assets))
|
||||
self.assertEqual(2, len(asset_descriptions))
|
||||
self.assertIn("A light", asset_descriptions)
|
||||
self.assertIn("Working Mic", asset_descriptions)
|
||||
|
||||
def test_asset_order(self):
|
||||
# Only the working stuff should be shown initially
|
||||
self.page.status_selector.open()
|
||||
self.page.status_selector.set_option("Broken", True)
|
||||
self.page.status_selector.close()
|
||||
|
||||
self.page.search()
|
||||
|
||||
asset_ids = list(map(lambda x: x.id, self.page.assets))
|
||||
self.assertEqual("1", asset_ids[0])
|
||||
self.assertEqual("2", asset_ids[1])
|
||||
self.assertEqual("10", asset_ids[2])
|
||||
self.assertEqual("C1", asset_ids[3])
|
||||
|
||||
def test_search(self):
|
||||
self.page.set_query("10")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 1)
|
||||
self.assertEqual("Working Mic", self.page.assets[0].description)
|
||||
self.assertEqual("10", self.page.assets[0].id)
|
||||
|
||||
self.page.set_query("light")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 1)
|
||||
self.assertEqual("A light", self.page.assets[0].description)
|
||||
|
||||
self.page.set_query("Random string")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 0)
|
||||
|
||||
self.page.set_query("")
|
||||
self.page.search()
|
||||
# Only working stuff shown by default
|
||||
self.assertTrue(len(self.page.assets) == 2)
|
||||
|
||||
self.page.status_selector.toggle()
|
||||
self.assertTrue(self.page.status_selector.is_open)
|
||||
self.page.status_selector.select_all()
|
||||
self.page.status_selector.toggle()
|
||||
self.assertFalse(self.page.status_selector.is_open)
|
||||
self.page.filter()
|
||||
self.assertTrue(len(self.page.assets) == 4)
|
||||
|
||||
self.page.category_selector.toggle()
|
||||
self.assertTrue(self.page.category_selector.is_open)
|
||||
self.page.category_selector.set_option("Sound", True)
|
||||
self.page.category_selector.close()
|
||||
self.assertFalse(self.page.category_selector.is_open)
|
||||
self.page.filter()
|
||||
self.assertTrue(len(self.page.assets) == 2)
|
||||
asset_ids = list(map(lambda x: x.id, self.page.assets))
|
||||
self.assertEqual("1", asset_ids[0])
|
||||
self.assertEqual("10", asset_ids[1])
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestAssetForm(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.category = models.AssetCategory.objects.create(name="Health & Safety")
|
||||
self.status = models.AssetStatus.objects.create(name="O.K.", should_show=True)
|
||||
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):
|
||||
# Test that ID is automatically assigned and properly incremented
|
||||
# self.assertIn(self.page.asset_id, "9001") FIXME
|
||||
|
||||
self.page.remove_all_required()
|
||||
self.page.asset_id = "XX$X"
|
||||
self.page.submit()
|
||||
self.assertFalse(self.page.success)
|
||||
self.assertIn("An Asset ID can only consist of letters and numbers, with a final number",
|
||||
self.page.errors["Asset id"])
|
||||
self.assertIn("This field is required.", self.page.errors["Description"])
|
||||
|
||||
self.page.open()
|
||||
|
||||
self.page.description = desc = "Bodge Lead"
|
||||
self.page.category = cat = "Health & Safety"
|
||||
self.page.status = status = "O.K."
|
||||
self.page.serial_number = sn = "0124567890-SAUSAGE"
|
||||
self.page.comments = comments = "This is actually a sledgehammer, not a cable..."
|
||||
|
||||
self.page.purchase_price = "12.99"
|
||||
self.page.salvage_value = "99.12"
|
||||
self.page.date_acquired = acquired = datetime.date(2020, 5, 2)
|
||||
self.page.purchased_from_selector.toggle()
|
||||
self.assertTrue(self.page.purchased_from_selector.is_open)
|
||||
self.page.purchased_from_selector.search(self.supplier.name[:-8])
|
||||
self.page.purchased_from_selector.set_option(self.supplier.name, True)
|
||||
|
||||
self.page.parent_selector.toggle()
|
||||
self.assertTrue(self.page.parent_selector.is_open)
|
||||
option = str(self.parent)
|
||||
self.page.parent_selector.search(option)
|
||||
self.driver.implicitly_wait(1)
|
||||
self.page.parent_selector.set_option(option, True)
|
||||
self.assertTrue(self.page.parent_selector.options[0].selected)
|
||||
self.page.parent_selector.toggle()
|
||||
|
||||
self.assertFalse(self.driver.find_element_by_id('cable-table').is_displayed())
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
# Check that data is right
|
||||
asset = models.Asset.objects.get(asset_id="9001")
|
||||
self.assertEqual(asset.description, desc)
|
||||
self.assertEqual(asset.category.name, cat)
|
||||
self.assertEqual(asset.status.name, status)
|
||||
self.assertEqual(asset.serial_number, sn)
|
||||
self.assertEqual(asset.comments, comments)
|
||||
# This one is important as it defaults to today's date
|
||||
self.assertEqual(asset.date_acquired, acquired)
|
||||
|
||||
def test_cable_create(self):
|
||||
self.page.description = "IEC -> IEC"
|
||||
self.page.category = "Health & Safety"
|
||||
self.page.status = "O.K."
|
||||
self.page.serial_number = "MELON-MELON-MELON"
|
||||
self.page.comments = "You might need that"
|
||||
self.page.is_cable = True
|
||||
|
||||
self.assertTrue(self.driver.find_element_by_id('cable-table').is_displayed())
|
||||
self.wait.until(animation_is_finished())
|
||||
self.page.cable_type = "IEC → IEC"
|
||||
self.page.socket = "IEC"
|
||||
self.page.length = 10
|
||||
self.page.csa = "1.5"
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
def test_asset_edit(self):
|
||||
self.page = pages.AssetEdit(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
|
||||
|
||||
self.assertTrue(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly') is not None)
|
||||
|
||||
new_description = "Big Shelf"
|
||||
self.page.description = new_description
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
self.assertEqual(models.Asset.objects.get(asset_id=self.parent.asset_id).description, new_description)
|
||||
|
||||
def test_asset_duplicate(self):
|
||||
self.page = pages.AssetDuplicate(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
|
||||
|
||||
self.assertNotEqual(self.parent.asset_id, self.page.asset_id)
|
||||
self.assertEqual(self.parent.description, self.page.description)
|
||||
self.assertEqual(self.parent.status.name, self.page.status)
|
||||
self.assertEqual(self.parent.category.name, self.page.category)
|
||||
self.assertEqual(self.parent.date_acquired, self.page.date_acquired.date())
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
self.assertEqual(models.Asset.objects.last().description, self.parent.description)
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestSupplierList(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
models.Supplier.objects.create(name="Fullmetal Heavy Industry")
|
||||
models.Supplier.objects.create(name="Acme.")
|
||||
models.Supplier.objects.create(name="TEC PA & Lighting")
|
||||
models.Supplier.objects.create(name="Caterpillar Inc.")
|
||||
models.Supplier.objects.create(name="N.E.R.D")
|
||||
models.Supplier.objects.create(name="Khumalo")
|
||||
models.Supplier.objects.create(name="1984 Incorporated")
|
||||
self.page = pages.SupplierList(self.driver, self.live_server_url).open()
|
||||
|
||||
# Should be sorted alphabetically
|
||||
def test_order(self):
|
||||
names = list(map(lambda x: x.name, self.page.suppliers))
|
||||
self.assertEqual("1984 Incorporated", names[0])
|
||||
self.assertEqual("Acme.", names[1])
|
||||
self.assertEqual("Caterpillar Inc.", names[2])
|
||||
self.assertEqual("Fullmetal Heavy Industry", names[3])
|
||||
self.assertEqual("Khumalo", names[4])
|
||||
self.assertEqual("N.E.R.D", names[5])
|
||||
self.assertEqual("TEC PA & Lighting", names[6])
|
||||
|
||||
def test_search(self):
|
||||
self.page.set_query("TEC")
|
||||
self.page.search()
|
||||
|
||||
self.assertTrue(len(self.page.suppliers) == 1)
|
||||
self.assertEqual("TEC PA & Lighting", self.page.suppliers[0].name)
|
||||
|
||||
self.page.set_query("")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.suppliers) == 7)
|
||||
|
||||
self.page.set_query("This is not a supplier")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.suppliers) == 0)
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestSupplierCreateAndEdit(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry")
|
||||
|
||||
def test_supplier_create(self):
|
||||
self.page = pages.SupplierCreate(self.driver, self.live_server_url).open()
|
||||
|
||||
self.page.remove_all_required()
|
||||
self.page.submit()
|
||||
self.assertFalse(self.page.success)
|
||||
self.assertIn("This field is required.", self.page.errors["Name"])
|
||||
|
||||
self.page.name = "Optican Health Supplies"
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
def test_supplier_edit(self):
|
||||
self.page = pages.SupplierEdit(self.driver, self.live_server_url, supplier_id=self.supplier.pk).open()
|
||||
|
||||
self.assertEqual("Fullmetal Heavy Industry", self.page.name)
|
||||
new_name = "Cyberdyne Systems"
|
||||
self.page.name = new_name
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
|
||||
def test_audit_search(logged_in_browser, live_server, test_asset):
|
||||
page = pages.AssetAuditList(logged_in_browser.driver, live_server.url).open()
|
||||
# Check that a failed search works
|
||||
page.set_query("NOTFOUND")
|
||||
page.search()
|
||||
assert not logged_in_browser.find_by_id('modal').visible
|
||||
logged_in_browser.driver.implicitly_wait(4)
|
||||
assert logged_in_browser.is_text_present("Asset with that ID does not exist!")
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestAssetAudit(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.category = models.AssetCategory.objects.create(name="Haulage")
|
||||
self.status = models.AssetStatus.objects.create(name="Probably Fine", should_show=True)
|
||||
self.supplier = models.Supplier.objects.create(name="The Bazaar")
|
||||
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1,
|
||||
voltage_rating=40, num_pins=13)
|
||||
models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status,
|
||||
category=self.category, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status,
|
||||
category=self.category, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
self.asset = models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status,
|
||||
category=self.category,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
self.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
|
||||
self.wait = WebDriverWait(self.driver, 20)
|
||||
|
||||
def test_audit_fail(self):
|
||||
self.page.set_query(self.asset.asset_id)
|
||||
self.page.search()
|
||||
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
||||
# Do it wrong on purpose to check error display
|
||||
self.page.modal.remove_all_required()
|
||||
self.page.modal.description = ""
|
||||
self.page.modal.submit()
|
||||
self.wait.until(animation_is_finished())
|
||||
self.driver.implicitly_wait(4)
|
||||
self.assertIn("This field is required.", self.page.modal.errors["Description"])
|
||||
|
||||
def test_audit_success(self):
|
||||
self.page.set_query(self.asset.asset_id)
|
||||
self.page.search()
|
||||
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
||||
# Now do it properly
|
||||
self.page.modal.description = new_desc = "A BIG hammer"
|
||||
self.page.modal.submit()
|
||||
self.driver.implicitly_wait(4)
|
||||
self.wait.until(animation_is_finished())
|
||||
submit_time = timezone.now()
|
||||
# Check data is correct
|
||||
self.asset.refresh_from_db()
|
||||
self.assertEqual(self.asset.description, new_desc)
|
||||
# Make sure audit 'log' was filled out
|
||||
self.assertEqual(self.profile.initials, self.asset.last_audited_by.initials)
|
||||
assert_times_almost_equal(submit_time, self.asset.last_audited_at)
|
||||
# Check we've removed it from the 'needing audit' list
|
||||
self.assertNotIn(self.asset.asset_id, self.page.assets)
|
||||
|
||||
def test_audit_list(self):
|
||||
self.assertEqual(len(models.Asset.objects.filter(last_audited_at=None)), len(self.page.assets))
|
||||
asset_row = self.page.assets[0]
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@class,'btn') and contains(., 'Audit')]").click()
|
||||
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
||||
self.assertEqual(self.page.modal.asset_id, asset_row.id)
|
||||
self.page.modal.close()
|
||||
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
|
||||
# Make sure audit log was NOT filled out
|
||||
audited = models.Asset.objects.get(asset_id=asset_row.id)
|
||||
assert audited.last_audited_by is None
|
||||
131
assets/tests/test_unit.py
Normal file
131
assets/tests/test_unit.py
Normal file
@@ -0,0 +1,131 @@
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from pytest_django.asserts import assertFormError, assertRedirects, assertContains, assertNotContains
|
||||
|
||||
from PyRIGS.tests.base import assert_oembed, login
|
||||
|
||||
from assets import models
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_supplier_create(admin_client):
|
||||
url = reverse('supplier_create')
|
||||
response = admin_client.post(url)
|
||||
assertFormError(response, 'form', 'name', 'This field is required.')
|
||||
|
||||
|
||||
def test_supplier_edit(admin_client):
|
||||
supplier = models.Supplier.objects.create(name="Gadgetron Corporation")
|
||||
url = reverse('supplier_update', kwargs={'pk': supplier.pk})
|
||||
response = admin_client.post(url, {'name': ""})
|
||||
assertFormError(response, 'form', 'name', 'This field is required.')
|
||||
|
||||
|
||||
def test_404(admin_client):
|
||||
urls = {'asset_detail', 'asset_update', 'asset_duplicate', 'supplier_detail', 'supplier_update'}
|
||||
for url_name in urls:
|
||||
request_url = reverse(url_name, kwargs={'pk': "0000"})
|
||||
response = admin_client.get(request_url, follow=True)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_embed_login_redirect(client, django_user_model, test_asset):
|
||||
request_url = reverse('asset_embed', kwargs={'pk': test_asset.asset_id})
|
||||
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||
|
||||
# Request the page and check it redirects
|
||||
response = client.get(request_url, follow=True)
|
||||
assertRedirects(response, expected_url, status_code=302, target_status_code=200)
|
||||
|
||||
# Now login
|
||||
login(client, django_user_model)
|
||||
|
||||
# And check that it no longer redirects
|
||||
response = client.get(request_url, follow=True)
|
||||
assert len(response.redirect_chain) == 0
|
||||
|
||||
|
||||
def test_login_cookie_warning(client, django_user_model):
|
||||
login_url = reverse('login_embed')
|
||||
response = client.post(login_url, follow=True)
|
||||
assert "Cookies do not seem to be enabled" in str(response.content)
|
||||
|
||||
|
||||
def test_x_frame_headers(client, django_user_model, test_asset):
|
||||
asset_url = reverse('asset_embed', kwargs={'pk': test_asset.asset_id})
|
||||
login_url = reverse('login_embed')
|
||||
|
||||
login(client, django_user_model)
|
||||
|
||||
response = client.get(asset_url, follow=True)
|
||||
with pytest.raises(KeyError):
|
||||
response._headers["X-Frame-Options"]
|
||||
|
||||
response = client.get(login_url, follow=True)
|
||||
with pytest.raises(KeyError):
|
||||
response._headers["X-Frame-Options"]
|
||||
|
||||
|
||||
def test_oembed(client, test_asset):
|
||||
client.logout()
|
||||
asset_url = reverse('asset_detail', kwargs={'pk': test_asset.asset_id})
|
||||
asset_embed_url = reverse('asset_embed', kwargs={'pk': test_asset.asset_id})
|
||||
oembed_url = reverse('asset_oembed', kwargs={'pk': test_asset.asset_id})
|
||||
|
||||
alt_oembed_url = reverse('asset_oembed', kwargs={'pk': 999})
|
||||
alt_asset_embed_url = reverse('asset_embed', kwargs={'pk': 999})
|
||||
|
||||
assert_oembed(alt_asset_embed_url, alt_oembed_url, client, asset_embed_url, asset_url, oembed_url)
|
||||
|
||||
|
||||
def test_asset_create(admin_client):
|
||||
response = admin_client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'})
|
||||
assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
||||
assert_asset_form_errors(response)
|
||||
|
||||
|
||||
def test_cable_create(admin_client):
|
||||
response = admin_client.post(reverse('asset_create'), {'asset_id': 'X$%A', 'is_cable': True})
|
||||
assertFormError(response, 'form', 'asset_id', 'An Asset ID can only consist of letters and numbers, with a final number')
|
||||
assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
|
||||
assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
|
||||
|
||||
def test_asset_edit(admin_client, test_asset):
|
||||
url = reverse('asset_update', kwargs={'pk': test_asset.asset_id})
|
||||
response = admin_client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'salvage_value': '-50', 'description': "", 'status': "", 'category': ""})
|
||||
assert_asset_form_errors(response)
|
||||
|
||||
|
||||
def test_cable_edit(admin_client, test_cable):
|
||||
url = reverse('asset_update', kwargs={'pk': test_cable.asset_id})
|
||||
# TODO Why do I have to send is_cable=True here?
|
||||
response = admin_client.post(url, {'is_cable': True, 'length': -3, 'csa': -3})
|
||||
|
||||
# TODO Can't figure out how to select the 'none' option...
|
||||
# assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
|
||||
assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
|
||||
|
||||
def test_asset_duplicate(admin_client, test_cable):
|
||||
url = reverse('asset_duplicate', kwargs={'pk': test_cable.asset_id})
|
||||
response = admin_client.post(url, {'is_cable': True, 'length': 0, 'csa': 0})
|
||||
|
||||
assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
|
||||
|
||||
def assert_asset_form_errors(response):
|
||||
assertFormError(response, 'form', 'description', 'This field is required.')
|
||||
assertFormError(response, 'form', 'status', 'This field is required.')
|
||||
assertFormError(response, 'form', 'category', 'This field is required.')
|
||||
assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
|
||||
assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
|
||||
assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
|
||||
@@ -1,44 +1,46 @@
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from assets import views, models
|
||||
from RIGS import versioning
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import path, register_converter
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
|
||||
from PyRIGS.decorators import has_oembed, permission_required_with_403
|
||||
from PyRIGS.views import OEmbedView
|
||||
from . import views, converters
|
||||
|
||||
register_converter(converters.AssetIDConverter, 'asset')
|
||||
|
||||
urlpatterns = [
|
||||
path('', login_required(views.AssetList.as_view()), name='asset_index'),
|
||||
path('asset/list/', login_required(views.AssetList.as_view()), name='asset_list'),
|
||||
path('asset/id/<str:pk>/', has_oembed(oembed_view="asset_oembed")(views.AssetDetail.as_view()), name='asset_detail'),
|
||||
path('asset/id/<asset:pk>/', has_oembed(oembed_view="asset_oembed")(views.AssetDetail.as_view()), name='asset_detail'),
|
||||
path('asset/create/', permission_required_with_403('assets.add_asset')
|
||||
(views.AssetCreate.as_view()), name='asset_create'),
|
||||
path('asset/id/<str:pk>/edit/', permission_required_with_403('assets.change_asset')
|
||||
path('asset/id/<asset:pk>/edit/', permission_required_with_403('assets.change_asset')
|
||||
(views.AssetEdit.as_view()), name='asset_update'),
|
||||
path('asset/id/<str:pk>/duplicate/', permission_required_with_403('assets.add_asset')
|
||||
path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset')
|
||||
(views.AssetDuplicate.as_view()), name='asset_duplicate'),
|
||||
path('asset/id/<str:pk>/history/', permission_required_with_403('assets.view_asset')(views.AssetVersionHistory.as_view()),
|
||||
name='asset_history', kwargs={'model': models.Asset}),
|
||||
path('activity', permission_required_with_403('assets.view_asset')
|
||||
(views.ActivityTable.as_view()), name='asset_activity_table'),
|
||||
path('asset/id/<asset:pk>/label', login_required(views.GenerateLabel.as_view()), name='generate_label'),
|
||||
|
||||
path('asset/search/', views.AssetSearch.as_view(), name='asset_search_json'),
|
||||
path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'),
|
||||
path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'),
|
||||
path('cabletype/<int:pk>/update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'),
|
||||
path('cabletype/<int:pk>/detail/', login_required(views.CableTypeDetail.as_view()), name='cable_type_detail'),
|
||||
|
||||
path('asset/search/', login_required(views.AssetSearch.as_view()), name='asset_search_json'),
|
||||
path('asset/id/<str:pk>/embed/',
|
||||
xframe_options_exempt(
|
||||
login_required(login_url='/user/login/embed/')(views.AssetEmbed.as_view())),
|
||||
login_required(login_url='/user/login/embed/')(views.AssetEmbed.as_view())),
|
||||
name='asset_embed'),
|
||||
path('asset/id/<str:pk>/oembed_json/',
|
||||
views.AssetOembed.as_view(),
|
||||
name='asset_oembed'),
|
||||
path('asset/id/<str:pk>/oembed_json/', views.AssetOEmbed.as_view(), name='asset_oembed'),
|
||||
|
||||
path('supplier/list', views.SupplierList.as_view(), name='supplier_list'),
|
||||
path('supplier/<int:pk>', views.SupplierDetail.as_view(), name='supplier_detail'),
|
||||
path('supplier/create', permission_required_with_403('assets.add_supplier')
|
||||
path('asset/audit/', permission_required_with_403('assets.change_asset')(views.AssetAuditList.as_view()), name='asset_audit_list'),
|
||||
path('asset/id/<str:pk>/audit/', permission_required_with_403('assets.change_asset')(views.AssetAudit.as_view()), name='asset_audit'),
|
||||
|
||||
path('supplier/list/', login_required(views.SupplierList.as_view()), name='supplier_list'),
|
||||
path('supplier/<int:pk>/', login_required(views.SupplierDetail.as_view()), name='supplier_detail'),
|
||||
path('supplier/create/', permission_required_with_403('assets.add_supplier')
|
||||
(views.SupplierCreate.as_view()), name='supplier_create'),
|
||||
path('supplier/<int:pk>/edit', permission_required_with_403('assets.change_supplier')
|
||||
path('supplier/<int:pk>/edit/', permission_required_with_403('assets.change_supplier')
|
||||
(views.SupplierUpdate.as_view()), name='supplier_update'),
|
||||
path('supplier/<int:pk>/history/', views.SupplierVersionHistory.as_view(),
|
||||
name='supplier_history', kwargs={'model': models.Supplier}),
|
||||
|
||||
path('supplier/search/', views.SupplierSearch.as_view(), name='supplier_search_json'),
|
||||
path('supplier/search/', login_required(views.SupplierSearch.as_view()), name='supplier_search_json'),
|
||||
]
|
||||
|
||||
308
assets/views.py
308
assets/views.py
@@ -1,19 +1,27 @@
|
||||
import simplejson
|
||||
import random
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import JsonResponse
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.core import serializers
|
||||
from django.db.models import Q
|
||||
from django.http import Http404, HttpResponse, JsonResponse
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import generic
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from assets import models, forms
|
||||
from RIGS import versioning
|
||||
|
||||
import simplejson
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from barcode import Code39
|
||||
from barcode.writer import ImageWriter
|
||||
|
||||
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \
|
||||
is_ajax, OEmbedView
|
||||
from assets import forms, models
|
||||
from assets.models import get_available_asset_id
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class AssetList(LoginRequiredMixin, generic.ListView):
|
||||
model = models.Asset
|
||||
template_name = 'asset_list.html'
|
||||
@@ -26,9 +34,7 @@ class AssetList(LoginRequiredMixin, generic.ListView):
|
||||
return initial
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.method == 'POST':
|
||||
self.form = forms.AssetSearchForm(data=self.request.POST)
|
||||
elif self.request.method == 'GET' and len(self.request.GET) > 0:
|
||||
if self.request.method == 'GET' and len(self.request.GET) > 0:
|
||||
self.form = forms.AssetSearchForm(data=self.request.GET)
|
||||
else:
|
||||
self.form = forms.AssetSearchForm(data=self.get_initial())
|
||||
@@ -37,14 +43,14 @@ class AssetList(LoginRequiredMixin, generic.ListView):
|
||||
return self.model.objects.none()
|
||||
|
||||
# TODO Feedback to user when search fails
|
||||
query_string = form.cleaned_data['query'] or ""
|
||||
query_string = form.cleaned_data['q'] or ""
|
||||
if len(query_string) == 0:
|
||||
queryset = self.model.objects.all()
|
||||
elif len(query_string) >= 3:
|
||||
queryset = self.model.objects.filter(
|
||||
Q(asset_id__exact=query_string) | Q(description__icontains=query_string) | Q(serial_number__exact=query_string))
|
||||
Q(asset_id__exact=query_string.upper()) | Q(description__icontains=query_string) | Q(serial_number__exact=query_string))
|
||||
else:
|
||||
queryset = self.model.objects.filter(Q(asset_id__exact=query_string))
|
||||
queryset = self.model.objects.filter(Q(asset_id__exact=query_string.upper()))
|
||||
|
||||
if form.cleaned_data['category']:
|
||||
queryset = queryset.filter(category__in=form.cleaned_data['category'])
|
||||
@@ -55,15 +61,17 @@ class AssetList(LoginRequiredMixin, generic.ListView):
|
||||
queryset = queryset.filter(
|
||||
status__in=models.AssetStatus.objects.filter(should_show=True))
|
||||
|
||||
return queryset
|
||||
return queryset.select_related('category', 'status')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AssetList, self).get_context_data(**kwargs)
|
||||
context["form"] = self.form
|
||||
|
||||
if hasattr(self.form, 'cleaned_data'):
|
||||
context["category_filters"] = self.form.cleaned_data.get('category')
|
||||
context["status_filters"] = self.form.cleaned_data.get('status')
|
||||
context["categories"] = models.AssetCategory.objects.all()
|
||||
|
||||
context["statuses"] = models.AssetStatus.objects.all()
|
||||
context["page_title"] = "Asset List"
|
||||
return context
|
||||
|
||||
|
||||
@@ -93,27 +101,39 @@ class AssetIDUrlMixin:
|
||||
|
||||
class AssetDetail(LoginRequiredMixin, AssetIDUrlMixin, generic.DetailView):
|
||||
model = models.Asset
|
||||
template_name = 'asset_update.html'
|
||||
template_name = 'asset_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_title"] = "Asset {}".format(self.object.display_id)
|
||||
return context
|
||||
|
||||
|
||||
class AssetEdit(LoginRequiredMixin, AssetIDUrlMixin, generic.UpdateView):
|
||||
template_name = 'asset_update.html'
|
||||
template_name = 'asset_form.html'
|
||||
model = models.Asset
|
||||
form_class = forms.AssetForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['edit'] = True
|
||||
context["edit"] = True
|
||||
context["connectors"] = models.Connector.objects.all()
|
||||
|
||||
context["page_title"] = "Edit Asset: {}".format(self.object.display_id)
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("asset_detail", kwargs={"pk": self.object.asset_id})
|
||||
if is_ajax(self.request):
|
||||
url = reverse_lazy('closemodal')
|
||||
update_url = str(reverse_lazy('asset_update', kwargs={'pk': self.object.pk}))
|
||||
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
|
||||
messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'")
|
||||
else:
|
||||
url = reverse_lazy('asset_detail', kwargs={'pk': self.object.asset_id, })
|
||||
return url
|
||||
|
||||
|
||||
class AssetCreate(LoginRequiredMixin, generic.CreateView):
|
||||
template_name = 'asset_create.html'
|
||||
template_name = 'asset_form.html'
|
||||
model = models.Asset
|
||||
form_class = forms.AssetForm
|
||||
|
||||
@@ -121,12 +141,12 @@ class AssetCreate(LoginRequiredMixin, generic.CreateView):
|
||||
context = super(AssetCreate, self).get_context_data(**kwargs)
|
||||
context["create"] = True
|
||||
context["connectors"] = models.Connector.objects.all()
|
||||
|
||||
context["page_title"] = "Create Asset"
|
||||
return context
|
||||
|
||||
def get_initial(self, *args, **kwargs):
|
||||
initial = super().get_initial(*args, **kwargs)
|
||||
initial["asset_id"] = models.Asset.get_available_asset_id()
|
||||
initial["asset_id"] = get_available_asset_id()
|
||||
return initial
|
||||
|
||||
def get_success_url(self):
|
||||
@@ -141,66 +161,71 @@ class DuplicateMixin:
|
||||
|
||||
|
||||
class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
|
||||
model = models.Asset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["create"] = None
|
||||
context["duplicate"] = True
|
||||
context['previous_asset_id'] = self.get_object().asset_id
|
||||
context["page_title"] = "Duplication of Asset: {}".format(context['previous_asset_id'])
|
||||
return context
|
||||
|
||||
|
||||
class AssetOembed(generic.View):
|
||||
model = models.Asset
|
||||
|
||||
def get(self, request, pk=None):
|
||||
embed_url = reverse('asset_embed', args=[pk])
|
||||
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
|
||||
|
||||
data = {
|
||||
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
|
||||
'version': '1.0',
|
||||
'type': 'rich',
|
||||
'height': '250'
|
||||
}
|
||||
|
||||
json = simplejson.JSONEncoderForHTML().encode(data)
|
||||
return HttpResponse(json, content_type="application/json")
|
||||
|
||||
|
||||
class AssetEmbed(AssetDetail):
|
||||
template_name = 'asset_embed.html'
|
||||
|
||||
|
||||
class SupplierList(generic.ListView):
|
||||
model = models.Supplier
|
||||
template_name = 'supplier_list.html'
|
||||
paginate_by = 40
|
||||
ordering = ['name']
|
||||
class AssetOEmbed(OEmbedView):
|
||||
model = models.Asset
|
||||
url_name = 'asset_embed'
|
||||
|
||||
|
||||
class AssetAuditList(AssetList):
|
||||
template_name = 'asset_audit_list.html'
|
||||
hide_hidden_status = False
|
||||
|
||||
# TODO Refresh this when the modal is submitted
|
||||
def get_queryset(self):
|
||||
if self.request.method == 'POST':
|
||||
self.form = forms.SupplierSearchForm(data=self.request.POST)
|
||||
elif self.request.method == 'GET':
|
||||
self.form = forms.SupplierSearchForm(data=self.request.GET)
|
||||
else:
|
||||
self.form = forms.SupplierSearchForm(data={})
|
||||
form = self.form
|
||||
if not form.is_valid():
|
||||
return self.model.objects.none()
|
||||
self.form = forms.AssetSearchForm(data=self.request.GET)
|
||||
return self.model.objects.filter(Q(last_audited_at__isnull=True)).select_related('category', 'status')
|
||||
|
||||
query_string = form.cleaned_data['query'] or ""
|
||||
if len(query_string) == 0:
|
||||
queryset = self.model.objects.all()
|
||||
else:
|
||||
queryset = self.model.objects.filter(Q(name__icontains=query_string))
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AssetAuditList, self).get_context_data(**kwargs)
|
||||
context['page_title'] = "Asset Audit List"
|
||||
return context
|
||||
|
||||
return queryset
|
||||
|
||||
class AssetAudit(AssetEdit):
|
||||
template_name = 'asset_audit.html'
|
||||
form_class = forms.AssetAuditForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_title"] = "Audit Asset: {}".format(self.object.display_id)
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
# TODO For some reason this doesn't stick when done in form_valid??
|
||||
asset = self.get_object()
|
||||
asset.last_audited_by = self.request.user
|
||||
asset.last_audited_at = timezone.now()
|
||||
asset.save()
|
||||
return super().get_success_url()
|
||||
|
||||
|
||||
class SupplierList(GenericListView):
|
||||
model = models.Supplier
|
||||
ordering = ['name']
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(SupplierList, self).get_context_data(**kwargs)
|
||||
context["form"] = self.form
|
||||
context['create'] = 'supplier_create'
|
||||
context['edit'] = 'supplier_update'
|
||||
context['can_edit'] = self.request.user.has_perm('assets.change_supplier')
|
||||
context['detail'] = 'supplier_detail'
|
||||
if is_ajax(self.request):
|
||||
context['override'] = "base_ajax.html"
|
||||
else:
|
||||
context['override'] = 'base_assets.html'
|
||||
return context
|
||||
|
||||
|
||||
@@ -215,40 +240,141 @@ class SupplierSearch(SupplierList):
|
||||
return JsonResponse(result, safe=False)
|
||||
|
||||
|
||||
class SupplierDetail(generic.DetailView):
|
||||
class SupplierDetail(GenericDetailView):
|
||||
model = models.Supplier
|
||||
template_name = 'supplier_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(SupplierDetail, self).get_context_data(**kwargs)
|
||||
context['history_link'] = 'supplier_history'
|
||||
context['update_link'] = 'supplier_update'
|
||||
context['detail_link'] = 'supplier_detail'
|
||||
context['associated'] = 'partials/associated_assets.html'
|
||||
context['associated2'] = ''
|
||||
if is_ajax(self.request):
|
||||
context['override'] = "base_ajax.html"
|
||||
else:
|
||||
context['override'] = 'base_assets.html'
|
||||
context['can_edit'] = self.request.user.has_perm('assets.change_supplier')
|
||||
return context
|
||||
|
||||
|
||||
class SupplierCreate(generic.CreateView):
|
||||
class SupplierCreate(GenericCreateView, ModalURLMixin):
|
||||
model = models.Supplier
|
||||
form_class = forms.SupplierForm
|
||||
template_name = 'supplier_update.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(SupplierCreate, self).get_context_data(**kwargs)
|
||||
if is_ajax(self.request):
|
||||
context['override'] = "base_ajax.html"
|
||||
else:
|
||||
context['override'] = 'base_assets.html'
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return self.get_close_url('supplier_update', 'supplier_detail')
|
||||
|
||||
|
||||
class SupplierUpdate(generic.UpdateView):
|
||||
class SupplierUpdate(GenericUpdateView, ModalURLMixin):
|
||||
model = models.Supplier
|
||||
form_class = forms.SupplierForm
|
||||
template_name = 'supplier_update.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(SupplierUpdate, self).get_context_data(**kwargs)
|
||||
if is_ajax(self.request):
|
||||
context['override'] = "base_ajax.html"
|
||||
else:
|
||||
context['override'] = 'base_assets.html'
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return self.get_close_url('supplier_update', 'supplier_detail')
|
||||
|
||||
|
||||
class SupplierVersionHistory(versioning.VersionHistory):
|
||||
template_name = "asset_version_history.html"
|
||||
|
||||
|
||||
class AssetVersionHistory(versioning.VersionHistory):
|
||||
template_name = "asset_version_history.html"
|
||||
|
||||
def get_object(self, **kwargs):
|
||||
return get_object_or_404(models.Asset, asset_id=self.kwargs['pk'])
|
||||
|
||||
|
||||
class ActivityTable(versioning.ActivityTable):
|
||||
model = versioning.RIGSVersion
|
||||
template_name = "asset_activity_table.html"
|
||||
paginate_by = 25
|
||||
class CableTypeList(generic.ListView):
|
||||
model = models.CableType
|
||||
template_name = 'cable_type_list.html'
|
||||
paginate_by = 40
|
||||
|
||||
def get_queryset(self):
|
||||
versions = versioning.RIGSVersion.objects.get_for_multiple_models(
|
||||
[models.Asset, models.Supplier])
|
||||
return versions
|
||||
return self.model.objects.select_related('plug', 'socket')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_title"] = "Cable Type List"
|
||||
return context
|
||||
|
||||
|
||||
class CableTypeDetail(generic.DetailView):
|
||||
model = models.CableType
|
||||
template_name = 'cable_type_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CableTypeDetail, self).get_context_data(**kwargs)
|
||||
context["page_title"] = "Cable Type {}".format(str(self.object))
|
||||
return context
|
||||
|
||||
|
||||
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
|
||||
context["page_title"] = "Create Cable Type"
|
||||
|
||||
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
|
||||
context["page_title"] = "Edit Cable Type"
|
||||
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
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)
|
||||
|
||||
asset_id = "Asset: {}".format(obj.asset_id)
|
||||
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)
|
||||
|
||||
draw.text((210, 140), asset_id, fill=black, font=font)
|
||||
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)
|
||||
|
||||
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))
|
||||
|
||||
response = HttpResponse(content_type="image/png")
|
||||
image.save(response, "PNG")
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user