From 1fbdf91eb55a9dfec357db73b27772d5469e88d0 Mon Sep 17 00:00:00 2001
From: Benjamin Bertrand <benjamin.bertrand@esss.se>
Date: Wed, 20 Dec 2017 15:52:48 +0100
Subject: [PATCH] Automatically assign temporary ics_id if not given

This allows to easily register items via the API without giving an ICS
id.
---
 app/api/inventory.py | 11 ++++++-----
 app/helpers.py       | 27 ---------------------------
 app/models.py        | 28 +++++++++++++++++++++++++++-
 3 files changed, 33 insertions(+), 33 deletions(-)

diff --git a/app/api/inventory.py b/app/api/inventory.py
index af34fa3..f9cf9a1 100644
--- a/app/api/inventory.py
+++ b/app/api/inventory.py
@@ -9,7 +9,7 @@ This module implements the inventory API.
 :license: BSD 2-Clause, see LICENSE for more details.
 
 """
-from flask import Blueprint, jsonify, request
+from flask import Blueprint, jsonify, request, current_app
 from flask_jwt_extended import jwt_required
 from .. import utils, models
 from ..decorators import jwt_groups_accepted
@@ -59,6 +59,7 @@ def create_item():
     # an item so ics_id should also be a mandatory field.
     # But there are existing items (in confluence and JIRA) that we want to
     # import and associate after they have been created.
+    # In that case a temporary id is automatically assigned.
     return create_generic_model(models.Item, mandatory_fields=('serial_number',))
 
 
@@ -70,7 +71,7 @@ def patch_item(id_):
 
     id_ can be the primary key or the ics_id field
     Fields allowed to update are:
-        - ics_id ONLY if currently null (422 returned otherwise)
+        - ics_id ONLY if current is temporary (422 returned otherwise)
         - manufacturer
         - model
         - location
@@ -89,9 +90,9 @@ def patch_item(id_):
                        'location', 'status', 'parent'):
             raise utils.CSEntryError(f"Invalid field '{key}'", status_code=422)
     item = get_item_by_id_or_ics_id(id_)
-    # Only allow to set ICS id if it's null
-    if item.ics_id is None:
-        item.ics_id = data.get('ics_id')
+    # Only allow to set ICS id if the current id is a temporary one
+    if item.ics_id.startswith(current_app.config['TEMPORARY_ICS_ID']):
+        item.ics_id = data.get('ics_id', item.ics_id)
     elif 'ics_id' in data:
         raise utils.CSEntryError("'ics_id' can't be changed", status_code=422)
     item.manufacturer = utils.convert_to_model(data.get('manufacturer', item.manufacturer), models.Manufacturer)
diff --git a/app/helpers.py b/app/helpers.py
index 3c4b20c..92db026 100644
--- a/app/helpers.py
+++ b/app/helpers.py
@@ -9,10 +9,7 @@ This module implements helpers functions for the models.
 :license: BSD 2-Clause, see LICENSE for more details.
 
 """
-import string
-from flask import current_app
 from flask_wtf import FlaskForm
-from . import models
 
 
 class CSEntryForm(FlaskForm):
@@ -28,27 +25,3 @@ class CSEntryForm(FlaskForm):
             super().__init__(obj=obj, **kwargs)
         else:
             super().__init__(formdata=formdata, obj=obj, **kwargs)
-
-
-def temporary_ics_ids():
-    """Generator that returns the full list of temporary ICS ids"""
-    return (f'{current_app.config["TEMPORARY_ICS_ID"]}{letter}{number:0=3d}'
-            for letter in string.ascii_uppercase
-            for number in range(0, 1000))
-
-
-def used_temporary_ics_ids():
-    """Generator that returns the list of temporary ICS ids used"""
-    temporary_items = models.Item.query.filter(
-        models.Item.ics_id.startswith(
-            current_app.config['TEMPORARY_ICS_ID'])).all()
-    return (item.ics_id for item in temporary_items)
-
-
-def get_temporary_ics_id():
-    """Return a temporary ICS id that is available"""
-    for ics_id in temporary_ics_ids():
-        if ics_id not in used_temporary_ics_ids():
-            return ics_id
-    else:
-        raise ValueError('No temporary ICS id available')
diff --git a/app/models.py b/app/models.py
index 39dc16f..023c8b7 100644
--- a/app/models.py
+++ b/app/models.py
@@ -10,6 +10,7 @@ This module implements the models used in the app.
 
 """
 import ipaddress
+import string
 import qrcode
 import sqlalchemy as sa
 from sqlalchemy.ext.declarative import declared_attr
@@ -40,6 +41,30 @@ def pg_utcnow(element, compiler, **kw):
         return "TIMEZONE('utc', CURRENT_TIMESTAMP)"
 
 
+def temporary_ics_ids():
+    """Generator that returns the full list of temporary ICS ids"""
+    return (f'{current_app.config["TEMPORARY_ICS_ID"]}{letter}{number:0=3d}'
+            for letter in string.ascii_uppercase
+            for number in range(0, 1000))
+
+
+def used_temporary_ics_ids():
+    """Generator that returns the list of temporary ICS ids used"""
+    temporary_items = Item.query.filter(
+        Item.ics_id.startswith(
+            current_app.config['TEMPORARY_ICS_ID'])).all()
+    return (item.ics_id for item in temporary_items)
+
+
+def get_temporary_ics_id():
+    """Return a temporary ICS id that is available"""
+    for ics_id in temporary_ics_ids():
+        if ics_id not in used_temporary_ics_ids():
+            return ics_id
+    else:
+        raise ValueError('No temporary ICS id available')
+
+
 @login_manager.user_loader
 def load_user(user_id):
     """User loader callback for flask-login
@@ -247,7 +272,8 @@ class Item(CreatedMixin, db.Model):
     # WARNING! Inheriting id from CreatedMixin doesn't play well with
     # SQLAlchemy-Continuum. It has to be defined here.
     id = db.Column(db.Integer, primary_key=True)
-    ics_id = db.Column(db.Text, unique=True, nullable=False, index=True)
+    ics_id = db.Column(db.Text, unique=True, nullable=False,
+                       index=True, default=get_temporary_ics_id)
     serial_number = db.Column(db.Text, nullable=False)
     manufacturer_id = db.Column(db.Integer, db.ForeignKey('manufacturer.id'))
     model_id = db.Column(db.Integer, db.ForeignKey('model.id'))
-- 
GitLab