From fe73933da766f54be2c0dc1978a4e2cd236d3c77 Mon Sep 17 00:00:00 2001 From: Benjamin Bertrand <benjamin.bertrand@esss.se> Date: Thu, 24 Aug 2017 16:00:38 +0200 Subject: [PATCH] Replace hash with specific ICS Id (3 letters + 3 digits) The serial number might not be unique and can't be used as the string to hash. There are for example SD cards that all have the same SN. Using an ICS id allows to pre-print labels that can be assigned to hardware when scanning serial numbers. This id shall be easy to remember (unlike UUID). --- app/admin/views.py | 18 +++++++-------- app/models.py | 56 +++++++++++++--------------------------------- app/utils.py | 9 -------- 3 files changed, 24 insertions(+), 59 deletions(-) diff --git a/app/admin/views.py b/app/admin/views.py index b6e8f3a..4578bda 100644 --- a/app/admin/views.py +++ b/app/admin/views.py @@ -9,10 +9,10 @@ This module customizes the admin views. :license: BSD 2-Clause, see LICENSE for more details. """ +from wtforms import validators from flask_admin.contrib import sqla from flask_login import current_user -from ..models import Item, User, Group -from .. import utils +from ..models import Item, User, Group, ICS_ID_RE class AdminModelView(sqla.ModelView): @@ -41,12 +41,12 @@ class UserAdmin(AdminModelView): class ItemAdmin(AdminModelView): + form_args = { + 'ics_id': { + 'label': 'ICS id', + 'validators': [validators.Regexp(ICS_ID_RE, message='ICS id shall match [A-Z]{3}[0-9]{3}')] + } + } + def __init__(self, session): super().__init__(Item, session) - - def on_model_change(self, form, model, is_created): - """Update the hash""" - # The hash is supposed to be computed in the __init__ method - # of the Item class but flask-admin doesn't pass any parameter - # when creating a class - we do it here instead - model.hash = utils.compute_hash(model.serial_number) diff --git a/app/models.py b/app/models.py index 066f554..e988370 100644 --- a/app/models.py +++ b/app/models.py @@ -9,10 +9,9 @@ This module implements the models used in the app. :license: BSD 2-Clause, see LICENSE for more details. """ -import uuid +import re import qrcode -from sqlalchemy.types import TypeDecorator, CHAR -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import validates from sqlalchemy.ext.associationproxy import association_proxy from citext import CIText from flask import current_app @@ -21,39 +20,7 @@ from .extensions import db, login_manager, ldap_manager, jwt from . import utils -class GUID(TypeDecorator): - """Platform-independent GUID type. - - Uses Postgresql's UUID type, otherwise uses - CHAR(32), storing as stringified hex values. - - From http://docs.sqlalchemy.org/en/rel_0_9/core/custom_types.html?highlight=guid#backend-agnostic-guid-type - """ - impl = CHAR - - def load_dialect_impl(self, dialect): - if dialect.name == 'postgresql': - return dialect.type_descriptor(UUID()) - else: - return dialect.type_descriptor(CHAR(32)) - - def process_bind_param(self, value, dialect): - if value is None: - return value - elif dialect.name == 'postgresql': - return str(value) - else: - if not isinstance(value, uuid.UUID): - return "%.32x" % uuid.UUID(value).int - else: - # hexstring - return "%.32x" % value.int - - def process_result_value(self, value, dialect): - if value is None: - return value - else: - return uuid.UUID(value) +ICS_ID_RE = re.compile('[A-Z]{3}[0-9]{3}') @login_manager.user_loader @@ -219,8 +186,8 @@ class Item(db.Model): id = db.Column(db.Integer, primary_key=True) _created = db.Column(db.DateTime, default=db.func.now()) _updated = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now()) + ics_id = db.Column(db.String(6), unique=True, index=True) serial_number = db.Column(db.String(100), nullable=False) - hash = db.Column(GUID, unique=True) manufacturer_id = db.Column(db.Integer, db.ForeignKey('manufacturer.id')) model_id = db.Column(db.Integer, db.ForeignKey('model.id')) location_id = db.Column(db.Integer, db.ForeignKey('location.id')) @@ -233,23 +200,30 @@ class Item(db.Model): status = db.relationship('Status', back_populates='items') children = db.relationship('Item', backref=db.backref('parent', remote_side=[id])) - def __init__(self, serial_number=None, manufacturer=None, model=None, location=None, status=None): + def __init__(self, ics_id=None, serial_number=None, manufacturer=None, model=None, location=None, status=None): # All arguments must be optional for this class to work with flask-admin! + self.ics_id = ics_id self.serial_number = serial_number self.manufacturer = utils.convert_to_model(manufacturer, Manufacturer) self.model = utils.convert_to_model(model, Model) self.location = utils.convert_to_model(location, Location) self.status = utils.convert_to_model(status, Status) - self.hash = utils.compute_hash(serial_number) def __str__(self): - return self.serial_number + return str(self.ics_id) + + @validates('ics_id') + def validate_ics_id(self, key, string): + """Ensure the ICS id field matches the required format""" + if string is not None: + assert ICS_ID_RE.fullmatch(string) is not None + return string def to_dict(self): return { 'id': self.id, + 'ics_id': self.ics_id, 'serial_number': self.serial_number, - 'hash': self.hash, 'manufacturer': utils.format_field(self.manufacturer), 'model': utils.format_field(self.model), 'location': utils.format_field(self.location), diff --git a/app/utils.py b/app/utils.py index 03f8a88..1ea13fa 100644 --- a/app/utils.py +++ b/app/utils.py @@ -12,8 +12,6 @@ This module implements utility functions. import base64 import datetime import io -import hashlib -import uuid class InventoryError(Exception): @@ -39,13 +37,6 @@ class InventoryError(Exception): return str(self.to_dict()) -def compute_hash(string): - if string is None: - return None - md5 = hashlib.md5(string.encode()).hexdigest() - return uuid.UUID(md5) - - def image_to_base64(img, format='PNG'): """Convert a Pillow image to a base64 string -- GitLab