diff --git a/.flake8 b/.flake8 index 8d6a197..7333708 100644 --- a/.flake8 +++ b/.flake8 @@ -8,5 +8,6 @@ exclude = docs/tools/ reproschema/_version.py reproschema/models/model.py + *venv* max-line-length=79 extend-ignore = B001, B006, B016, E501, E722, F821 diff --git a/reproschema/cli.py b/reproschema/cli.py index 7ec52c4..047c362 100644 --- a/reproschema/cli.py +++ b/reproschema/cli.py @@ -302,3 +302,7 @@ def reproschema2fhir(reproschema_questionnaire, output): with open(output_path / f"{file_name}/{file_name}.json", "w+") as f: f.write(json.dumps(fhir_questionnaire)) + + +if __name__ == "__main__": + main(prog_name="reproschema") diff --git a/reproschema/models/model.py b/reproschema/models/model.py index 1dbc067..9489828 100644 --- a/reproschema/models/model.py +++ b/reproschema/models/model.py @@ -1,7 +1,5 @@ from __future__ import annotations -import re -import sys from datetime import date, datetime from decimal import Decimal from enum import Enum @@ -10,7 +8,13 @@ from pydantic.version import VERSION as PYDANTIC_VERSION if int(PYDANTIC_VERSION[0]) >= 2: - from pydantic import BaseModel, ConfigDict, Field, field_validator + from pydantic import ( + BaseModel, + ConfigDict, + Field, + field_validator, + model_validator, + ) else: from pydantic import BaseModel, Field, validator metamodel_version = "None" @@ -178,6 +182,9 @@ class AdditionalProperty(Thing): An object to describe the various properties added to assessments and Items. """ + # Override parent's extra="forbid" to allow extra fields + model_config = ConfigDict(extra="allow") + allow: Optional[List[AllowedType]] = Field( default_factory=list, title="allow", @@ -229,6 +236,31 @@ class AdditionalProperty(Thing): title="UI", description="An element to control UI specifications. Originally @nest in jsonld, but using a class in the model.", ) + + # Add backgroundImage as an explicit field to allow it + backgroundImage: Optional[str] = Field( + None, + title="backgroundImage", + description="Background image for drawing activities.", + ) + + @model_validator(mode="after") + def validate_only_background_image_extra(self): + """Validate that only backgroundImage is allowed as an extra field.""" + # Get any extra fields that weren't explicitly defined + extra_fields = getattr(self, "__pydantic_extra__", {}) + + if extra_fields: + # Since backgroundImage is now an explicit field, any extra fields are not allowed + extra_field_names = list(extra_fields.keys()) + raise ValueError( + f"Extra fields are not permitted in AdditionalProperty. " + f"Only 'backgroundImage' is allowed as an additional field, " + f"but found: {extra_field_names}" + ) + + return self + id: Optional[str] = Field( None, description="A unique identifier for an entity. Must be either a CURIE shorthand for a URI or a complete URI.", diff --git a/reproschema/models/tests/test_schema.py b/reproschema/models/tests/test_schema.py index f7ee126..d5c9891 100644 --- a/reproschema/models/tests/test_schema.py +++ b/reproschema/models/tests/test_schema.py @@ -3,6 +3,7 @@ from pathlib import Path import pytest +from pydantic import ValidationError from pyld import jsonld from ...jsonldutils import load_file @@ -176,3 +177,115 @@ def test_item(tmp_path, server_http_kwargs): ) del data_comp["@context"] assert item_dict == data_comp + + +def test_canvas_activity_allows_extra_fields(): + """Test that canvas activities allow backgroundImage (now an explicit field).""" + activity_dict = { + "category": "Activity", + "id": "canvas_activity.jsonld", + "prefLabel": {"en": "Canvas Drawing Activity"}, + "description": {"en": "A canvas drawing activity"}, + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "canvas", + "addProperties": [ + { + "isAbout": "item1", + "variableName": "canvas_item", + "backgroundImage": "./images/drawaclock.png", + } + ], + }, + } + + # This should work fine + activity_obj = Activity(**activity_dict) + assert ( + activity_obj.ui.addProperties[0].backgroundImage + == "./images/drawaclock.png" + ) + + +def test_background_image_always_allowed(): + """Test that backgroundImage is always allowed regardless of inputType.""" + activity_dict = { + "category": "Activity", + "id": "radio_activity_with_background.jsonld", + "prefLabel": {"en": "Radio Button Activity with Background"}, + "description": {"en": "A radio button activity with background image"}, + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "radio", + "addProperties": [ + { + "isAbout": "item1", + "variableName": "radio_item", + # backgroundImage should be allowed for all activities + "backgroundImage": "./images/some_image.png", + } + ], + }, + } + + # This should work fine - backgroundImage is now an explicit field + activity_obj = Activity(**activity_dict) + assert ( + activity_obj.ui.addProperties[0].backgroundImage + == "./images/some_image.png" + ) + + +def test_extra_fields_rejected(): + """Test that extra fields other than backgroundImage are rejected.""" + activity_dict = { + "category": "Activity", + "id": "activity_with_invalid_extra.jsonld", + "prefLabel": {"en": "Activity with Invalid Extra Field"}, + "description": {"en": "An activity with an invalid extra field"}, + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "radio", + "addProperties": [ + { + "isAbout": "item1", + "variableName": "test_item", + "backgroundImage": "./images/test.png", # This is allowed + "customTool": "special_pen", # This should be rejected + } + ], + }, + } + + # This should fail because customTool is not allowed + with pytest.raises(ValueError, match="Extra fields are not permitted"): + Activity(**activity_dict) + + +def test_activity_without_extra_fields_works(): + """Test that activities without extra fields work normally regardless of input type.""" + activity_dict = { + "category": "Activity", + "id": "normal_activity.jsonld", + "prefLabel": {"en": "Normal Activity"}, + "description": {"en": "A normal activity without extra fields"}, + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "radio", + "addProperties": [ + { + "isAbout": "item1", + "variableName": "normal_item", + # No extra fields + } + ], + }, + } + + # This should work fine + activity_obj = Activity(**activity_dict) + assert activity_obj.ui.addProperties[0].variableName == "normal_item"