From b77615b9b940bee255d6acdd309efec7ba20c7e5 Mon Sep 17 00:00:00 2001 From: Matthew Smith Date: Fri, 6 Dec 2019 00:28:54 +0000 Subject: [PATCH] Fix handling of prefixed Asset IDs and sorting of the asset list (#382) * FIX: Remove misleading admin site title * Moved across assets_id sorting to use proper numeric values. Also mofifies SQL command to find free asset IDs so that it works on postgres. * Changed generateSampleAssetsData.py to include prefices on some cables. * Fixed pep8 * Fixed missed migration * Ensured hidden asset fields are completed on every database write * CMULTI is a thing, and therefore a max prefix length of 5 cannot be a thing --- assets/admin.py | 5 -- assets/forms.py | 1 + .../commands/generateSampleAssetsData.py | 15 +++-- ...5_1937_squashed_0009_auto_20191205_2041.py | 64 +++++++++++++++++++ assets/models.py | 41 +++++++++--- 5 files changed, 106 insertions(+), 20 deletions(-) create mode 100644 assets/migrations/0008_auto_20191205_1937_squashed_0009_auto_20191205_2041.py diff --git a/assets/admin.py b/assets/admin.py index 7044040f..3e6c9d58 100644 --- a/assets/admin.py +++ b/assets/admin.py @@ -30,8 +30,3 @@ class AssetAdmin(admin.ModelAdmin): @admin.register(assets.Connector) class ConnectorAdmin(admin.ModelAdmin): list_display = ['id', '__str__', 'current_rating', 'voltage_rating', 'num_pins'] - - -admin.AdminSite.site_header = 'PyAssets - TEC\'s Asset System' -admin.AdminSite.site_title = 'PyAssets Admin' -admin.AdminSite.index_title = 'System Administration' diff --git a/assets/forms.py b/assets/forms.py index 430eef20..7571d28b 100644 --- a/assets/forms.py +++ b/assets/forms.py @@ -7,6 +7,7 @@ class AssetForm(forms.ModelForm): class Meta: model = models.Asset fields = '__all__' + exclude = ['asset_id_prefix', 'asset_id_number'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/assets/management/commands/generateSampleAssetsData.py b/assets/management/commands/generateSampleAssetsData.py index c41de7fa..258551c2 100644 --- a/assets/management/commands/generateSampleAssetsData.py +++ b/assets/management/commands/generateSampleAssetsData.py @@ -1,8 +1,7 @@ +import random from django.core.management.base import BaseCommand, CommandError from django.utils import timezone - from assets import models -import random class Command(BaseCommand): @@ -50,7 +49,7 @@ class Command(BaseCommand): suppliers = models.Supplier.objects.all() for i in range(100): - asset = models.Asset.objects.create( + asset = models.Asset( asset_id='{}'.format(models.Asset.get_available_asset_id()), description=random.choice(asset_description), category=random.choice(categories), @@ -63,11 +62,12 @@ class Command(BaseCommand): if i % 3 == 0: asset.purchased_from = random.choice(suppliers) - + asset.clean() asset.save() 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] @@ -79,7 +79,7 @@ class Command(BaseCommand): connectors = models.Connector.objects.all() for i in range(100): - asset = models.Asset.objects.create( + asset = models.Asset( asset_id='{}'.format(models.Asset.get_available_asset_id()), description=random.choice(asset_description), category=random.choice(categories), @@ -95,12 +95,17 @@ class Command(BaseCommand): 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() def create_connectors(self): diff --git a/assets/migrations/0008_auto_20191205_1937_squashed_0009_auto_20191205_2041.py b/assets/migrations/0008_auto_20191205_1937_squashed_0009_auto_20191205_2041.py new file mode 100644 index 00000000..15898dc8 --- /dev/null +++ b/assets/migrations/0008_auto_20191205_1937_squashed_0009_auto_20191205_2041.py @@ -0,0 +1,64 @@ +# Generated by Django 2.0.13 on 2019-12-05 20:42 + +from django.db import migrations, models +import django.db.migrations.operations.special + +def forwards(apps, schema_editor): + AssetModel = apps.get_model('assets', 'Asset') + + for row in AssetModel.objects.all(): + + row.asset_id = row.asset_id.upper() + asset_search = re.search("^([A-Z0-9]*?[A-Z]?)([0-9]+)$", row.asset_id) + if asset_search is None: # If the asset_id doesn't have a number at the end + row.asset_id += "1" + + asset_search = re.search("^([A-Z0-9]*?[A-Z]?)([0-9]+)$", row.asset_id) + row.asset_id_prefix = asset_search.group(1) + row.asset_id_number = int(asset_search.group(2)) + + row.save(update_fields=['asset_id', 'asset_id_prefix', 'asset_id_number']) + +# Functions from the following migrations need manual copying. +# Move them and any dependencies into this file, then update the +# RunPython operations to refer to the local versions: +# assets.migrations.0008_auto_20191205_1937 + +class Migration(migrations.Migration): + + replaces = [('assets', '0008_auto_20191205_1937'), ('assets', '0009_auto_20191205_2041')] + + dependencies = [ + ('assets', '0007_auto_20190108_0202_squashed_0014_auto_20191017_2052'), + ] + + operations = [ + migrations.AddField( + model_name='asset', + name='asset_id_number', + field=models.IntegerField(default=1), + ), + migrations.AddField( + model_name='asset', + name='asset_id_prefix', + field=models.CharField(default='', max_length=5), + ), + migrations.RunPython( + code=forwards, + reverse_code=django.db.migrations.operations.special.RunPython.noop, + ), + migrations.AlterModelOptions( + name='asset', + options={'ordering': ['asset_id_prefix', 'asset_id_number'], 'permissions': (('asset_finance', 'Can see financial data for assets'), ('view_asset', 'Can view an asset'))}, + ), + migrations.AlterField( + model_name='asset', + name='asset_id', + field=models.CharField(max_length=15, unique=True), + ), + migrations.AlterField( + model_name='asset', + name='asset_id_prefix', + field=models.CharField(default='', max_length=8), + ), + ] diff --git a/assets/models.py b/assets/models.py index 7abddfd2..993c8c34 100644 --- a/assets/models.py +++ b/assets/models.py @@ -3,6 +3,9 @@ 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 + class AssetCategory(models.Model): class Meta: @@ -54,14 +57,14 @@ class Connector(models.Model): class Asset(models.Model): class Meta: - ordering = ['asset_id'] + 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=10, unique=True) + asset_id = models.CharField(max_length=15, unique=True) description = models.CharField(max_length=120) category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE) status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE) @@ -83,18 +86,24 @@ class Asset(models.Model): circuits = models.IntegerField(blank=True, null=True) cores = models.IntegerField(blank=True, null=True) - def get_available_asset_id(): + # 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 MIN(CAST(a.asset_id AS int))+1 + SELECT a.asset_id_number+1 FROM assets_asset a LEFT OUTER JOIN assets_asset b ON - (CAST(a.asset_id AS int) + 1 = CAST(b.asset_id AS int)) - WHERE b.asset_id IS NULL AND CAST(a.asset_id AS int) >= %s; + (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]) + cursor.execute(sql, [9000, wanted_prefix]) row = cursor.fetchone() - if row[0] is None: + if row is None or row[0] is None: return 9000 else: return row[0] @@ -114,8 +123,9 @@ class Asset(models.Model): errdict["date_sold"] = ["Cannot sell an item before it is acquired"] self.asset_id = self.asset_id.upper() - if re.search("^[a-zA-Z0-9]+$", self.asset_id) is None: - errdict["asset_id"] = ["An Asset ID can only consist of letters and numbers"] + asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", self.asset_id) + if asset_search is None: + errdict["asset_id"] = ["An Asset ID can only consist of letters and numbers, with a final number"] if self.purchase_price and self.purchase_price < 0: errdict["purchase_price"] = ["A price cannot be negative"] @@ -139,3 +149,14 @@ class Asset(models.Model): if errdict != {}: # If there was an error when validation raise ValidationError(errdict) + + +@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))