diff --git a/app/admin/views.py b/app/admin/views.py index b6e8f3a251a5933bf07c2903909b6b92ecb8fe01..4578bdaf22b24f2023caae8e68a5aed9d62880e4 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 066f55436bf0d08550fd6276bfcd1dda3c2542d9..e98837032255aff8640df552f5a3ac8ee475634e 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 03f8a885ab721a4f2b48c5720d3ca6ffcd4c595e..1ea13faa0fbad4e49cbeb8290779cd24df97cdc8 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