diff --git a/.gitignore b/.gitignore index 4a705707..588e1c26 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 00000000..32444a1f --- /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/API/Routes/Upload/UploadRoute.py b/API/Routes/Upload/UploadRoute.py index 4d451c37..b445cb5f 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 @@ -213,10 +213,12 @@ 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) + # 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 # for filename in os.listdir(str(casePath)): diff --git a/WebAPP/App/View/Modals.html b/WebAPP/App/View/Modals.html new file mode 100644 index 00000000..1c412da6 --- /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 00000000..0967ef42 --- /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 00000000..0967ef42 --- /dev/null +++ b/WebAPP/DataStorage/Indicators.json @@ -0,0 +1 @@ +{}