From 5fbb8e93f24033044b07a3eeeabeb2a6f58dc2f1 Mon Sep 17 00:00:00 2001 From: AwKjay Date: Sun, 15 Mar 2026 07:26:02 +0530 Subject: [PATCH 1/3] fix(backupCase): store relative paths in ZIP archive --- API/Routes/Upload/UploadRoute.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/API/Routes/Upload/UploadRoute.py b/API/Routes/Upload/UploadRoute.py index 3aef38b26..d0f42befe 100644 --- a/API/Routes/Upload/UploadRoute.py +++ b/API/Routes/Upload/UploadRoute.py @@ -213,10 +213,10 @@ def backupCase(): for filename in filenames: if filename != 'lp.lp': - #create complete filepath of file in directory filePath = os.path.join(folderName, filename) - # Add file to zip - zipObj.write(filePath) + # store relative path so ZIP is portable across machines + arcname = os.path.join(case, os.path.relpath(filePath, str(casePath))) + zipObj.write(filePath, arcname=arcname) #osemosys 2.1 backup only input files # for filename in os.listdir(str(casePath)): From b9d92101bb1128dd5fd841851805b3f1e32a4986 Mon Sep 17 00:00:00 2001 From: AwKjay Date: Sun, 15 Mar 2026 11:20:49 +0530 Subject: [PATCH 2/3] fix: use PurePosixPath for cross-platform ZIP entry names --- API/Routes/Upload/UploadRoute.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/API/Routes/Upload/UploadRoute.py b/API/Routes/Upload/UploadRoute.py index d0f42befe..bdac19587 100644 --- a/API/Routes/Upload/UploadRoute.py +++ b/API/Routes/Upload/UploadRoute.py @@ -1,7 +1,7 @@ import shutil from flask import Blueprint, request, jsonify, send_file, after_this_request from zipfile import ZipFile -from pathlib import Path +from pathlib import Path, PurePosixPath from werkzeug.utils import secure_filename import os, time, json, glob @@ -214,8 +214,10 @@ def backupCase(): for filename in filenames: if filename != 'lp.lp': filePath = os.path.join(folderName, filename) - # store relative path so ZIP is portable across machines - arcname = os.path.join(case, os.path.relpath(filePath, str(casePath))) + # PurePosixPath enforces forward-slash separators in the ZIP + # regardless of host OS, required by the ZIP spec and ensures + # Windows-created backups restore correctly on Linux/macOS. + arcname = str(PurePosixPath(case) / os.path.relpath(filePath, str(casePath)).replace('\\', '/')) zipObj.write(filePath, arcname=arcname) #osemosys 2.1 backup only input files From 2d3df2d1e104fad1d9a1271444fc51f0e5e9e4b0 Mon Sep 17 00:00:00 2001 From: SeaCelo Date: Fri, 8 May 2026 21:34:17 +0300 Subject: [PATCH 3/3] port v5.6 HelpersClass, Modals, and stub Indicators/Duals JSONs (#458) --- .gitignore | 2 + API/Classes/Case/HelpersClass.py | 195 +++++++++++++++++++++++++++++ WebAPP/App/View/Modals.html | 24 ++++ WebAPP/DataStorage/Duals.json | 1 + WebAPP/DataStorage/Indicators.json | 1 + 5 files changed, 223 insertions(+) create mode 100644 API/Classes/Case/HelpersClass.py create mode 100644 WebAPP/App/View/Modals.html create mode 100644 WebAPP/DataStorage/Duals.json create mode 100644 WebAPP/DataStorage/Indicators.json diff --git a/.gitignore b/.gitignore index 4a7057074..588e1c262 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,8 @@ WebAPP/SOLVERs/_COIN-OR WebAPP/DataStorage/* !WebAPP/DataStorage/Parameters.json !WebAPP/DataStorage/Variables.json +!WebAPP/DataStorage/Indicators.json +!WebAPP/DataStorage/Duals.json WebAPP/References/jqwidgets_licenced WebAPP/References/jqwidgets_free diff --git a/API/Classes/Case/HelpersClass.py b/API/Classes/Case/HelpersClass.py new file mode 100644 index 000000000..32444a1ff --- /dev/null +++ b/API/Classes/Case/HelpersClass.py @@ -0,0 +1,195 @@ +# Classes/Helpers/helpers.py + +import os +import shutil +from pathlib import Path +from copy import deepcopy + + +class Helpers: + + @staticmethod + def build_param(parameters: dict) -> dict[str, dict[str, str]]: + d = {} + for k, lst in parameters.items(): + tmp = {} + for de in lst: + tmp[de['id']] = str(de['value']).replace(" ", "") + d[k] = tmp + return d + + @staticmethod + def build_vars(variables: dict) -> list[str]: + names = [] + for _, lst in variables.items(): + for de in lst: + names.append(de['name']) + return names + + @staticmethod + def build_var_by_name(variables: dict) -> dict: + out = {} + for group, entries in variables.items(): + for obj in entries: + out[obj["name"]] = { + "id": obj["id"], + "group": group, + "setrelation": obj.get("setrelation", []) + } + return out + + @staticmethod + def merge_groups(a: dict, b: dict) -> dict: + out = {**a} + for key, value in b.items(): + if key in out and isinstance(out[key], dict) and isinstance(value, dict): + out[key] = {**out[key], **value} + elif key in out and isinstance(out[key], list) and isinstance(value, list): + out[key] = out[key] + value + else: + out[key] = value + return out + + @staticmethod + def resolve_solver_executable(folder: Path, exe_name: str, system: str): + candidate = folder / exe_name + if candidate.exists(): + if system != "Windows": + os.chmod(candidate, os.stat(candidate).st_mode | 0o111) + return str(candidate.resolve()), True + + which = shutil.which(exe_name) + if which: + return which, False + + paths = [] + if system == "Darwin": + paths = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin"] + elif system == "Linux": + paths = ["/usr/bin", "/usr/local/bin", "/bin", "/snap/bin"] + + for p in paths: + test = Path(p) / exe_name + if test.exists(): + return str(test), False + + raise FileNotFoundError(f"Solver not found: {exe_name}") + + @staticmethod + def keys_exists(element: dict, *keys) -> bool: + if not isinstance(element, dict): + return False + + cur = element + for key in keys: + if key not in cur: + return False + cur = cur[key] + return True + + # ------------------------------------------------------- + # Ovdje ide logika indikatora, ali bez `self` + # ------------------------------------------------------- + + @staticmethod + def merge_all_indicators(indicator_types_json, custom_indicators, tech_map): + type_by_id = {} + + for group, items in indicator_types_json.items(): + for item in items: + type_by_id[item["id"]] = {**item, "group": group} + + result = {} + + for item in custom_indicators: + indicator_id = item.get("IndicatorId") + type_id = item.get("IndicatorTypeId") + + if not indicator_id or not type_id: + continue + + type_rec = type_by_id.get(type_id) + if not type_rec: + continue + + merged = deepcopy(item) + merged["Techs"] = [tech_map.get(t, t) for t in merged.get("Techs", [])] + merged["group"] = type_rec["group"] + merged["indicator_type"] = {k: v for k, v in type_rec.items() if k != "group"} + merged["id"] = indicator_id + merged.pop("IndicatorId", None) + + result[indicator_id] = merged + + return result + + @staticmethod + def merge_all_indicators_grouped(indicator_types_json: dict, custom_indicators: list, tech_map: dict) -> dict: + """ + Vraća strukturu grupisanu po 'group': + { + "": [ + { ... indikator ... }, + { ... indikator ... } + ] + } + """ + + # 1) Sakupi sve tipove indikatora + group info + type_by_id = {} + for group_name, group_items in indicator_types_json.items(): + if not isinstance(group_items, list): + continue + + for item in group_items: + if isinstance(item, dict) and "id" in item: + type_by_id[item["id"]] = { **item, "group": group_name } + + # 2) rezultat: group -> list of objects + result = {} + + # 3) obrada custom indikatora + for item in custom_indicators: + if not isinstance(item, dict): + continue + + indicator_name = item.get("Indicator") + indicator_id = item.get("IndicatorId") + indicator_type_id = item.get("IndicatorTypeId") + + if not indicator_name or not indicator_id or not indicator_type_id: + continue + + type_rec = type_by_id.get(indicator_type_id) + if not type_rec: + continue + + group = type_rec["group"] + merged = deepcopy(item) + + # Mapiranje Sets: TECHid -> TechName + techs_ids = merged.get("Techs", []) + if isinstance(techs_ids, list): + merged["Techs"] = [tech_map.get(t, t) for t in techs_ids] + + # Root-level group + merged["group"] = group + + # indicator_type bez 'group' + clean_type = {k: v for k, v in type_rec.items() if k != "group"} + merged["indicator_type"] = deepcopy(clean_type) + + # Rename IndicatorId -> id + merged["id"] = indicator_id + if "IndicatorId" in merged: + del merged["IndicatorId"] + + # ------------------------------- + # UPIS: grupa -> lista objekata + # ------------------------------- + if group not in result: + result[group] = [] + + result[group].append(merged) + + return result \ No newline at end of file diff --git a/WebAPP/App/View/Modals.html b/WebAPP/App/View/Modals.html new file mode 100644 index 000000000..1c412da60 --- /dev/null +++ b/WebAPP/App/View/Modals.html @@ -0,0 +1,24 @@ + \ No newline at end of file diff --git a/WebAPP/DataStorage/Duals.json b/WebAPP/DataStorage/Duals.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/WebAPP/DataStorage/Duals.json @@ -0,0 +1 @@ +{} diff --git a/WebAPP/DataStorage/Indicators.json b/WebAPP/DataStorage/Indicators.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/WebAPP/DataStorage/Indicators.json @@ -0,0 +1 @@ +{}