diff --git a/.github/workflows/coverage-pages.yml b/.github/workflows/coverage-pages.yml index 5d2025c..2cd7948 100644 --- a/.github/workflows/coverage-pages.yml +++ b/.github/workflows/coverage-pages.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 20.x cache: 'npm' diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index e1e5390..107ee73 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ matrix.node-version }} cache: 'npm' diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index 22f6046..5fe77f1 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 20.x cache: 'npm' diff --git a/dist/js/applicationMixin.d.ts b/dist/js/applicationMixin.d.ts index 11aab73..c620da9 100644 --- a/dist/js/applicationMixin.d.ts +++ b/dist/js/applicationMixin.d.ts @@ -4,6 +4,7 @@ import type { NamedInMemoryEntity } from "@mat3ra/code/dist/js/entity/mixins/Nam import type { Constructor } from "@mat3ra/code/dist/js/utils/types"; import type { ApplicationSchemaBase } from "@mat3ra/esse/dist/js/types"; import Executable from "./executable"; +export declare const APPS_USING_MATERIAL_FALLBACK: Set; type Base = InMemoryEntity & NamedInMemoryEntity & DefaultableInMemoryEntity; export type BaseConstructor = Constructor & { constructCustomExecutable?: (config: object) => Executable; diff --git a/dist/js/applicationMixin.js b/dist/js/applicationMixin.js index e298efb..b5dd302 100644 --- a/dist/js/applicationMixin.js +++ b/dist/js/applicationMixin.js @@ -3,10 +3,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +exports.APPS_USING_MATERIAL_FALLBACK = void 0; exports.applicationMixin = applicationMixin; exports.applicationStaticMixin = applicationStaticMixin; const JSONSchemasInterface_1 = __importDefault(require("@mat3ra/esse/dist/js/esse/JSONSchemasInterface")); const standata_1 = require("@mat3ra/standata"); +// Fallback list consulted only when `isUsingMaterial` is absent from the +// application config. Keeps legacy workflows/jobs (created before the +// `isUsingMaterial` flag existed) rendering the materials tab without +// requiring a DB migration. +// DO NOT add new apps to this list, new apps should use `isUsingMaterial` in +// standata definitions instead. +exports.APPS_USING_MATERIAL_FALLBACK = new Set(["vasp", "nwchem", "espresso"]); function applicationMixin(item) { // @ts-expect-error const properties = { @@ -29,7 +37,10 @@ function applicationMixin(item) { return this.prop("isLicensed", false); }, get isUsingMaterial() { - return this.prop("isUsingMaterial", false); + const stored = this.prop("isUsingMaterial"); + if (typeof stored === "boolean") + return stored; + return exports.APPS_USING_MATERIAL_FALLBACK.has(this.name); }, }; Object.defineProperties(item, Object.getOwnPropertyDescriptors(properties)); diff --git a/src/js/applicationMixin.ts b/src/js/applicationMixin.ts index 0e75b70..972c1ac 100644 --- a/src/js/applicationMixin.ts +++ b/src/js/applicationMixin.ts @@ -8,6 +8,14 @@ import { ApplicationStandata } from "@mat3ra/standata"; import Executable from "./executable"; +// Fallback list consulted only when `isUsingMaterial` is absent from the +// application config. Keeps legacy workflows/jobs (created before the +// `isUsingMaterial` flag existed) rendering the materials tab without +// requiring a DB migration. +// DO NOT add new apps to this list, new apps should use `isUsingMaterial` in +// standata definitions instead. +export const APPS_USING_MATERIAL_FALLBACK = new Set(["vasp", "nwchem", "espresso"]); + type Base = InMemoryEntity & NamedInMemoryEntity & DefaultableInMemoryEntity; export type BaseConstructor = Constructor & { @@ -62,7 +70,9 @@ export function applicationMixin(item: Base) { }, get isUsingMaterial() { - return this.prop("isUsingMaterial", false); + const stored = this.prop("isUsingMaterial"); + if (typeof stored === "boolean") return stored; + return APPS_USING_MATERIAL_FALLBACK.has(this.name); }, }; diff --git a/src/py/mat3ra/ade/application.py b/src/py/mat3ra/ade/application.py index a6bf4c0..9272587 100644 --- a/src/py/mat3ra/ade/application.py +++ b/src/py/mat3ra/ade/application.py @@ -3,6 +3,15 @@ from mat3ra.utils.object import calculate_hash_from_object, remove_timestampable_keys from pydantic import ConfigDict +# Fallback list consulted only when `isUsingMaterial` is absent from the +# application config. Keeps legacy workflows/jobs (created before the +# `isUsingMaterial` flag existed) rendering the materials tab without +# requiring a DB migration. Mirror of the JS-side constant in +# `applicationMixin.ts`. +# DO NOT add new apps to this list, new apps should use `isUsingMaterial` in +# standata definitions instead. +APPS_USING_MATERIAL_FALLBACK = frozenset({"vasp", "nwchem", "espresso"}) + class Application(ApplicationSchemaBase, InMemoryEntitySnakeCase): """ @@ -16,7 +25,8 @@ class Application(ApplicationSchemaBase, InMemoryEntitySnakeCase): summary: Application's short description hasAdvancedComputeOptions: Whether advanced compute options are present isLicensed: Whether licensing is present - isUsingMaterial: Whether this application is used for materials processing + isUsingMaterial: Whether this application uses a material as input. + When absent, falls back to `APPS_USING_MATERIAL_FALLBACK`. isDefault: Identifies that entity is defaultable schemaVersion: Entity's schema version """ @@ -29,7 +39,9 @@ class Application(ApplicationSchemaBase, InMemoryEntitySnakeCase): @property def is_using_material(self) -> bool: - return bool(self.isUsingMaterial) + if self.isUsingMaterial is not None: + return bool(self.isUsingMaterial) + return self.name in APPS_USING_MATERIAL_FALLBACK def get_short_name(self) -> str: return self.short_name if self.short_name else self.name diff --git a/tests/js/application.test.ts b/tests/js/application.test.ts index 735c544..fc0ea45 100644 --- a/tests/js/application.test.ts +++ b/tests/js/application.test.ts @@ -109,6 +109,25 @@ describe("Application", () => { const otherApp = new Application({ name: "other_app" }); expect(otherApp.isUsingMaterial).to.be.false; }); + + describe("fallback when key is missing", () => { + ["vasp", "nwchem", "espresso"].forEach((name) => { + it(`should fall back to true for ${name} when key is absent`, () => { + expect(new Application({ name }).isUsingMaterial).to.be.true; + }); + }); + + ["deepmd", "lammps", "python", "shell"].forEach((name) => { + it(`should stay false for ${name} when key is absent (not in fallback list)`, () => { + expect(new Application({ name }).isUsingMaterial).to.be.false; + }); + }); + + it("should honor explicit false even for fallback-listed app", () => { + const app = new Application({ name: "espresso", isUsingMaterial: false }); + expect(app.isUsingMaterial).to.be.false; + }); + }); }); }); diff --git a/tests/py/test_application.py b/tests/py/test_application.py index 989feb7..1084949 100644 --- a/tests/py/test_application.py +++ b/tests/py/test_application.py @@ -68,6 +68,21 @@ def test_is_using_material_property(): assert Application(name="other_app").is_using_material is False +def test_is_using_material_falls_back_when_key_missing(): + # When `isUsingMaterial` is absent (e.g. legacy workflow snapshots) the + # fallback list of well-known material apps is consulted by name. + for name in ["vasp", "nwchem", "espresso"]: + assert Application(name=name).is_using_material is True + # Apps outside the fallback list (and without an explicit flag) stay False. + for name in ["deepmd", "lammps", "python", "shell", "other_app"]: + assert Application(name=name).is_using_material is False + + +def test_is_using_material_explicit_false_overrides_fallback(): + # An explicit `isUsingMaterial: False` must win over the fallback list. + assert Application(name="espresso", isUsingMaterial=False).is_using_material is False + + def test_get_short_name(): app_with_short = Application(name="espresso", shortName="QE") assert app_with_short.get_short_name() == "QE"