Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions reproschema/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
38 changes: 35 additions & 3 deletions reproschema/models/model.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
Expand Down
113 changes: 113 additions & 0 deletions reproschema/models/tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pathlib import Path

import pytest
from pydantic import ValidationError
from pyld import jsonld

from ...jsonldutils import load_file
Expand Down Expand Up @@ -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"