From 5ef4bfe582b4cb7ccfc7b23a30aaded8090e1e17 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 28 Mar 2026 22:46:25 -0700 Subject: [PATCH] Validate model version during model load --- API/Classes/Base/ModelVersionClass.py | 79 +++++++++++++++++++++++++++ API/Classes/Case/ImportTemplate.py | 2 + API/Classes/Case/OsemosysClass.py | 2 + API/Routes/Case/CaseRoute.py | 2 + API/Routes/Upload/UploadRoute.py | 11 +++- API/app.py | 11 ++-- WebAPP/App/Controller/AddCase.js | 2 +- WebAPP/App/Controller/Home.js | 7 +-- WebAPP/App/Controller/LegacyImport.js | 3 +- WebAPP/Classes/Base.Class.js | 42 ++++++-------- WebAPP/Classes/Osemosys.Class.js | 75 ++++++++++--------------- 11 files changed, 154 insertions(+), 82 deletions(-) create mode 100644 API/Classes/Base/ModelVersionClass.py diff --git a/API/Classes/Base/ModelVersionClass.py b/API/Classes/Base/ModelVersionClass.py new file mode 100644 index 000000000..cf8120bb4 --- /dev/null +++ b/API/Classes/Base/ModelVersionClass.py @@ -0,0 +1,79 @@ +import logging + +from Classes.Base.CustomExceptionClass import CustomException + +CURRENT_MODEL_VERSION = "5.0" + + +class ModelVersionError(CustomException): + def __init__(self, message, case=None, detected_version=None, expected_version=None): + payload = { + "status_code": "version_mismatch", + "case": case, + "detected_version": detected_version, + "expected_version": expected_version or CURRENT_MODEL_VERSION, + } + CustomException.__init__(self, message, status_code=409, payload=payload) + + +def get_model_version(genData): + if not isinstance(genData, dict): + return None + + version = genData.get("modelVersion") + if version is None: + version = genData.get("osy-version") + + if version is None: + return None + + version = str(version).strip() + return version or None + + +def stamp_model_version(genData, version=CURRENT_MODEL_VERSION): + if genData is None: + return genData + + version = str(version).strip() or CURRENT_MODEL_VERSION + genData["modelVersion"] = version + genData["osy-version"] = version + return genData + + +def validate_model_version(genData, case=None): + detected_version = get_model_version(genData) + + if detected_version == CURRENT_MODEL_VERSION: + return detected_version + + if case: + case_label = f"Model {case}" + else: + case_label = "Selected model" + + if detected_version is None: + message = ( + f"{case_label} is missing schema version metadata. " + f"Open the model configuration page and click Update model " + f"before generating data or running the solver." + ) + else: + message = ( + f"{case_label} uses schema version {detected_version}, but the current backend " + f"expects {CURRENT_MODEL_VERSION}. Open the model configuration page and click " + f"Update model before generating data or running the solver." + ) + + logging.warning( + "Model version mismatch for case '%s': detected=%s expected=%s", + case, + detected_version, + CURRENT_MODEL_VERSION, + ) + raise ModelVersionError( + message, + case=case, + detected_version=detected_version, + expected_version=CURRENT_MODEL_VERSION, + ) diff --git a/API/Classes/Case/ImportTemplate.py b/API/Classes/Case/ImportTemplate.py index 3ae0033e5..b6d1f7b23 100644 --- a/API/Classes/Case/ImportTemplate.py +++ b/API/Classes/Case/ImportTemplate.py @@ -5,6 +5,7 @@ from Classes.Base import Config from Classes.Case.CaseClass import Case from Classes.Base.FileClass import File +from Classes.Base.ModelVersionClass import stamp_model_version class ImportTemplate(): def __init__(self,template): @@ -814,6 +815,7 @@ def importProcess(self, data): genData["osy-scenarios"] = self.defaultScenario(True) genData["osy-constraints"] = [] genData["osy-years"] = yearsArray + stamp_model_version(genData, version) casename = genData['osy-casename'] diff --git a/API/Classes/Case/OsemosysClass.py b/API/Classes/Case/OsemosysClass.py index 5ef3a5e53..d9e5e13ce 100644 --- a/API/Classes/Case/OsemosysClass.py +++ b/API/Classes/Case/OsemosysClass.py @@ -4,6 +4,7 @@ import shutil from Classes.Base import Config from Classes.Base.FileClass import File +from Classes.Base.ModelVersionClass import validate_model_version class Osemosys(): def __init__(self, case): @@ -11,6 +12,7 @@ def __init__(self, case): self.PARAMETERS = File.readParamFile(Path(Config.DATA_STORAGE, 'Parameters.json')) self.VARIABLES = File.readParamFile(Path(Config.DATA_STORAGE, 'Variables.json')) self.genData = File.readFile(Path(Config.DATA_STORAGE,case,'genData.json')) + validate_model_version(self.genData, case) self.resData = File.readFile( Path(Config.DATA_STORAGE, case,'view', 'resData.json')) #Case.__init__(self, case) diff --git a/API/Routes/Case/CaseRoute.py b/API/Routes/Case/CaseRoute.py index d95482f41..3e05f4853 100644 --- a/API/Routes/Case/CaseRoute.py +++ b/API/Routes/Case/CaseRoute.py @@ -5,6 +5,7 @@ import pandas as pd from Classes.Base import Config from Classes.Base.FileClass import File +from Classes.Base.ModelVersionClass import stamp_model_version from Classes.Case.CaseClass import Case from Classes.Case.UpdateCaseClass import UpdateCase from Classes.Case.ImportTemplate import ImportTemplate @@ -248,6 +249,7 @@ def updateData(): def saveCase(): try: genData = request.json['data'] + stamp_model_version(genData) casename = genData['osy-casename'] case = session.get('osycase', None) diff --git a/API/Routes/Upload/UploadRoute.py b/API/Routes/Upload/UploadRoute.py index 3aef38b26..ed6d885ce 100644 --- a/API/Routes/Upload/UploadRoute.py +++ b/API/Routes/Upload/UploadRoute.py @@ -9,6 +9,7 @@ from Classes.Base import Config from Classes.Base.FileClass import File +from Classes.Base.ModelVersionClass import stamp_model_version upload_api = Blueprint('UploadRoute', __name__) @@ -138,6 +139,12 @@ def updateStorageSet(casename): File.writeFile( genData, genDataPath) +def stampCurrentModelVersion(casename): + genDataPath = Path(Config.DATA_STORAGE, casename, 'genData.json') + genData = File.readParamFile(genDataPath) + stamp_model_version(genData) + File.writeFile(genData, genDataPath) + def updateViewDefintions(casename): viewDataPath = Path(Config.DATA_STORAGE,casename,'view','viewDefinitions.json') viewDefExisting = File.readParamFile(viewDataPath) @@ -371,6 +378,7 @@ def uploadCaseUnchunked_old(): elif name == '5.0': zf.extractall(os.path.join(Config.EXTRACT_FOLDER)) updateViewDefintions(casename) + stampCurrentModelVersion(casename) msg.append({ "message": "Model " + casename +" have been uploaded!", "status_code": "success", @@ -511,6 +519,7 @@ def handle_full_zip(file, filepath=None): elif name == '5.0': zf.extractall(os.path.join(Config.EXTRACT_FOLDER)) updateViewDefintions(casename) + stampCurrentModelVersion(casename) msg.append({ "message": "Model " + casename +" have been uploaded!", "status_code": "success", @@ -652,4 +661,4 @@ def uploadXls(): except(IOError): raise IOError except OSError: - raise OSError \ No newline at end of file + raise OSError diff --git a/API/app.py b/API/app.py index 458aad034..bc70e8888 100644 --- a/API/app.py +++ b/API/app.py @@ -27,6 +27,7 @@ #import json from Classes.Base import Config +from Classes.Base.CustomExceptionClass import CustomException # from API.Classes.Base.SyncS3 import SyncS3 from Routes.Upload.UploadRoute import upload_api from Routes.Case.CaseRoute import case_api @@ -97,11 +98,11 @@ def add_headers(response): #response.headers['Content-Type'] = 'application/javascript' return response -# @app.errorhandler(CustomException) -# def handle_invalid_usage(error): -# response = jsonify(error.to_dict()) -# response.status_code = error.status_code -# return response +@app.errorhandler(CustomException) +def handle_invalid_usage(error): + response = jsonify(error.to_dict()) + response.status_code = error.status_code + return response #entry point to frontend @app.route("/", methods=['GET']) diff --git a/WebAPP/App/Controller/AddCase.js b/WebAPP/App/Controller/AddCase.js index e3e542e1c..6d2f998ea 100644 --- a/WebAPP/App/Controller/AddCase.js +++ b/WebAPP/App/Controller/AddCase.js @@ -229,6 +229,7 @@ export default class AddCase { }); let POSTDATA = { + "modelVersion": "5.0", "osy-version": "5.0", "osy-casename": casename, "osy-desc": desc, @@ -1144,4 +1145,3 @@ export default class AddCase { - diff --git a/WebAPP/App/Controller/Home.js b/WebAPP/App/Controller/Home.js index d3448eea5..5c300a8e0 100644 --- a/WebAPP/App/Controller/Home.js +++ b/WebAPP/App/Controller/Home.js @@ -85,12 +85,11 @@ export default class Home { //Sidebar.Load(casename, model.genData, model.PARAMETERS); Osemosys.getData(casename, 'genData.json') .then(genData => { - //console.log('genData ', genData["osy-version"]) + let modelVersion = parseFloat(genData.modelVersion || genData["osy-version"] || 0); Home.refreshPage(casename); Message.smallBoxInfo("Case selection", casename + " is selected!", 3000); - if(parseFloat(genData["osy-version"]) < 4.5){ - //console.log('manje od 4.5') - Message.bigBoxWarning("Warning", "You have selected a model created in a earlier version of this UI. In order to update to the current version click Update model on the configuration page.", 10000); + if(modelVersion < 5.0){ + Message.bigBoxWarning("Warning", "You have selected a model created with an older schema version. Open Model configuration and click Update model before generating data or running the solver.", 10000); } }) }); diff --git a/WebAPP/App/Controller/LegacyImport.js b/WebAPP/App/Controller/LegacyImport.js index bceb3179e..2e5e1e1b9 100644 --- a/WebAPP/App/Controller/LegacyImport.js +++ b/WebAPP/App/Controller/LegacyImport.js @@ -117,6 +117,7 @@ export default class LegacyImport { } let POSTDATA = { + "modelVersion": "5.0", "osy-version": "5.0", "osy-casename": casename, "osy-desc": desc, @@ -154,4 +155,4 @@ export default class LegacyImport { $('#definition').toggle('slow'); }); } -} \ No newline at end of file +} diff --git a/WebAPP/Classes/Base.Class.js b/WebAPP/Classes/Base.Class.js index 40befa648..3cae1027e 100644 --- a/WebAPP/Classes/Base.Class.js +++ b/WebAPP/Classes/Base.Class.js @@ -2,6 +2,13 @@ import { Message } from "./Message.Class.js"; import { Html } from "./Html.Class.js"; import { SyncS3 } from "./SyncS3.Class.js"; +function getApiError(xhr, error) { + if (xhr && xhr.responseJSON && xhr.responseJSON.message) { + return xhr.responseJSON.message; + } + return error; +} + export class Base { static HEROKU = 0; static AWS_SYNC = 0; @@ -29,8 +36,7 @@ export class Base { resolve(result); }, error: function (xhr, status, error) { - if (error == 'UNKNOWN') { error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -50,8 +56,7 @@ export class Base { resolve(result); }, error: function (xhr, status, error) { - if (error == 'UNKNOWN') { error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -73,8 +78,7 @@ export class Base { resolve(result); }, error: function (xhr, status, error) { - if (error == 'UNKNOWN') { error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -93,9 +97,7 @@ export class Base { resolve(result); }, error: function (xhr, status, error) { - if (error == 'UNKNOWN') { error = xhr.responseJSON.message } - - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -114,8 +116,7 @@ export class Base { resolve(result); }, error: function (xhr, status, error) { - if (error == 'UNKNOWN') { error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -134,8 +135,7 @@ export class Base { resolve(result); }, error: function (xhr, status, error) { - if (error == 'UNKNOWN') { error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -154,9 +154,7 @@ export class Base { resolve(result); }, error: function (xhr, status, error) { - //custom exception - if (xhr.responseJSON && xhr.responseJSON.message) { error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -175,9 +173,7 @@ export class Base { resolve(result); }, error: function (xhr, status, error) { - //custom exception - if (xhr.responseJSON && xhr.responseJSON.message) { error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -196,9 +192,7 @@ export class Base { resolve(result); }, error: function (xhr, status, error) { - //custom exception - if (error == 'UNKNOWN') { error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -219,9 +213,7 @@ export class Base { resolve(result); }, error: function(xhr, status, error) { - //custom exception - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); diff --git a/WebAPP/Classes/Osemosys.Class.js b/WebAPP/Classes/Osemosys.Class.js index 602802e44..ffb5b0651 100644 --- a/WebAPP/Classes/Osemosys.Class.js +++ b/WebAPP/Classes/Osemosys.Class.js @@ -1,5 +1,12 @@ import { Base } from "./Base.Class.js"; +function getApiError(xhr, error) { + if (xhr && xhr.responseJSON && xhr.responseJSON.message) { + return xhr.responseJSON.message; + } + return error; +} + export class Osemosys { static getParamFile(dataJson='Parameters.json') { @@ -16,8 +23,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -37,8 +43,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -57,8 +62,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -80,8 +84,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -100,8 +103,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -120,8 +122,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -143,8 +144,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -166,8 +166,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -186,8 +185,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -209,8 +207,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -233,8 +230,7 @@ export class Osemosys { }, error: function(xhr, status, error) { console.log("xhr, status, error ", xhr, status, error ) - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -257,8 +253,7 @@ export class Osemosys { }, error: function(xhr, status, error) { console.log("xhr, status, error ", xhr, status, error ) - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -281,8 +276,7 @@ export class Osemosys { }, error: function(xhr, status, error) { console.log("xhr, status, error ", xhr, status, error ) - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -304,8 +298,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -327,8 +320,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -347,8 +339,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -466,8 +457,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -486,8 +476,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -521,8 +510,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -542,8 +530,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -563,8 +550,7 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); @@ -583,10 +569,9 @@ export class Osemosys { resolve(result); }, error: function(xhr, status, error) { - if(error == 'UNKNOWN'){ error = xhr.responseJSON.message } - reject(error); + reject(getApiError(xhr, error)); } }); }); } -} \ No newline at end of file +}