From 0d1a74875b0eb03510b0224ac5ecfa2074eecbfa Mon Sep 17 00:00:00 2001 From: Quitterie Lucas Date: Tue, 16 May 2023 09:45:03 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8(models)=20clean=20xAPI=20pydantic?= =?UTF-8?q?=20models=20naming=20convention?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The naming convention used in xAPI Pydantic modelisation was confusing for users. Furthermore, some models are being cross used between several profiles. A new naming convention has been applied for more clarity and to factorize common vocabulary usable in all profiles. --- CHANGELOG.md | 1 + src/ralph/api/models.py | 7 +- src/ralph/api/routers/statements.py | 22 +- src/ralph/models/edx/converters/xapi/base.py | 16 +- src/ralph/models/edx/converters/xapi/video.py | 50 ++-- .../models/xapi/{fields => base}/__init__.py | 0 src/ralph/models/xapi/base/agents.py | 95 +++++++ .../xapi/{fields => base}/attachments.py | 6 +- .../models/xapi/{fields => base}/common.py | 2 +- src/ralph/models/xapi/base/contexts.py | 57 ++++ src/ralph/models/xapi/base/groups.py | 102 ++++++++ src/ralph/models/xapi/base/objects.py | 49 ++++ .../models/xapi/{fields => base}/results.py | 17 +- .../xapi/{base.py => base/statements.py} | 51 ++-- .../xapi/{fields => base}/unnested_objects.py | 50 ++-- src/ralph/models/xapi/base/verbs.py | 18 ++ .../fields => concepts}/__init__.py | 0 .../activity_types}/__init__.py | 0 .../activity_types/acrossx_profile.py | 26 ++ .../activity_streams_vocabulary.py | 24 ++ .../activity_types}/adl_vocabulary.py | 0 .../concepts/activity_types/scorm_profile.py | 26 ++ .../xapi/concepts/activity_types/video.py | 35 +++ .../activity_types/virtual_classroom.py | 29 ++ .../xapi/concepts/constants/__init__.py | 1 + .../concepts/constants/acrossx_profile.py | 22 ++ .../constants/activity_streams_vocabulary.py | 19 ++ .../xapi/concepts/constants/adl_vocabulary.py | 18 ++ .../xapi/concepts/constants/cmi5_profile.py | 6 + .../fields => concepts}/constants/quiz.py | 0 .../xapi/concepts/constants/scorm_profile.py | 39 +++ .../concepts/constants/tincan_vocabulary.py | 20 ++ .../models/xapi/concepts/constants/video.py | 61 +++++ .../concepts/constants/virtual_classroom.py | 53 ++++ .../models/xapi/concepts/verbs/__init__.py | 1 + .../xapi/concepts/verbs/acrossx_profile.py | 19 ++ .../verbs/activity_streams_vocabulary.py | 36 +++ .../xapi/concepts/verbs/adl_vocabulary.py | 36 +++ .../xapi/concepts/verbs/scorm_profile.py | 64 +++++ .../xapi/concepts/verbs/tincan_vocabulary.py | 19 ++ src/ralph/models/xapi/concepts/verbs/video.py | 50 ++++ .../xapi/concepts/verbs/virtual_classroom.py | 144 ++++++++++ src/ralph/models/xapi/constants.py | 42 --- src/ralph/models/xapi/fields/actors.py | 156 ----------- src/ralph/models/xapi/fields/contexts.py | 54 ---- src/ralph/models/xapi/fields/objects.py | 61 ----- src/ralph/models/xapi/fields/verbs.py | 53 ---- .../models/xapi/navigation/fields/objects.py | 33 --- .../models/xapi/navigation/statements.py | 27 +- src/ralph/models/xapi/video/constants.py | 58 ---- src/ralph/models/xapi/video/contexts.py | 247 ++++++++++++++++++ .../models/xapi/video/fields/contexts.py | 241 ----------------- src/ralph/models/xapi/video/fields/objects.py | 36 --- src/ralph/models/xapi/video/fields/results.py | 175 ------------- src/ralph/models/xapi/video/fields/verbs.py | 118 --------- src/ralph/models/xapi/video/results.py | 175 +++++++++++++ src/ralph/models/xapi/video/statements.py | 168 ++++++------ tests/fixtures/hypothesis_configuration.py | 2 +- tests/fixtures/hypothesis_strategies.py | 8 +- .../edx/converters/xapi/test_navigational.py | 2 - .../models/edx/converters/xapi/test_server.py | 2 - .../models/edx/converters/xapi/test_video.py | 25 +- tests/models/test_converter.py | 4 +- .../models/xapi/{fields => base}/__init__.py | 0 tests/models/xapi/base/test_agents.py | 47 ++++ .../xapi/{fields => base}/test_common.py | 2 +- tests/models/xapi/base/test_groups.py | 16 ++ tests/models/xapi/base/test_objects.py | 12 + tests/models/xapi/base/test_results.py | 35 +++ .../{test_base.py => base/test_statements.py} | 183 +++++++------ .../models/xapi/base/test_unnested_objects.py | 72 +++++ tests/models/xapi/concepts/__init__.py | 0 .../xapi/concepts/test_activity_types.py | 74 ++++++ tests/models/xapi/concepts/test_verbs.py | 112 ++++++++ tests/models/xapi/fields/test_actors.py | 12 - tests/models/xapi/fields/test_objects.py | 12 - tests/models/xapi/fields/test_verbs.py | 21 -- tests/models/xapi/test_navigation.py | 16 +- tests/models/xapi/test_video.py | 76 +++++- 79 files changed, 2263 insertions(+), 1405 deletions(-) rename src/ralph/models/xapi/{fields => base}/__init__.py (100%) create mode 100644 src/ralph/models/xapi/base/agents.py rename src/ralph/models/xapi/{fields => base}/attachments.py (85%) rename src/ralph/models/xapi/{fields => base}/common.py (97%) create mode 100644 src/ralph/models/xapi/base/contexts.py create mode 100644 src/ralph/models/xapi/base/groups.py create mode 100644 src/ralph/models/xapi/base/objects.py rename src/ralph/models/xapi/{fields => base}/results.py (82%) rename src/ralph/models/xapi/{base.py => base/statements.py} (58%) rename src/ralph/models/xapi/{fields => base}/unnested_objects.py (69%) create mode 100644 src/ralph/models/xapi/base/verbs.py rename src/ralph/models/xapi/{navigation/fields => concepts}/__init__.py (100%) rename src/ralph/models/xapi/{video/fields => concepts/activity_types}/__init__.py (100%) create mode 100644 src/ralph/models/xapi/concepts/activity_types/acrossx_profile.py create mode 100644 src/ralph/models/xapi/concepts/activity_types/activity_streams_vocabulary.py rename src/ralph/models/xapi/{video/fields => concepts/activity_types}/adl_vocabulary.py (100%) create mode 100644 src/ralph/models/xapi/concepts/activity_types/scorm_profile.py create mode 100644 src/ralph/models/xapi/concepts/activity_types/video.py create mode 100644 src/ralph/models/xapi/concepts/activity_types/virtual_classroom.py create mode 100644 src/ralph/models/xapi/concepts/constants/__init__.py create mode 100644 src/ralph/models/xapi/concepts/constants/acrossx_profile.py create mode 100644 src/ralph/models/xapi/concepts/constants/activity_streams_vocabulary.py create mode 100644 src/ralph/models/xapi/concepts/constants/adl_vocabulary.py create mode 100644 src/ralph/models/xapi/concepts/constants/cmi5_profile.py rename src/ralph/models/xapi/{navigation/fields => concepts}/constants/quiz.py (100%) create mode 100644 src/ralph/models/xapi/concepts/constants/scorm_profile.py create mode 100644 src/ralph/models/xapi/concepts/constants/tincan_vocabulary.py create mode 100644 src/ralph/models/xapi/concepts/constants/video.py create mode 100644 src/ralph/models/xapi/concepts/constants/virtual_classroom.py create mode 100644 src/ralph/models/xapi/concepts/verbs/__init__.py create mode 100644 src/ralph/models/xapi/concepts/verbs/acrossx_profile.py create mode 100644 src/ralph/models/xapi/concepts/verbs/activity_streams_vocabulary.py create mode 100644 src/ralph/models/xapi/concepts/verbs/adl_vocabulary.py create mode 100644 src/ralph/models/xapi/concepts/verbs/scorm_profile.py create mode 100644 src/ralph/models/xapi/concepts/verbs/tincan_vocabulary.py create mode 100644 src/ralph/models/xapi/concepts/verbs/video.py create mode 100644 src/ralph/models/xapi/concepts/verbs/virtual_classroom.py delete mode 100644 src/ralph/models/xapi/fields/actors.py delete mode 100644 src/ralph/models/xapi/fields/contexts.py delete mode 100644 src/ralph/models/xapi/fields/objects.py delete mode 100644 src/ralph/models/xapi/fields/verbs.py delete mode 100644 src/ralph/models/xapi/navigation/fields/objects.py delete mode 100644 src/ralph/models/xapi/video/constants.py create mode 100644 src/ralph/models/xapi/video/contexts.py delete mode 100644 src/ralph/models/xapi/video/fields/contexts.py delete mode 100644 src/ralph/models/xapi/video/fields/objects.py delete mode 100644 src/ralph/models/xapi/video/fields/results.py delete mode 100644 src/ralph/models/xapi/video/fields/verbs.py create mode 100644 src/ralph/models/xapi/video/results.py rename tests/models/xapi/{fields => base}/__init__.py (100%) create mode 100644 tests/models/xapi/base/test_agents.py rename tests/models/xapi/{fields => base}/test_common.py (98%) create mode 100644 tests/models/xapi/base/test_groups.py create mode 100644 tests/models/xapi/base/test_objects.py create mode 100644 tests/models/xapi/base/test_results.py rename tests/models/xapi/{test_base.py => base/test_statements.py} (77%) create mode 100644 tests/models/xapi/base/test_unnested_objects.py create mode 100644 tests/models/xapi/concepts/__init__.py create mode 100644 tests/models/xapi/concepts/test_activity_types.py create mode 100644 tests/models/xapi/concepts/test_verbs.py delete mode 100644 tests/models/xapi/fields/test_actors.py delete mode 100644 tests/models/xapi/fields/test_objects.py delete mode 100644 tests/models/xapi/fields/test_verbs.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d8dd175fb..902819b37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to ### Changed +- Clean xAPI pydantic models naming convention - Upgrade `fastapi` to `0.95.2` - Upgrade `sentry_sdk` to `1.23.1` - Upgrade `httpx` to `0.24.1` diff --git a/src/ralph/api/models.py b/src/ralph/api/models.py index 45fc92e42..94a8ee3d1 100644 --- a/src/ralph/api/models.py +++ b/src/ralph/api/models.py @@ -3,12 +3,13 @@ Allows to be exactly as lax as we want when it comes to exact object shape and validation. """ -from typing import Optional +from typing import Optional, Union from uuid import UUID from pydantic import AnyUrl, BaseModel, Extra -from ..models.xapi.fields.actors import ActorField +from ..models.xapi.base.agents import BaseXapiAgent +from ..models.xapi.base.groups import BaseXapiGroup class ErrorDetail(BaseModel): @@ -62,7 +63,7 @@ class LaxStatement(BaseModelWithLaxConfig): qualify an object as an XAPI statement. """ - actor: ActorField + actor: Union[BaseXapiAgent, BaseXapiGroup] id: Optional[UUID] object: LaxObjectField verb: LaxVerbField diff --git a/src/ralph/api/routers/statements.py b/src/ralph/api/routers/statements.py index 408ea61a5..a71220f0c 100644 --- a/src/ralph/api/routers/statements.py +++ b/src/ralph/api/routers/statements.py @@ -22,12 +22,12 @@ from ralph.backends.database.base import BaseDatabase, StatementParameters from ralph.conf import settings from ralph.exceptions import BackendException, BadFormatException -from ralph.models.xapi.fields.actors import ( - AccountActorField, - AgentActorField, - MboxActorField, - MboxSha1SumActorField, - OpenIdActorField, +from ralph.models.xapi.base.agents import ( + BaseXapiAgent, + BaseXapiAgentWithAccountIFI, + BaseXapiAgentWithMboxIFI, + BaseXapiAgentWithMboxSha1SumIFI, + BaseXapiAgentWithOpenIdIFI, ) from ..auth import authenticated_user @@ -240,17 +240,17 @@ async def get( query_params = dict(request.query_params) if query_params.get("agent") is not None: # Transform agent to `dict` as FastAPI cannot parse JSON (seen as string) - agent = parse_raw_as(AgentActorField, query_params["agent"]) + agent = parse_raw_as(BaseXapiAgent, query_params["agent"]) query_params.pop("agent") - if isinstance(agent, MboxActorField): + if isinstance(agent, BaseXapiAgentWithMboxIFI): query_params["agent__mbox"] = agent.mbox - elif isinstance(agent, MboxSha1SumActorField): + elif isinstance(agent, BaseXapiAgentWithMboxSha1SumIFI): query_params["agent__mbox_sha1sum"] = agent.mbox_sha1sum - elif isinstance(agent, OpenIdActorField): + elif isinstance(agent, BaseXapiAgentWithOpenIdIFI): query_params["agent__openid"] = agent.openid - elif isinstance(agent, AccountActorField): + elif isinstance(agent, BaseXapiAgentWithAccountIFI): query_params["agent__account__name"] = agent.account.name query_params["agent__account__home_page"] = agent.account.homePage diff --git a/src/ralph/models/edx/converters/xapi/base.py b/src/ralph/models/edx/converters/xapi/base.py index e4a653a4e..e325ce2a8 100644 --- a/src/ralph/models/edx/converters/xapi/base.py +++ b/src/ralph/models/edx/converters/xapi/base.py @@ -5,10 +5,12 @@ from ralph.exceptions import ConfigurationException from ralph.models.converter import BaseConversionSet, ConversionItem -from ralph.models.xapi.constants import ( - EXTENSION_COURSE_ID, - EXTENSION_MODULE_ID, - EXTENSION_SCHOOL_ID, +from ralph.models.xapi.concepts.constants.acrossx_profile import ( + CONTEXT_EXTENSION_SCHOOL_ID, +) +from ralph.models.xapi.concepts.constants.scorm_profile import ( + CONTEXT_EXTENSION_COURSE_ID, + CONTEXT_EXTENSION_MODULE_ID, ) @@ -51,16 +53,16 @@ def _get_conversion_items(self): lambda user_id: str(user_id) if user_id else "anonymous", ), ConversionItem( - "object__definition__extensions__" + EXTENSION_SCHOOL_ID, + "object__definition__extensions__" + CONTEXT_EXTENSION_SCHOOL_ID, "context__org_id", ), ConversionItem( - "object__definition__extensions__" + EXTENSION_COURSE_ID, + "object__definition__extensions__" + CONTEXT_EXTENSION_COURSE_ID, "context__course_id", (self.parse_course_id, lambda x: x["course"]), ), ConversionItem( - "object__definition__extensions__" + EXTENSION_MODULE_ID, + "object__definition__extensions__" + CONTEXT_EXTENSION_MODULE_ID, "context__course_id", (self.parse_course_id, lambda x: x["module"]), ), diff --git a/src/ralph/models/edx/converters/xapi/video.py b/src/ralph/models/edx/converters/xapi/video.py index 172499135..ce8293ed2 100644 --- a/src/ralph/models/edx/converters/xapi/video.py +++ b/src/ralph/models/edx/converters/xapi/video.py @@ -8,16 +8,16 @@ UISeekVideo, UIStopVideo, ) -from ralph.models.xapi.constants import LANG_EN_US_DISPLAY -from ralph.models.xapi.video.constants import ( - VIDEO_EXTENSION_LENGTH, - VIDEO_EXTENSION_PROGRESS, - VIDEO_EXTENSION_SESSION_ID, - VIDEO_EXTENSION_TIME, - VIDEO_EXTENSION_TIME_FROM, - VIDEO_EXTENSION_TIME_TO, - VIDEO_EXTENSION_USER_AGENT, +from ralph.models.xapi.concepts.constants.video import ( + CONTEXT_EXTENSION_LENGTH, + CONTEXT_EXTENSION_PROGRESS, + CONTEXT_EXTENSION_SESSION_ID, + CONTEXT_EXTENSION_TIME, + CONTEXT_EXTENSION_TIME_FROM, + CONTEXT_EXTENSION_TIME_TO, + CONTEXT_EXTENSION_USER_AGENT, ) +from ralph.models.xapi.constants import LANG_EN_US_DISPLAY from ralph.models.xapi.video.statements import ( VideoInitialized, VideoPaused, @@ -52,7 +52,7 @@ def _get_conversion_items(self): + event["event"]["id"], ), ConversionItem( - "context__extensions__" + VIDEO_EXTENSION_SESSION_ID, + "context__extensions__" + CONTEXT_EXTENSION_SESSION_ID, "session", ), }, @@ -71,7 +71,7 @@ def _get_conversion_items(self): return conversion_items.union( { ConversionItem( - "context__extensions__" + VIDEO_EXTENSION_LENGTH, + "context__extensions__" + CONTEXT_EXTENSION_LENGTH, None, # Set the video length to null by default. # This information is mandatory in the xAPI template @@ -79,11 +79,11 @@ def _get_conversion_items(self): lambda _: 0.0, ), ConversionItem( - "context__extensions__" + VIDEO_EXTENSION_SESSION_ID, + "context__extensions__" + CONTEXT_EXTENSION_SESSION_ID, "session", ), ConversionItem( - "context__extensions__" + VIDEO_EXTENSION_USER_AGENT, "agent" + "context__extensions__" + CONTEXT_EXTENSION_USER_AGENT, "agent" ), }, ) @@ -101,11 +101,11 @@ def _get_conversion_items(self): return conversion_items.union( { ConversionItem( - "result__extensions__" + VIDEO_EXTENSION_TIME, + "result__extensions__" + CONTEXT_EXTENSION_TIME, "event__currentTime", ), ConversionItem( - "context__extensions__" + VIDEO_EXTENSION_SESSION_ID, + "context__extensions__" + CONTEXT_EXTENSION_SESSION_ID, "session", ), }, @@ -124,11 +124,11 @@ def _get_conversion_items(self): return conversion_items.union( { ConversionItem( - "result__extensions__" + VIDEO_EXTENSION_TIME, + "result__extensions__" + CONTEXT_EXTENSION_TIME, "event__currentTime", ), ConversionItem( - "context__extensions__" + VIDEO_EXTENSION_LENGTH, + "context__extensions__" + CONTEXT_EXTENSION_LENGTH, None, # Set the video length to null by default. # This information is mandatory in the xAPI template @@ -136,7 +136,7 @@ def _get_conversion_items(self): lambda _: 0.0, ), ConversionItem( - "context__extensions__" + VIDEO_EXTENSION_SESSION_ID, + "context__extensions__" + CONTEXT_EXTENSION_SESSION_ID, "session", ), }, @@ -155,11 +155,11 @@ def _get_conversion_items(self): return conversion_items.union( { ConversionItem( - "result__extensions__" + VIDEO_EXTENSION_TIME, + "result__extensions__" + CONTEXT_EXTENSION_TIME, "event__currentTime", ), ConversionItem( - "result__extensions__" + VIDEO_EXTENSION_PROGRESS, + "result__extensions__" + CONTEXT_EXTENSION_PROGRESS, None, # Set the video progress to null by default. # This information is mandatory in the xAPI template @@ -167,7 +167,7 @@ def _get_conversion_items(self): lambda _: 0.0, ), ConversionItem( - "context__extensions__" + VIDEO_EXTENSION_LENGTH, + "context__extensions__" + CONTEXT_EXTENSION_LENGTH, None, # Set the video length to null by default. # This information is mandatory in the xAPI template @@ -175,7 +175,7 @@ def _get_conversion_items(self): lambda _: 0.0, ), ConversionItem( - "context__extensions__" + VIDEO_EXTENSION_SESSION_ID, + "context__extensions__" + CONTEXT_EXTENSION_SESSION_ID, "session", ), }, @@ -194,15 +194,15 @@ def _get_conversion_items(self): return conversion_items.union( { ConversionItem( - "result__extensions__" + VIDEO_EXTENSION_TIME_FROM, + "result__extensions__" + CONTEXT_EXTENSION_TIME_FROM, "event__old_time", ), ConversionItem( - "result__extensions__" + VIDEO_EXTENSION_TIME_TO, + "result__extensions__" + CONTEXT_EXTENSION_TIME_TO, "event__new_time", ), ConversionItem( - "context__extensions__" + VIDEO_EXTENSION_SESSION_ID, + "context__extensions__" + CONTEXT_EXTENSION_SESSION_ID, "session", ), }, diff --git a/src/ralph/models/xapi/fields/__init__.py b/src/ralph/models/xapi/base/__init__.py similarity index 100% rename from src/ralph/models/xapi/fields/__init__.py rename to src/ralph/models/xapi/base/__init__.py diff --git a/src/ralph/models/xapi/base/agents.py b/src/ralph/models/xapi/base/agents.py new file mode 100644 index 000000000..1225aff9f --- /dev/null +++ b/src/ralph/models/xapi/base/agents.py @@ -0,0 +1,95 @@ +"""Base xAPI `Agent` definitions.""" + +from typing import Optional, Union + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +from pydantic import AnyUrl, StrictStr, constr + +from ..config import BaseModelWithConfig +from .common import IRI, MailtoEmail + + +class BaseXapiAgentAccount(BaseModelWithConfig): + """Pydantic model for `Agent` type account` property. + + Attributes: + homePage (IRI): Consists of the home page of the account's service provider. + name (str): Consists of the unique id or name of the Actor's account. + """ + + homePage: IRI + name: StrictStr + + +class BaseXapiAgentCommonProperties(BaseModelWithConfig): + """Pydantic model for core `Agent` type property. + + It defines who performed the action. + + Attributes: + objectType (str): Consists of the value `Agent`. + name (str): Consists of the full name of the Agent. + """ + + objectType: Optional[Literal["Agent"]] + name: Optional[StrictStr] + + +class BaseXapiAgentWithMboxIFI(BaseXapiAgentCommonProperties): + """Pydantic model for `Agent` type property. + + It defines a mailto Inverse Functional Identifier. + + Attributes: + mbox (MailtoEmail): Consists of the Agent's email address. + """ + + mbox: MailtoEmail + + +class BaseXapiAgentWithMboxSha1SumIFI(BaseXapiAgentCommonProperties): + """Pydantic model for `Agent` type property. + + It defines a hash Inverse Functional Identifier. + + Attributes: + mbox_sha1sum (str): Consists of the SHA1 hash of the Agent's email address. + """ + + mbox_sha1sum: constr(regex=r"^[0-9a-f]{40}$") # noqa:F722 + + +class BaseXapiAgentWithOpenIdIFI(BaseXapiAgentCommonProperties): + """Pydantic model for `Agent` type property. + + It defines an OpenID Inverse Functional Identifier. + + Attributes: + openid (URI): Consists of an openID that uniquely identifies the Agent. + """ + + openid: AnyUrl + + +class BaseXapiAgentWithAccountIFI(BaseXapiAgentCommonProperties): + """Pydantic model for `Agent` type property. + + It defines an account Inverse Functional Identifier. + + Attributes: + account (dict): See BaseXapiAgentAccount. + """ + + account: BaseXapiAgentAccount + + +BaseXapiAgent = Union[ + BaseXapiAgentWithMboxIFI, + BaseXapiAgentWithMboxSha1SumIFI, + BaseXapiAgentWithOpenIdIFI, + BaseXapiAgentWithAccountIFI, +] diff --git a/src/ralph/models/xapi/fields/attachments.py b/src/ralph/models/xapi/base/attachments.py similarity index 85% rename from src/ralph/models/xapi/fields/attachments.py rename to src/ralph/models/xapi/base/attachments.py index 9bb4e3124..91ffdf93a 100644 --- a/src/ralph/models/xapi/fields/attachments.py +++ b/src/ralph/models/xapi/base/attachments.py @@ -1,4 +1,4 @@ -"""Common xAPI attachments field definitions.""" +"""Base xAPI `Attachments` definitions.""" from typing import Optional @@ -8,8 +8,8 @@ from .common import IRI, LanguageMap -class AttachmentField(BaseModelWithConfig): - """Pydantic model for `attachment` field. +class BaseXapiAttachment(BaseModelWithConfig): + """Pydantic model for `attachment` property. Attributes: usageType (IRI): Identifies the usage of this Attachment. diff --git a/src/ralph/models/xapi/fields/common.py b/src/ralph/models/xapi/base/common.py similarity index 97% rename from src/ralph/models/xapi/fields/common.py rename to src/ralph/models/xapi/base/common.py index 5c4165c30..813da9446 100644 --- a/src/ralph/models/xapi/fields/common.py +++ b/src/ralph/models/xapi/base/common.py @@ -1,4 +1,4 @@ -"""Common xAPI field definitions.""" +"""Common for xAPI base definitions.""" from typing import Dict diff --git a/src/ralph/models/xapi/base/contexts.py b/src/ralph/models/xapi/base/contexts.py new file mode 100644 index 000000000..92ffdebac --- /dev/null +++ b/src/ralph/models/xapi/base/contexts.py @@ -0,0 +1,57 @@ +"""Base xAPI `Context` definitions.""" + +from typing import Dict, List, Optional, Union +from uuid import UUID + +from pydantic import StrictStr + +from ..config import BaseModelWithConfig +from .agents import BaseXapiAgent +from .common import IRI, LanguageTag +from .groups import BaseXapiGroup +from .unnested_objects import BaseXapiActivity, BaseXapiStatementRef + + +class BaseXapiContextContextActivities(BaseModelWithConfig): + """Pydantic model for context `contextActivities` property. + + Attributes: + parent (dict, list): An Activity with a direct relation to the statement's + Activity. + grouping (dict, list): An Activity with an indirect relation to the statement's + Activity. + category (dict, list): An Activity used to categorize the Statement. + other (dict, list): A contextActivity that doesn't fit one of the other + properties. + """ + + parent: Optional[Union[BaseXapiActivity, List[BaseXapiActivity]]] + grouping: Optional[Union[BaseXapiActivity, List[BaseXapiActivity]]] + category: Optional[Union[BaseXapiActivity, List[BaseXapiActivity]]] + other: Optional[Union[BaseXapiActivity, List[BaseXapiActivity]]] + + +class BaseXapiContext(BaseModelWithConfig): + """Pydantic model for `context` property. + + Attributes: + registration (UUID): The registration that the Statement is associated with. + instructor (dict): The instructor that the Statement relates to. + team (dict): The team that this Statement relates to. + contextActivities (dict): See BaseXapiContextContextActivities. + revision (str): The revision of the activity associated with this Statement. + platform (str): The platform where the learning activity took place. + language (dict): The language in which the experience occurred. + statement (dict): Another Statement giving context for this Statement. + extensions (dict): Consists of an dictionary of other properties as needed. + """ + + registration: Optional[UUID] + instructor: Optional[BaseXapiAgent] + team: Optional[BaseXapiGroup] + contextActivities: Optional[BaseXapiContextContextActivities] + revision: Optional[StrictStr] + platform: Optional[StrictStr] + language: Optional[LanguageTag] + statement: Optional[BaseXapiStatementRef] + extensions: Optional[Dict[IRI, Union[str, int, bool, list, dict, None]]] diff --git a/src/ralph/models/xapi/base/groups.py b/src/ralph/models/xapi/base/groups.py new file mode 100644 index 000000000..6ec9fbfe4 --- /dev/null +++ b/src/ralph/models/xapi/base/groups.py @@ -0,0 +1,102 @@ +"""Base xAPI `Group` definitions.""" + +from typing import List, Optional, Union + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +from pydantic import StrictStr + +from ..config import BaseModelWithConfig +from .agents import ( + BaseXapiAgent, + BaseXapiAgentWithMboxIFI, + BaseXapiAgentWithMboxSha1SumIFI, + BaseXapiAgentWithOpenIdIFI, +) + + +class BaseXapiGroupCommonProperties(BaseModelWithConfig): + """Pydantic model for core `Group` type property. + + It is defined the Group which performed the action. + + Attributes: + objectType (str): Consists of the value `Group`. + name (str): Consists of the full name of the Group. + member (list): Consist of a list of the members of this Group. + """ + + objectType: Literal["Group"] + name: Optional[StrictStr] + + +class BaseXapiAnonymousGroup(BaseXapiGroupCommonProperties): + """Pydantic model for `Group` type property. + + It is defined for Anonymous Group type. + + Attributes: + member (list): Consist of a list of the members of this Group. + """ + + member: List[BaseXapiAgent] + + +class BaseXapiIdentifiedGroup(BaseXapiGroupCommonProperties): + """Pydantic model for `Group` type property. + + It is defined for Identified Group type. + + Attributes: + member (list): Consist of a list of the members of this Group. + """ + + member: Optional[List[BaseXapiAgent]] + + +class BaseXapiIdentifiedGroupWithMboxIFI( + BaseXapiIdentifiedGroup, BaseXapiAgentWithMboxIFI +): + """Pydantic model for `Group` type property. + + It is defined for group type with a mailto IFI. + """ + + +class BaseXapiIdentifiedGroupWithMboxSha1SumIFI( + BaseXapiIdentifiedGroup, BaseXapiAgentWithMboxSha1SumIFI +): + """Pydantic model for `Group` type property. + + It is defined for group type with a hash IFI. + """ + + +class BaseXapiIdentifiedGroupWithOpenIdIFI( + BaseXapiIdentifiedGroup, BaseXapiAgentWithOpenIdIFI +): + """Pydantic model for `Group` type property. + + It is defined for group type with an openID IFI. + """ + + +class BaseXapiIdentifiedGroupWithAccountIFI( + BaseXapiIdentifiedGroup, BaseXapiAgentWithOpenIdIFI +): + """Pydantic model for `Group` type property. + + It is defined for group type with an account IFI. + """ + + +BaseXapiGroup = Union[ + BaseXapiAnonymousGroup, + BaseXapiIdentifiedGroupWithMboxIFI, + BaseXapiIdentifiedGroupWithMboxSha1SumIFI, + BaseXapiIdentifiedGroupWithOpenIdIFI, + BaseXapiIdentifiedGroupWithAccountIFI, +] diff --git a/src/ralph/models/xapi/base/objects.py b/src/ralph/models/xapi/base/objects.py new file mode 100644 index 000000000..1d35de573 --- /dev/null +++ b/src/ralph/models/xapi/base/objects.py @@ -0,0 +1,49 @@ +"""Base xAPI `Object` definitions (2).""" + +# Nota bene: we split object definitions into `objects.py` and `unnested_objects.py` +# because of the circular dependency : objects -> context -> objects. + +from datetime import datetime +from typing import List, Optional, Union + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +from ..config import BaseModelWithConfig +from .agents import BaseXapiAgent +from .attachments import BaseXapiAttachment +from .contexts import BaseXapiContext +from .groups import BaseXapiGroup +from .results import BaseXapiResult +from .unnested_objects import BaseXapiUnnestedObject +from .verbs import BaseXapiVerb + + +class BaseXapiSubStatement(BaseModelWithConfig): + """Pydantic model for `SubStatement` type property. + + Attributes: + actor (dict): See BaseXapiAgent and BaseXapiGroup. + verb (dict): See BaseXapiVerb. + object (dict): See BaseXapiUnnestedObject. + objecType (dict): Consists of the value `SubStatement`. + """ + + actor: Union[BaseXapiAgent, BaseXapiGroup] + verb: BaseXapiVerb + object: BaseXapiUnnestedObject + objectType: Literal["SubStatement"] + result: Optional[BaseXapiResult] + context: Optional[BaseXapiContext] + timestamp: Optional[datetime] + attachments: Optional[List[BaseXapiAttachment]] + + +BaseXapiObject = Union[ + BaseXapiUnnestedObject, + BaseXapiSubStatement, + BaseXapiAgent, + BaseXapiGroup, +] diff --git a/src/ralph/models/xapi/fields/results.py b/src/ralph/models/xapi/base/results.py similarity index 82% rename from src/ralph/models/xapi/fields/results.py rename to src/ralph/models/xapi/base/results.py index d5778440b..1064880f6 100644 --- a/src/ralph/models/xapi/fields/results.py +++ b/src/ralph/models/xapi/base/results.py @@ -1,4 +1,4 @@ -"""Common xAPI result field definitions.""" +"""Base xAPI `Result` definitions.""" from datetime import timedelta from decimal import Decimal @@ -10,8 +10,8 @@ from .common import IRI -class ScoreResultField(BaseModelWithConfig): - """Pydantic model for `results.score` field. +class BaseXapiResultScore(BaseModelWithConfig): + """Pydantic model for result `score` property. Attributes: scaled (int): Consists of the normalized score related to the experience. @@ -45,19 +45,20 @@ def check_raw_min_max_relation(cls, values): return values -class ResultField(BaseModelWithConfig): - """Pydantic model for `result` field. +class BaseXapiResult(BaseModelWithConfig): + """Pydantic model for `result` property. Attributes: - score (ScoreResultField): See ScoreResultField. + score (dict): See BaseXapiResultScore. success (bool): Indicates whether the attempt on the Activity was successful. completion (bool): Indicates whether the Activity was completed. response (str): Consists of the response for the given Activity. - duration (str): Consists of the duration over which the Statement occurred. + duration (timedelta): Consists of the duration over which the Statement + occurred. extensions (dict): Consists of a dictionary of other properties as needed. """ - score: Optional[ScoreResultField] + score: Optional[BaseXapiResultScore] success: Optional[StrictBool] completion: Optional[StrictBool] response: Optional[StrictStr] diff --git a/src/ralph/models/xapi/base.py b/src/ralph/models/xapi/base/statements.py similarity index 58% rename from src/ralph/models/xapi/base.py rename to src/ralph/models/xapi/base/statements.py index 4aff165b3..7e272961a 100644 --- a/src/ralph/models/xapi/base.py +++ b/src/ralph/models/xapi/base/statements.py @@ -1,48 +1,49 @@ -"""Base xAPI model definition.""" +"""Base xAPI `Statement` definitions.""" from datetime import datetime -from typing import List, Optional +from typing import List, Optional, Union from uuid import UUID from pydantic import constr, root_validator -from .config import BaseModelWithConfig -from .fields.actors import ActorField -from .fields.attachments import AttachmentField -from .fields.contexts import ContextField -from .fields.objects import ObjectField -from .fields.results import ResultField -from .fields.verbs import VerbField +from ..config import BaseModelWithConfig +from .agents import BaseXapiAgent +from .attachments import BaseXapiAttachment +from .contexts import BaseXapiContext +from .groups import BaseXapiGroup +from .objects import BaseXapiObject +from .results import BaseXapiResult +from .verbs import BaseXapiVerb -class BaseXapiModel(BaseModelWithConfig): - """Pydantic model for base statements. +class BaseXapiStatement(BaseModelWithConfig): + """Pydantic model for base xAPI statements. Attributes: id (UUID): Consists of a generated UUID string from the source event string. - actor (ActorField): Consists of a definition of who performed the action. - verb (VerbField): Consists of the action between an Actor and an Activity. - object (ObjectField): Consists of a definition of the thing that was acted on. - result (ResultField): Consists of the outcome related to the Statement. - context (ContextField): Consists of contextual information for the Statement. + actor (dict): Consists of a definition of who performed the action. + verb (dict): Consists of the action between an Actor and an Activity. + object (dict): Consists of a definition of the thing that was acted on. + result (dict): Consists of the outcome related to the Statement. + context (dict): Consists of contextual information for the Statement. timestamp (datetime): Consists of the timestamp of when the event occurred. stored (datetime): Consists of the timestamp of when the event was recorded. - authority (ActorField): Consists of the Actor asserting this Statement is true. + authority (dict): Consists of the Actor asserting this Statement is true. version (str): Consists of the associated xAPI version of the Statement. - attachments (List): Consists of a list of Attachments. + attachments (list): Consists of a list of attachments. """ id: Optional[UUID] - actor: ActorField - verb: VerbField - object: ObjectField - result: Optional[ResultField] - context: Optional[ContextField] + actor: Union[BaseXapiAgent, BaseXapiGroup] + verb: BaseXapiVerb + object: BaseXapiObject + result: Optional[BaseXapiResult] + context: Optional[BaseXapiContext] timestamp: Optional[datetime] stored: Optional[datetime] - authority: Optional[ActorField] + authority: Optional[Union[BaseXapiAgent, BaseXapiGroup]] version: constr(regex=r"^1\.0\.[0-9]+$") = "1.0.0" # noqa:F722 - attachments: Optional[List[AttachmentField]] + attachments: Optional[List[BaseXapiAttachment]] @root_validator(pre=True) @classmethod diff --git a/src/ralph/models/xapi/fields/unnested_objects.py b/src/ralph/models/xapi/base/unnested_objects.py similarity index 69% rename from src/ralph/models/xapi/fields/unnested_objects.py rename to src/ralph/models/xapi/base/unnested_objects.py index c5a077087..418c65c2c 100644 --- a/src/ralph/models/xapi/fields/unnested_objects.py +++ b/src/ralph/models/xapi/base/unnested_objects.py @@ -1,4 +1,4 @@ -"""Common xAPI object field definitions.""" +"""Base xAPI `Object` definitions (1).""" from typing import Dict, List, Optional, Union @@ -15,15 +15,15 @@ from .common import IRI, LanguageMap -class ObjectDefinitionField(BaseModelWithConfig): - """Pydantic model for `object.definition` field. +class BaseXapiActivityDefinition(BaseModelWithConfig): + """Pydantic model for `Activity` type `definition` property. Attributes: name (LanguageMap): Consists of the human readable/visual name of the Activity. description (LanguageMap): Consists of a description of the Activity. type (IRI): Consists of the type of the Activity. moreInfo (URL): Consists of an URL to a document about the Activity. - extensions (Dict): Consists of a dictionary of other properties as needed. + extensions (dict): Consists of a dictionary of other properties as needed. """ name: Optional[LanguageMap] @@ -33,7 +33,7 @@ class ObjectDefinitionField(BaseModelWithConfig): extensions: Optional[Dict[IRI, Union[str, int, bool, list, dict, None]]] -class InteractionComponent(BaseModelWithConfig): +class BaseXapiInteractionComponent(BaseModelWithConfig): """Pydantic model for an interaction component. Attributes: @@ -45,8 +45,8 @@ class InteractionComponent(BaseModelWithConfig): description: Optional[LanguageMap] -class InteractionObjectDefinitionField(ObjectDefinitionField): - """Pydantic model for `object.definition` field. +class BaseXapiActivityInteractionDefinition(BaseXapiActivityDefinition): + """Pydantic model for `Activity` type `definition` property. It is defined for field with interaction properties. @@ -73,11 +73,11 @@ class InteractionObjectDefinitionField(ObjectDefinitionField): "other", ] correctResponsesPattern: Optional[List[StrictStr]] - choices: Optional[List[InteractionComponent]] - scale: Optional[List[InteractionComponent]] - source: Optional[List[InteractionComponent]] - target: Optional[List[InteractionComponent]] - steps: Optional[List[InteractionComponent]] + choices: Optional[List[BaseXapiInteractionComponent]] + scale: Optional[List[BaseXapiInteractionComponent]] + source: Optional[List[BaseXapiInteractionComponent]] + target: Optional[List[BaseXapiInteractionComponent]] + steps: Optional[List[BaseXapiInteractionComponent]] @validator("choices", "scale", "source", "target", "steps") @classmethod @@ -87,26 +87,28 @@ def check_unique_ids(cls, value): raise ValueError("Duplicate InteractionComponents are not valid") -class ActivityObjectField(BaseModelWithConfig): - """Pydantic model for `object` field. - - It is defined for Activity type. +class BaseXapiActivity(BaseModelWithConfig): + """Pydantic model for `Activity` type property. Attributes: - objectType (str): Consists of the value `Activity`. id (IRI): Consists of an identifier for a single unique Activity. - definition (dict): See ObjectDefinitionField. + objectType (str): Consists of the value `Activity`. + definition (dict): See BaseXapiActivityDefinition and + BaseXapiActivityInteractionDefinition. """ id: IRI objectType: Optional[Literal["Activity"]] - definition: Optional[Union[ObjectDefinitionField, InteractionObjectDefinitionField]] - + definition: Optional[ + Union[ + BaseXapiActivityDefinition, + BaseXapiActivityInteractionDefinition, + ] + ] -class StatementRefObjectField(BaseModelWithConfig): - """Pydantic model for `object` field. - It is defined for StatementRef type. +class BaseXapiStatementRef(BaseModelWithConfig): + """Pydantic model for `StatementRef` type property. Attributes: objectType (str): Consists of the value `StatementRef`. @@ -117,4 +119,4 @@ class StatementRefObjectField(BaseModelWithConfig): objectType: Literal["StatementRef"] -UnnestedObjectField = Union[ActivityObjectField, StatementRefObjectField] +BaseXapiUnnestedObject = Union[BaseXapiActivity, BaseXapiStatementRef] diff --git a/src/ralph/models/xapi/base/verbs.py b/src/ralph/models/xapi/base/verbs.py new file mode 100644 index 000000000..1fa31b53f --- /dev/null +++ b/src/ralph/models/xapi/base/verbs.py @@ -0,0 +1,18 @@ +"""Base xAPI `Verb` definitions.""" + +from typing import Optional + +from ..config import BaseModelWithConfig +from .common import IRI, LanguageMap + + +class BaseXapiVerb(BaseModelWithConfig): + """Pydantic model for `verb` property. + + Attributes: + id (IRI): Consists of an identifier for the verb. + display (LanguageMap): Consists of a human readable representation of the verb. + """ + + id: IRI + display: Optional[LanguageMap] diff --git a/src/ralph/models/xapi/navigation/fields/__init__.py b/src/ralph/models/xapi/concepts/__init__.py similarity index 100% rename from src/ralph/models/xapi/navigation/fields/__init__.py rename to src/ralph/models/xapi/concepts/__init__.py diff --git a/src/ralph/models/xapi/video/fields/__init__.py b/src/ralph/models/xapi/concepts/activity_types/__init__.py similarity index 100% rename from src/ralph/models/xapi/video/fields/__init__.py rename to src/ralph/models/xapi/concepts/activity_types/__init__.py diff --git a/src/ralph/models/xapi/concepts/activity_types/acrossx_profile.py b/src/ralph/models/xapi/concepts/activity_types/acrossx_profile.py new file mode 100644 index 000000000..773b7e056 --- /dev/null +++ b/src/ralph/models/xapi/concepts/activity_types/acrossx_profile.py @@ -0,0 +1,26 @@ +"""`AcrossX Profile` activity types definitions.""" + +from ...base.unnested_objects import BaseXapiActivity, BaseXapiActivityDefinition +from ..constants.acrossx_profile import ACTIVITY_ID_MESSAGE + + +# Message +class MessageActivityDefinition(BaseXapiActivityDefinition): + """Pydantic model for message `Activity` type `definition` property. + + Attributes: + type (str): Consists of the value + `https://w3id.org/xapi/acrossx/activities/message`. + """ + + type: ACTIVITY_ID_MESSAGE = ACTIVITY_ID_MESSAGE.__args__[0] + + +class MessageActivity(BaseXapiActivity): + """Pydantic model for message `Activity` type. + + Attributes: + definition (dict): see MessageActivityDefinition. + """ + + definition: MessageActivityDefinition = MessageActivityDefinition() diff --git a/src/ralph/models/xapi/concepts/activity_types/activity_streams_vocabulary.py b/src/ralph/models/xapi/concepts/activity_types/activity_streams_vocabulary.py new file mode 100644 index 000000000..d0b33dd64 --- /dev/null +++ b/src/ralph/models/xapi/concepts/activity_types/activity_streams_vocabulary.py @@ -0,0 +1,24 @@ +"""`Activity streams vocabulary` activity types definitions.""" + +from ...base.unnested_objects import BaseXapiActivity, BaseXapiActivityDefinition +from ..constants.activity_streams_vocabulary import ACTIVITY_ID_PAGE + + +class PageActivityDefinition(BaseXapiActivityDefinition): + """Pydantic model for page `Activity` type `definition` property. + + Attributes: + type (str): Consists of the value `http://activitystrea.ms/schema/1.0/page`. + """ + + type: ACTIVITY_ID_PAGE = ACTIVITY_ID_PAGE.__args__[0] + + +class PageActivity(BaseXapiActivity): + """Pydantic model for page `Activity` type. + + Attributes: + definition (dict): See PageActivityDefinition. + """ + + definition: PageActivityDefinition = PageActivityDefinition() diff --git a/src/ralph/models/xapi/video/fields/adl_vocabulary.py b/src/ralph/models/xapi/concepts/activity_types/adl_vocabulary.py similarity index 100% rename from src/ralph/models/xapi/video/fields/adl_vocabulary.py rename to src/ralph/models/xapi/concepts/activity_types/adl_vocabulary.py diff --git a/src/ralph/models/xapi/concepts/activity_types/scorm_profile.py b/src/ralph/models/xapi/concepts/activity_types/scorm_profile.py new file mode 100644 index 000000000..e8fde20fc --- /dev/null +++ b/src/ralph/models/xapi/concepts/activity_types/scorm_profile.py @@ -0,0 +1,26 @@ +"""`Scorm Profile` activity types definitions.""" + +from ...base.unnested_objects import BaseXapiActivity, BaseXapiActivityDefinition +from ..constants.scorm_profile import ACTIVITY_ID_CMI_INTERACTION + + +# CMI Interaction +class CMIInteractionInteraction(BaseXapiActivityDefinition): + """Pydantic model for CMI Interaction `Activity` type `definition` property. + + Attributes: + type (str): Consists of the value + `http://adlnet.gov/expapi/activities/cmi.interaction`. + """ + + type: ACTIVITY_ID_CMI_INTERACTION = ACTIVITY_ID_CMI_INTERACTION.__args__[0] + + +class CMIInteractionActivity(BaseXapiActivity): + """Pydantic model for CMI Interaction `Activity` type. + + Attributes: + definition (dict): see CMIInteractionInteraction. + """ + + definition: CMIInteractionInteraction = CMIInteractionInteraction() diff --git a/src/ralph/models/xapi/concepts/activity_types/video.py b/src/ralph/models/xapi/concepts/activity_types/video.py new file mode 100644 index 000000000..8ac8eefe8 --- /dev/null +++ b/src/ralph/models/xapi/concepts/activity_types/video.py @@ -0,0 +1,35 @@ +"""`Video` activity types definitions.""" + +from typing import Dict, Optional + +from ...base.unnested_objects import BaseXapiActivity, BaseXapiActivityDefinition +from ...constants import LANG_EN_US_DISPLAY +from ..constants.video import ACTIVITY_ID_VIDEO + +# Video + + +class VideoActivityDefinition(BaseXapiActivityDefinition): + """Pydantic model for video `Activity` type `definition` property. + + Attributes: + type (str): Consists of the value + `https://w3id.org/xapi/video/activity-type/video`. + """ + + type: ACTIVITY_ID_VIDEO = ACTIVITY_ID_VIDEO.__args__[0] + + +class VideoActivity(BaseXapiActivity): + """Pydantic model for video `Activity` type. + + WARNING: Contains an optional name property, this is not a violation of + conformity but goes against xAPI specification recommendations. + + Attributes: + name (dict): Consists of the dictionary `{"en-US": }`. + definition (dict): See VideoActivityDefinition. + """ + + name: Optional[Dict[LANG_EN_US_DISPLAY, str]] + definition: VideoActivityDefinition = VideoActivityDefinition() diff --git a/src/ralph/models/xapi/concepts/activity_types/virtual_classroom.py b/src/ralph/models/xapi/concepts/activity_types/virtual_classroom.py new file mode 100644 index 000000000..91b3f740b --- /dev/null +++ b/src/ralph/models/xapi/concepts/activity_types/virtual_classroom.py @@ -0,0 +1,29 @@ +"""`Virtual classroom` activity types definitions.""" + +from ...base.unnested_objects import BaseXapiActivity, BaseXapiActivityDefinition +from ..constants.virtual_classroom import ACTIVITY_ID_VIRTUAL_CLASSROOM + +# Virtual classroom + + +class VirtualClassroomActivityDefinition(BaseXapiActivityDefinition): + """Pydantic model for virtual classroom `Activity` type `definition` property. + + Attributes: + type (str): Consists of the value + `https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom`. + """ + + type: ACTIVITY_ID_VIRTUAL_CLASSROOM = ACTIVITY_ID_VIRTUAL_CLASSROOM.__args__[0] + + +class VirtualClassroomActivity(BaseXapiActivity): + """Pydantic model for virtual classroom `Activity` type. + + Attributes: + definition (dict): See VirtualClassroomActivityDefinition. + """ + + definition: VirtualClassroomActivityDefinition = ( + VirtualClassroomActivityDefinition() + ) diff --git a/src/ralph/models/xapi/concepts/constants/__init__.py b/src/ralph/models/xapi/concepts/constants/__init__.py new file mode 100644 index 000000000..6e031999e --- /dev/null +++ b/src/ralph/models/xapi/concepts/constants/__init__.py @@ -0,0 +1 @@ +# noqa: D104 diff --git a/src/ralph/models/xapi/concepts/constants/acrossx_profile.py b/src/ralph/models/xapi/concepts/constants/acrossx_profile.py new file mode 100644 index 000000000..830458bb0 --- /dev/null +++ b/src/ralph/models/xapi/concepts/constants/acrossx_profile.py @@ -0,0 +1,22 @@ +"""Constants for `AcrossX Profile` xAPI profile.""" + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +# Activity IDs +ACTIVITY_ID_MESSAGE = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/acrossx/activities/message" +] + +# Verb IDs +VERB_ID_POSTED = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/acrossx/verbs/posted" +] + +# Verb displays +VERB_DISPLAY_POSTED = Literal["posted"] # pylint:disable=invalid-name + +# Context extensions +CONTEXT_EXTENSION_SCHOOL_ID = "https://w3id.org/xapi/acrossx/extensions/school" diff --git a/src/ralph/models/xapi/concepts/constants/activity_streams_vocabulary.py b/src/ralph/models/xapi/concepts/constants/activity_streams_vocabulary.py new file mode 100644 index 000000000..6fde803d9 --- /dev/null +++ b/src/ralph/models/xapi/concepts/constants/activity_streams_vocabulary.py @@ -0,0 +1,19 @@ +"""Constants for `Activity Streams Vocabulary` xAPI profile.""" + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +# Activity IDs +ACTIVITY_ID_PAGE = Literal[ # pylint:disable=invalid-name + "http://activitystrea.ms/schema/1.0/page" +] + +# Verb IDs +VERB_ID_JOIN = Literal["http://activitystrea.ms/join"] # pylint:disable=invalid-name +VERB_ID_LEAVE = Literal["http://activitystrea.ms/leave"] # pylint:disable=invalid-name + +# Verb displays +VERB_DISPLAY_JOINED = Literal["joined"] # pylint:disable=invalid-name +VERB_DISPLAY_LEFT = Literal["left"] # pylint:disable=invalid-name diff --git a/src/ralph/models/xapi/concepts/constants/adl_vocabulary.py b/src/ralph/models/xapi/concepts/constants/adl_vocabulary.py new file mode 100644 index 000000000..fbe18272f --- /dev/null +++ b/src/ralph/models/xapi/concepts/constants/adl_vocabulary.py @@ -0,0 +1,18 @@ +"""Constants for `ADL Vocabulary` xAPI profile.""" + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +# Verb IDs +VERB_ID_ASKED = Literal[ # pylint:disable=invalid-name + "http://adlnet.gov/expapi/verbs/asked" +] +VERB_ID_ANSWERED = Literal[ # pylint:disable=invalid-name + "http://adlnet.gov/expapi/verbs/answered" +] + +# Verb displays +VERB_DISPLAY_ASKED = Literal["asked"] # pylint:disable=invalid-name +VERB_DISPLAY_ANSWERED = Literal["answered"] # pylint:disable=invalid-name diff --git a/src/ralph/models/xapi/concepts/constants/cmi5_profile.py b/src/ralph/models/xapi/concepts/constants/cmi5_profile.py new file mode 100644 index 000000000..ed8aab98c --- /dev/null +++ b/src/ralph/models/xapi/concepts/constants/cmi5_profile.py @@ -0,0 +1,6 @@ +"""Constants for `cmi5 Profile` xAPI profile.""" + +# Context extension IDs +CONTEXT_EXTENSION_SESSION_ID = ( # pylint:disable=invalid-name + "https://w3id.org/xapi/cmi5/context/extensions/sessionid" +) diff --git a/src/ralph/models/xapi/navigation/fields/constants/quiz.py b/src/ralph/models/xapi/concepts/constants/quiz.py similarity index 100% rename from src/ralph/models/xapi/navigation/fields/constants/quiz.py rename to src/ralph/models/xapi/concepts/constants/quiz.py diff --git a/src/ralph/models/xapi/concepts/constants/scorm_profile.py b/src/ralph/models/xapi/concepts/constants/scorm_profile.py new file mode 100644 index 000000000..c7de2e287 --- /dev/null +++ b/src/ralph/models/xapi/concepts/constants/scorm_profile.py @@ -0,0 +1,39 @@ +"""Constants for `Scorm Profile` xAPI profile.""" + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +# Activity IDs +ACTIVITY_ID_CMI_INTERACTION = Literal[ # pylint:disable=invalid-name + "http://adlnet.gov/expapi/activities/cmi.interaction" +] + +ACTIVITY_ID_PROFILE = Literal[ # pylint:disable=invalid-name + "http://adlnet.gov/expapi/activities/profile" +] + +# Verb IDs +VERB_ID_COMPLETED = Literal[ # pylint:disable=invalid-name + "http://adlnet.gov/expapi/verbs/completed" +] +VERB_ID_INITIALIZED = Literal[ # pylint:disable=invalid-name + "http://adlnet.gov/expapi/verbs/initialized" +] +VERB_ID_INTERACTED = Literal[ # pylint:disable=invalid-name + "http://adlnet.gov/expapi/verbs/interacted" +] +VERB_ID_TERMINATED = Literal[ # pylint:disable=invalid-name + "http://adlnet.gov/expapi/verbs/terminated" +] + +# Verb displays +VERB_DISPLAY_COMPLETED = Literal["completed"] # pylint:disable=invalid-name +VERB_DISPLAY_INITIALIZED = Literal["initialized"] # pylint:disable=invalid-name +VERB_DISPLAY_INTERACTED = Literal["interacted"] # pylint:disable=invalid-name +VERB_DISPLAY_TERMINATED = Literal["terminated"] # pylint:disable=invalid-name + +# Context extensions +CONTEXT_EXTENSION_COURSE_ID = "http://adlnet.gov/expapi/activities/course" +CONTEXT_EXTENSION_MODULE_ID = "http://adlnet.gov/expapi/activities/module" diff --git a/src/ralph/models/xapi/concepts/constants/tincan_vocabulary.py b/src/ralph/models/xapi/concepts/constants/tincan_vocabulary.py new file mode 100644 index 000000000..3ed35a158 --- /dev/null +++ b/src/ralph/models/xapi/concepts/constants/tincan_vocabulary.py @@ -0,0 +1,20 @@ +"""Constants for `TinCan Vocabulary` xAPI profile.""" + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + + +# Verb IDs +VERB_ID_VIEWED = Literal[ # pylint:disable=invalid-name + "http://id.tincanapi.com/verb/viewed" +] + +# Verb displays +VERB_DISPLAY_VIEWED = Literal["viewed"] # pylint:disable=invalid-name + +# Context extension IRIs +CONTEXT_EXTENSION_PLANNED_DURATION = ( # pylint:disable=invalid-name + "http://id.tincanapi.com/extension/planned-duration" +) diff --git a/src/ralph/models/xapi/concepts/constants/video.py b/src/ralph/models/xapi/concepts/constants/video.py new file mode 100644 index 000000000..1780e87c0 --- /dev/null +++ b/src/ralph/models/xapi/concepts/constants/video.py @@ -0,0 +1,61 @@ +"""Constants for `Video` xAPI profile.""" + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +# Profile ID +PROFILE_ID_VIDEO = Literal["https://w3id.org/xapi/video"] # pylint:disable=invalid-name + +# Activity IDs +ACTIVITY_ID_VIDEO = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/video/activity-type/video" +] + +# Verb IDs +VERB_ID_PAUSED = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/video/verbs/paused" +] +VERB_ID_PLAYED = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/video/verbs/played" +] +VERB_ID_SEEKED = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/video/verbs/seeked" +] + +# Verb displays +VERB_DISPLAY_PAUSED = Literal["paused"] # pylint:disable=invalid-name +VERB_DISPLAY_PLAYED = Literal["played"] # pylint:disable=invalid-name +VERB_DISPLAY_SEEKED = Literal["seeked"] # pylint:disable=invalid-name + + +# Extensions +CONTEXT_EXTENSION_CC_SUBTITLE_LANG = ( + "https://w3id.org/xapi/video/extensions/cc-subtitle-lang" +) +CONTEXT_EXTENSION_CC_ENABLED = "https://w3id.org/xapi/video/extensions/cc-enabled" +CONTEXT_EXTENSION_COMPLETION_THRESHOLD = ( + "https://w3id.org/xapi/video/extensions/completion-threshold" +) +CONTEXT_EXTENSION_FRAME_RATE = "https://w3id.org/xapi/video/extensions/frame-rate" +CONTEXT_EXTENSION_FULL_SCREEN = "https://w3id.org/xapi/video/extensions/full-screen" +CONTEXT_EXTENSION_LENGTH = "https://w3id.org/xapi/video/extensions/length" +CONTEXT_EXTENSION_PLAYED_SEGMENTS = ( + "https://w3id.org/xapi/video/extensions/played-segments" +) +CONTEXT_EXTENSION_PROGRESS = "https://w3id.org/xapi/video/extensions/progress" +CONTEXT_EXTENSION_QUALITY = "https://w3id.org/xapi/video/extensions/quality" + +CONTEXT_EXTENSION_SCREEN_SIZE = "https://w3id.org/xapi/video/extensions/screen-size" +CONTEXT_EXTENSION_SESSION_ID = "https://w3id.org/xapi/video/extensions/session-id" +CONTEXT_EXTENSION_SPEED = "https://w3id.org/xapi/video/extensions/speed" +CONTEXT_EXTENSION_TIME = "https://w3id.org/xapi/video/extensions/time" +CONTEXT_EXTENSION_TIME_FROM = "https://w3id.org/xapi/video/extensions/time-from" +CONTEXT_EXTENSION_TIME_TO = "https://w3id.org/xapi/video/extensions/time-to" +CONTEXT_EXTENSION_TRACK = "https://w3id.org/xapi/video/extensions/track" +CONTEXT_EXTENSION_USER_AGENT = "https://w3id.org/xapi/video/extensions/user-agent" +CONTEXT_EXTENSION_VIDEO_PLAYBACK_SIZE = ( + "https://w3id.org/xapi/video/extensions/video-playback-size" +) +CONTEXT_EXTENSION_VOLUME = "https://w3id.org/xapi/video/extensions/volume" diff --git a/src/ralph/models/xapi/concepts/constants/virtual_classroom.py b/src/ralph/models/xapi/concepts/constants/virtual_classroom.py new file mode 100644 index 000000000..aef86d9db --- /dev/null +++ b/src/ralph/models/xapi/concepts/constants/virtual_classroom.py @@ -0,0 +1,53 @@ +"""Constants for `Virtual classroom` xAPI profile.""" + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +# Profile ID +PROFILE_ID_VIRTUAL_CLASSROOM = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/virtual-classroom" +] + +# Activity IDs +ACTIVITY_ID_VIRTUAL_CLASSROOM = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom" +] + +# Verb IDs +VERB_ID_MUTED = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/virtual-classroom/verbs/muted" +] +VERB_ID_UNMUTED = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/virtual-classroom/verbs/unmuted" +] +VERB_ID_SHARED_SCREEN = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/virtual-classroom/verbs/shared-screen" +] +VERB_ID_UNSHARED_SCREEN = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/virtual-classroom/verbs/unshared-screen" +] +VERB_ID_RAISED_HAND = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/virtual-classroom/verbs/raised-hand" +] +VERB_ID_LOWERED_HAND = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/virtual-classroom/verbs/lowered-hand" +] +VERB_ID_STARTED_CAMERA = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/virtual-classroom/verbs/started-camera" +] +VERB_ID_STOPPED_CAMERA = Literal[ # pylint:disable=invalid-name + "https://w3id.org/xapi/virtual-classroom/verbs/stopped-camera" +] + +# Verb displays + +VERB_DISPLAY_MUTED = Literal["muted"] # pylint:disable=invalid-name +VERB_DISPLAY_UNMUTED = Literal["unmuted"] # pylint:disable=invalid-name +VERB_DISPLAY_SHARED_SCREEN = Literal["shared screen"] # pylint:disable=invalid-name +VERB_DISPLAY_UNSHARED_SCREEN = Literal["unshared screen"] # pylint:disable=invalid-name +VERB_DISPLAY_RAISED_HAND = Literal["raised hand"] # pylint:disable=invalid-name +VERB_DISPLAY_LOWERED_HAND = Literal["lowered hand"] # pylint:disable=invalid-name +VERB_DISPLAY_STARTED_CAMERA = Literal["started camera"] # pylint:disable=invalid-name +VERB_DISPLAY_STOPPED_CAMERA = Literal["stopped camera"] # pylint:disable=invalid-name diff --git a/src/ralph/models/xapi/concepts/verbs/__init__.py b/src/ralph/models/xapi/concepts/verbs/__init__.py new file mode 100644 index 000000000..6e031999e --- /dev/null +++ b/src/ralph/models/xapi/concepts/verbs/__init__.py @@ -0,0 +1 @@ +# noqa: D104 diff --git a/src/ralph/models/xapi/concepts/verbs/acrossx_profile.py b/src/ralph/models/xapi/concepts/verbs/acrossx_profile.py new file mode 100644 index 000000000..c2aeec1b3 --- /dev/null +++ b/src/ralph/models/xapi/concepts/verbs/acrossx_profile.py @@ -0,0 +1,19 @@ +"""`AcrossX Profile` verbs definitions.""" + +from typing import Dict, Optional + +from ...base.verbs import BaseXapiVerb +from ...constants import LANG_EN_US_DISPLAY +from ..constants.acrossx_profile import VERB_DISPLAY_POSTED, VERB_ID_POSTED + + +class PostedVerb(BaseXapiVerb): + """Pydantic model for posted `verb`. + + Attributes: + id (str): Consists of the value `https://w3id.org/xapi/acrossx/verbs/posted`. + display (dict): Consists of the dictionary `{"en-US": "posted"}`. + """ + + id: VERB_ID_POSTED = VERB_ID_POSTED.__args__[0] + display: Optional[Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_POSTED]] diff --git a/src/ralph/models/xapi/concepts/verbs/activity_streams_vocabulary.py b/src/ralph/models/xapi/concepts/verbs/activity_streams_vocabulary.py new file mode 100644 index 000000000..4b682916f --- /dev/null +++ b/src/ralph/models/xapi/concepts/verbs/activity_streams_vocabulary.py @@ -0,0 +1,36 @@ +"""`Activity streams vocabulary` verbs definitions.""" + +from typing import Dict, Optional + +from ...base.verbs import BaseXapiVerb +from ...constants import LANG_EN_US_DISPLAY +from ..constants.activity_streams_vocabulary import ( + VERB_DISPLAY_JOINED, + VERB_DISPLAY_LEFT, + VERB_ID_JOIN, + VERB_ID_LEAVE, +) + + +class JoinVerb(BaseXapiVerb): + """Pydantic model for join verb. + + Attributes: + id (str): Consists of the value `http://activitystrea.ms/join`. + display (dict): Consists of the dictionary `{"en-US": "joined"}`. + """ + + id: VERB_ID_JOIN = VERB_ID_JOIN.__args__[0] + display: Optional[Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_JOINED]] + + +class LeaveVerb(BaseXapiVerb): + """Pydantic model for leave `verb`. + + Attributes: + id (str): Consists of the value `http://activitystrea.ms/leave`. + display (dict): Consists of the dictionary `{"en-US": "left"}`. + """ + + id: VERB_ID_LEAVE = VERB_ID_LEAVE.__args__[0] + display: Optional[Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_LEFT]] diff --git a/src/ralph/models/xapi/concepts/verbs/adl_vocabulary.py b/src/ralph/models/xapi/concepts/verbs/adl_vocabulary.py new file mode 100644 index 000000000..ae87e6773 --- /dev/null +++ b/src/ralph/models/xapi/concepts/verbs/adl_vocabulary.py @@ -0,0 +1,36 @@ +"""`ADL Vocabulary` verbs definitions.""" + +from typing import Dict, Optional + +from ...base.verbs import BaseXapiVerb +from ...constants import LANG_EN_US_DISPLAY +from ..constants.adl_vocabulary import ( + VERB_DISPLAY_ANSWERED, + VERB_DISPLAY_ASKED, + VERB_ID_ANSWERED, + VERB_ID_ASKED, +) + + +class AskedVerb(BaseXapiVerb): + """Pydantic model for asked `verb`. + + Attributes: + id (str): Consists of the value `http://adlnet.gov/expapi/verbs/asked`. + display (dict): Consists of the dictionary `{"en-US": "asked"}`. + """ + + id: VERB_ID_ASKED = VERB_ID_ASKED.__args__[0] + display: Optional[Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_ASKED]] + + +class AnsweredVerb(BaseXapiVerb): + """Pydantic model for answered `verb`. + + Attributes: + id (str): Consists of the value `http://adlnet.gov/expapi/verbs/answered`. + display (dict): Consists of the dictionary `{"en-US": "answered"}`. + """ + + id: VERB_ID_ANSWERED = VERB_ID_ANSWERED.__args__[0] + display: Optional[Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_ANSWERED]] diff --git a/src/ralph/models/xapi/concepts/verbs/scorm_profile.py b/src/ralph/models/xapi/concepts/verbs/scorm_profile.py new file mode 100644 index 000000000..83a033aa0 --- /dev/null +++ b/src/ralph/models/xapi/concepts/verbs/scorm_profile.py @@ -0,0 +1,64 @@ +"""`Scorm Profile` verbs definitions.""" + +from typing import Dict, Optional + +from ...base.verbs import BaseXapiVerb +from ...concepts.constants.scorm_profile import ( + VERB_DISPLAY_COMPLETED, + VERB_DISPLAY_INITIALIZED, + VERB_DISPLAY_INTERACTED, + VERB_DISPLAY_TERMINATED, + VERB_ID_COMPLETED, + VERB_ID_INITIALIZED, + VERB_ID_INTERACTED, + VERB_ID_TERMINATED, +) +from ...constants import LANG_EN_US_DISPLAY + + +class CompletedVerb(BaseXapiVerb): + """Pydantic model for completed `verb`. + + Attributes: + id (str): Consists of the value `http://adlnet.gov/expapi/verbs/completed`. + display (dict): Consists of the dictionary `{"en-US": "completed"}`. + """ + + id: VERB_ID_COMPLETED = VERB_ID_COMPLETED.__args__[0] + display: Optional[Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_COMPLETED]] + + +class InitializedVerb(BaseXapiVerb): + """Pydantic model for initialized `verb`. + + Attributes: + id (str): Consists of the value `http://adlnet.gov/expapi/verbs/initialized`. + display (Dict): Consists of the dictionary `{"en-US": "initialized"}`. + """ + + id: VERB_ID_INITIALIZED = VERB_ID_INITIALIZED.__args__[0] + display: Optional[Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_INITIALIZED]] + + +class InteractedVerb(BaseXapiVerb): + """Pydantic model for interacted `verb`. + + Attributes: + id (str): Consists of the value `http://adlnet.gov/expapi/verbs/interacted`. + display (dict): Consists of the dictionary `{"en-US": "interacted"}`. + """ + + id: VERB_ID_INTERACTED = VERB_ID_INTERACTED.__args__[0] + display: Optional[Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_INTERACTED]] + + +class TerminatedVerb(BaseXapiVerb): + """Pydantic model for terminated `verb`. + + Attributes: + id (str): Consists of the value `http://adlnet.gov/expapi/verbs/terminated`. + display (dict): Consists of the dictionary `{"en-US": "terminated"}`. + """ + + id: VERB_ID_TERMINATED = VERB_ID_TERMINATED.__args__[0] + display: Optional[Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_TERMINATED]] diff --git a/src/ralph/models/xapi/concepts/verbs/tincan_vocabulary.py b/src/ralph/models/xapi/concepts/verbs/tincan_vocabulary.py new file mode 100644 index 000000000..ec782e433 --- /dev/null +++ b/src/ralph/models/xapi/concepts/verbs/tincan_vocabulary.py @@ -0,0 +1,19 @@ +"""`TinCan Vocabulary` verbs definitions.""" + +from typing import Dict, Optional + +from ...base.verbs import BaseXapiVerb +from ...constants import LANG_EN_US_DISPLAY +from ..constants.tincan_vocabulary import VERB_DISPLAY_VIEWED, VERB_ID_VIEWED + + +class ViewedVerb(BaseXapiVerb): + """Pydantic model for viewed `verb`. + + Attributes: + id (str): Consists of the value `http://id.tincanapi.com/verb/viewed`. + display (dict): Consists of the dictionary `{"en-US": "viewed"}`. + """ + + id: VERB_ID_VIEWED = VERB_ID_VIEWED.__args__[0] + display: Optional[Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_VIEWED]] diff --git a/src/ralph/models/xapi/concepts/verbs/video.py b/src/ralph/models/xapi/concepts/verbs/video.py new file mode 100644 index 000000000..bfb123acf --- /dev/null +++ b/src/ralph/models/xapi/concepts/verbs/video.py @@ -0,0 +1,50 @@ +"""`Video` verbs definitions.""" + +from typing import Dict, Optional + +from ...base.verbs import BaseXapiVerb +from ...constants import LANG_EN_US_DISPLAY +from ..constants.video import ( + VERB_DISPLAY_PAUSED, + VERB_DISPLAY_PLAYED, + VERB_DISPLAY_SEEKED, + VERB_ID_PAUSED, + VERB_ID_PLAYED, + VERB_ID_SEEKED, +) + + +class PlayedVerb(BaseXapiVerb): + """Pydantic model for played `verb`. + + Attributes: + id (str): Consists of the value `https://w3id.org/xapi/video/verbs/played`. + display (dict): Consists of the dictionary `{"en-US": "played"}`. + """ + + id: VERB_ID_PLAYED = VERB_ID_PLAYED.__args__[0] + display: Optional[Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_PLAYED]] + + +class PausedVerb(BaseXapiVerb): + """Pydantic model for paused `verb` field. + + Attributes: + id (str): Consists of the value `https://w3id.org/xapi/video/verbs/paused`. + display (dict): Consists of the dictionary `{"en-US": "paused"}`. + """ + + id: VERB_ID_PAUSED = VERB_ID_PAUSED.__args__[0] + display: Optional[Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_PAUSED]] + + +class SeekedVerb(BaseXapiVerb): + """Pydantic model for seeked `verb` field. + + Attributes: + id (str): Consists of the value `https://w3id.org/xapi/video/verbs/seeked`. + display (dict): Consists of the dictionary `{"en-US": "seeked"}`. + """ + + id: VERB_ID_SEEKED = VERB_ID_SEEKED.__args__[0] + display: Optional[Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_SEEKED]] diff --git a/src/ralph/models/xapi/concepts/verbs/virtual_classroom.py b/src/ralph/models/xapi/concepts/verbs/virtual_classroom.py new file mode 100644 index 000000000..fed8630e8 --- /dev/null +++ b/src/ralph/models/xapi/concepts/verbs/virtual_classroom.py @@ -0,0 +1,144 @@ +"""`Virtual classroom` verbs definitions.""" + +from typing import Dict + +from ...base.verbs import BaseXapiVerb +from ...constants import LANG_EN_US_DISPLAY +from ..constants.virtual_classroom import ( + VERB_DISPLAY_LOWERED_HAND, + VERB_DISPLAY_MUTED, + VERB_DISPLAY_RAISED_HAND, + VERB_DISPLAY_SHARED_SCREEN, + VERB_DISPLAY_STARTED_CAMERA, + VERB_DISPLAY_STOPPED_CAMERA, + VERB_DISPLAY_UNMUTED, + VERB_DISPLAY_UNSHARED_SCREEN, + VERB_ID_LOWERED_HAND, + VERB_ID_MUTED, + VERB_ID_RAISED_HAND, + VERB_ID_SHARED_SCREEN, + VERB_ID_STARTED_CAMERA, + VERB_ID_STOPPED_CAMERA, + VERB_ID_UNMUTED, + VERB_ID_UNSHARED_SCREEN, +) + + +class MutedVerb(BaseXapiVerb): + """Pydantic model for muted `verb`. + + Attributes: + id (str): Consists of the value + `https://w3id.org/xapi/virtual-classroom/verbs/muted`. + display (dict): Consists of the dictionary `{"en-US": "muted"}`. + """ + + id: VERB_ID_MUTED = VERB_ID_MUTED.__args__[0] + display: Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_MUTED] = { + LANG_EN_US_DISPLAY.__args__[0]: VERB_DISPLAY_MUTED.__args__[0] + } + + +class UnmutedVerb(BaseXapiVerb): + """Pydantic model for unmuted `verb`. + + Attributes: + id (str): Consists of the value + `https://w3id.org/xapi/virtual-classroom/verbs/unmuted`. + display (dict): Consists of the dictionary `{"en-US": "unmuted"}`. + """ + + id: VERB_ID_UNMUTED = VERB_ID_UNMUTED.__args__[0] + display: Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_UNMUTED] = { + LANG_EN_US_DISPLAY.__args__[0]: VERB_DISPLAY_UNMUTED.__args__[0] + } + + +class StartedCameraVerb(BaseXapiVerb): + """Pydantic model for started camera `verb`. + + Attributes: + id (str): Consists of the value + `https://w3id.org/xapi/virtual-classroom/verbs/started-camera`. + display (dict): Consists of the dictionary `{"en-US": "started camera"}`. + """ + + id: VERB_ID_STARTED_CAMERA = VERB_ID_STARTED_CAMERA.__args__[0] + display: Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_STARTED_CAMERA] = { + LANG_EN_US_DISPLAY.__args__[0]: VERB_DISPLAY_STARTED_CAMERA.__args__[0] + } + + +class StoppedCameraVerb(BaseXapiVerb): + """Pydantic model for stopped camera `verb`. + + Attributes: + id (str): Consists of the value + `https://w3id.org/xapi/virtual-classroom/verbs/stopped-camera`. + display (dict): Consists of the dictionary `{"en-US": "stopped camera"}`. + """ + + id: VERB_ID_STOPPED_CAMERA = VERB_ID_STOPPED_CAMERA.__args__[0] + display: Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_STOPPED_CAMERA] = { + LANG_EN_US_DISPLAY.__args__[0]: VERB_DISPLAY_STOPPED_CAMERA.__args__[0] + } + + +class SharedScreenVerb(BaseXapiVerb): + """Pydantic model for shared screen `verb`. + + Attributes: + id (str): Consists of the value + `https://w3id.org/xapi/virtual-classroom/verbs/shared-screen`. + display (dict): Consists of the dictionary `{"en-US": "shared screen"}`. + """ + + id: VERB_ID_SHARED_SCREEN = VERB_ID_SHARED_SCREEN.__args__[0] + display: Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_SHARED_SCREEN] = { + LANG_EN_US_DISPLAY.__args__[0]: VERB_DISPLAY_SHARED_SCREEN.__args__[0] + } + + +class UnsharedScreenVerb(BaseXapiVerb): + """Pydantic model for unshared screen `verb`. + + Attributes: + id (str): Consists of the value + `https://w3id.org/xapi/virtual-classroom/verbs/unshared-screen`. + display (dict): Consists of the dictionary `{"en-US": "unshared screen"}`. + """ + + id: VERB_ID_UNSHARED_SCREEN = VERB_ID_UNSHARED_SCREEN.__args__[0] + display: Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_UNSHARED_SCREEN] = { + LANG_EN_US_DISPLAY.__args__[0]: VERB_DISPLAY_UNSHARED_SCREEN.__args__[0] + } + + +class RaisedHandVerb(BaseXapiVerb): + """Pydantic model for raised hand `verb`. + + Attributes: + id (str): Consists of the value + `https://w3id.org/xapi/virtual-classroom/verbs/raised-hand`. + display (dict): Consists of the dictionary `{"en-US": "raised hand"}`. + """ + + id: VERB_ID_RAISED_HAND = VERB_ID_RAISED_HAND.__args__[0] + display: Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_RAISED_HAND] = { + LANG_EN_US_DISPLAY.__args__[0]: VERB_DISPLAY_RAISED_HAND.__args__[0] + } + + +class LoweredHandVerb(BaseXapiVerb): + """Pydantic model for lowered hand `verb`. + + Attributes: + id (str): Consists of the value + `https://w3id.org/xapi/virtual-classroom/verbs/lowered-hand`. + display (dict): Consists of the dictionary `{"en-US": "lowered hand"}`. + """ + + id: VERB_ID_LOWERED_HAND = VERB_ID_LOWERED_HAND.__args__[0] + display: Dict[LANG_EN_US_DISPLAY, VERB_DISPLAY_LOWERED_HAND] = { + LANG_EN_US_DISPLAY.__args__[0]: VERB_DISPLAY_LOWERED_HAND.__args__[0] + } diff --git a/src/ralph/models/xapi/constants.py b/src/ralph/models/xapi/constants.py index 86a175a73..323b31042 100644 --- a/src/ralph/models/xapi/constants.py +++ b/src/ralph/models/xapi/constants.py @@ -7,45 +7,3 @@ # Languages LANG_EN_US_DISPLAY = Literal["en-US"] # pylint:disable=invalid-name - -# xAPI activities -ACTIVITY_PAGE_DISPLAY = Literal["page"] # pylint:disable=invalid-name -ACTIVITY_PAGE_ID = Literal[ # pylint:disable=invalid-name - "http://activitystrea.ms/schema/1.0/page" -] - -# xAPI verbs -VERB_ANSWERED_DISPLAY = Literal["answered"] # pylint:disable=invalid-name -VERB_COMPLETED_DISPLAY = Literal["completed"] # pylint:disable=invalid-name -VERB_INITIALIZED_DISPLAY = Literal["initialized"] # pylint:disable=invalid-name -VERB_INTERACTED_DISPLAY = Literal["interacted"] # pylint:disable=invalid-name -VERB_PAUSED_DISPLAY = Literal["paused"] # pylint:disable=invalid-name -VERB_PLAYED_DISPLAY = Literal["played"] # pylint:disable=invalid-name -VERB_SEEKED_DISPLAY = Literal["seeked"] # pylint:disable=invalid-name -VERB_TERMINATED_DISPLAY = Literal["terminated"] # pylint:disable=invalid-name -VERB_VIEWED_DISPLAY = Literal["viewed"] # pylint:disable=invalid-name - -# xAPI verbs IDs -VERB_ANSWERED_ID = Literal[ # pylint:disable=invalid-name - "http://adlnet.gov/expapi/verbs/answered" -] -VERB_COMPLETED_ID = Literal[ # pylint:disable=invalid-name - "http://adlnet.gov/expapi/verbs/completed" -] -VERB_INITIALIZED_ID = Literal[ # pylint:disable=invalid-name - "http://adlnet.gov/expapi/verbs/initialized" -] -VERB_INTERACTED_ID = Literal[ # pylint:disable=invalid-name - "http://adlnet.gov/expapi/verbs/interacted" -] -VERB_TERMINATED_ID = Literal[ # pylint:disable=invalid-name - "http://adlnet.gov/expapi/verbs/terminated" -] -VERB_VIEWED_ID = Literal[ # pylint:disable=invalid-name - "http://id.tincanapi.com/verb/viewed" -] -# xAPI extensions -EXTENSION_COURSE_ID = "http://adlnet.gov/expapi/activities/course" -EXTENSION_MODULE_ID = "http://adlnet.gov/expapi/activities/module" -EXTENSION_SCHOOL_ID = "https://w3id.org/xapi/acrossx/extensions/school" -EXTENSION_SESSION_ID = "https://w3id.org/xapi/cmi5/context/extensions/sessionid" diff --git a/src/ralph/models/xapi/fields/actors.py b/src/ralph/models/xapi/fields/actors.py deleted file mode 100644 index 864a44776..000000000 --- a/src/ralph/models/xapi/fields/actors.py +++ /dev/null @@ -1,156 +0,0 @@ -"""Common xAPI actor field definitions.""" - -from typing import List, Optional, Union - -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal - -from pydantic import AnyUrl, StrictStr, constr - -from ..config import BaseModelWithConfig -from .common import IRI, MailtoEmail - - -class AccountActorAccountField(BaseModelWithConfig): - """Pydantic model for `actor.account` field. - - Attributes: - homePage (IRI): Consists of the home page of the account's service provider. - name (str): Consists of the unique id or name of the Actor's account. - """ - - homePage: IRI - name: StrictStr - - -class BaseActorField(BaseModelWithConfig): - """Pydantic model for core `actor` field. - - It defines who performed the action. - - Attributes: - objectType (str): Consists of the value `Agent`. - name (str): Consists of the full name of the Agent. - """ - - objectType: Optional[Literal["Agent"]] - name: Optional[StrictStr] - - -class MboxActorField(BaseActorField): - """Pydantic model for `actor` field. - - It defines a mailto Inverse Functional Identifier. - - Attributes: - mbox (MailtoEmail): Consists of the Agent's email address. - """ - - mbox: MailtoEmail - - -class MboxSha1SumActorField(BaseActorField): - """Pydantic model for `actor` field. - - It defines a hash Inverse Functional Identifier. - - Attributes: - mbox_sha1sum (str): Consists of the SHA1 hash of the Agent's email address. - """ - - mbox_sha1sum: constr(regex=r"^[0-9a-f]{40}$") # noqa:F722 - - -class OpenIdActorField(BaseActorField): - """Pydantic model for `actor` field. - - It defines an OpenID Inverse Functional Identifier. - - Attributes: - openid (URI): Consists of an openID that uniquely identifies the Agent. - """ - - openid: AnyUrl - - -class AccountActorField(BaseActorField): - """Pydantic model for `actor` field. - - It defines an account Inverse Functional Identifier. - - Attributes: - account (dict): See AccountActorAccountField. - """ - - account: AccountActorAccountField - - -AgentActorField = Union[ - MboxActorField, MboxSha1SumActorField, OpenIdActorField, AccountActorField -] - - -class AnonymousGroupActorField(BaseActorField): - """Pydantic model for `actor` field. - - It is defined for Anonymous Group type. - - Attributes: - objectType (str): Consists of the value `Group`. - member (list): Consist of a list of the members of this Group. - """ - - objectType: Literal["Group"] - member: List[AgentActorField] - - -class BaseIdentifiedGroupActorField(AnonymousGroupActorField): - """Pydantic model for `actor` field. - - It is defined for Identified Group type. - - Attributes: - member (list): Consist of a list of the members of this Group. - """ - - member: Optional[List[AgentActorField]] - - -class MboxGroupActorField(BaseIdentifiedGroupActorField, MboxActorField): - """Pydantic model for `actor` field. - - It is defined for group type with a mailto IFI. - """ - - -class MboxSha1SumGroupActorField(BaseIdentifiedGroupActorField, MboxSha1SumActorField): - """Pydantic model for `actor` field. - - It is defined for group type with a hash IFI. - """ - - -class OpenIdGroupActorField(BaseIdentifiedGroupActorField, OpenIdActorField): - """Pydantic model for `actor` field. - - It is defined for group type with an openID IFI. - """ - - -class AccountGroupActorField(BaseIdentifiedGroupActorField, AccountActorField): - """Pydantic model for `actor` field. - - It is defined for group type with an account IFI. - """ - - -GroupActorField = Union[ - AnonymousGroupActorField, - MboxGroupActorField, - MboxSha1SumGroupActorField, - OpenIdGroupActorField, - AccountGroupActorField, -] -ActorField = Union[AgentActorField, GroupActorField] diff --git a/src/ralph/models/xapi/fields/contexts.py b/src/ralph/models/xapi/fields/contexts.py deleted file mode 100644 index 8a86d52d7..000000000 --- a/src/ralph/models/xapi/fields/contexts.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Common xAPI context field definitions.""" - -from typing import Dict, List, Optional, Union -from uuid import UUID - -from pydantic import StrictStr - -from ..config import BaseModelWithConfig -from .actors import ActorField, GroupActorField -from .common import IRI, LanguageTag -from .unnested_objects import ActivityObjectField, StatementRefObjectField - - -class ContextActivitiesContextField(BaseModelWithConfig): - """Pydantic model for `context.contextActivities` field. - - Attributes: - parent (List): An Activity with a direct relation to the statement's Activity. - grouping (List): An Activity with an indirect relation to the statement's - Activity. - category (List): An Activity used to categorize the Statement. - other (List): A contextActivity that doesn't fit one of the other properties. - """ - - parent: Optional[Union[ActivityObjectField, List[ActivityObjectField]]] - grouping: Optional[Union[ActivityObjectField, List[ActivityObjectField]]] - category: Optional[Union[ActivityObjectField, List[ActivityObjectField]]] - other: Optional[Union[ActivityObjectField, List[ActivityObjectField]]] - - -class ContextField(BaseModelWithConfig): - """Pydantic model for `context` field. - - Attributes: - registration (UUID): The registration that the Statement is associated with. - instructor (ActorField): The instructor that the Statement relates to. - team (GroupActorField): The team that this Statement relates to. - contextActivities (dict): See ContextActivitiesContextField. - revision (str): The revision of the activity associated with this Statement. - platform (str): The platform where the learning activity took place. - language (LanguageTag): The language in which the experience occurred. - statement (StatementRef): Another Statement giving context for this Statement. - extensions (dict): Consists of an dictionary of other properties as needed. - """ - - registration: Optional[UUID] - instructor: Optional[ActorField] - team: Optional[GroupActorField] - contextActivities: Optional[ContextActivitiesContextField] - revision: Optional[StrictStr] - platform: Optional[StrictStr] - language: Optional[LanguageTag] - statement: Optional[StatementRefObjectField] - extensions: Optional[Dict[IRI, Union[str, int, bool, list, dict, None]]] diff --git a/src/ralph/models/xapi/fields/objects.py b/src/ralph/models/xapi/fields/objects.py deleted file mode 100644 index 1e3fb6e81..000000000 --- a/src/ralph/models/xapi/fields/objects.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Common xAPI object field definitions.""" - -# Nota bene: we split object definitions into `objects.py` and `unnested_objects.py` -# because of the circular dependency : objects -> context -> objects. - -from datetime import datetime -from typing import List, Optional, Union - -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal - -from pydantic import Field - -from ..config import BaseExtensionModelWithConfig, BaseModelWithConfig -from ..constants import EXTENSION_COURSE_ID, EXTENSION_MODULE_ID, EXTENSION_SCHOOL_ID -from .actors import ActorField -from .attachments import AttachmentField -from .contexts import ContextField -from .results import ResultField -from .unnested_objects import UnnestedObjectField -from .verbs import VerbField - - -class SubStatementObjectField(BaseModelWithConfig): - """Pydantic model for `object` field. - - It is defined for SubStatement tyoe. - - Attributes: - actor (ActorField): See ActorField. - verb (VerbField): See VerbField. - object (UnnestedObjectField): See UnnestedObjectField. - """ - - actor: ActorField - verb: VerbField - object: UnnestedObjectField - objectType: Literal["SubStatement"] - result: Optional[ResultField] - context: Optional[ContextField] - timestamp: Optional[datetime] - attachments: Optional[List[AttachmentField]] - - -ObjectField = Union[UnnestedObjectField, SubStatementObjectField] - - -class ObjectDefinitionExtensionsField(BaseExtensionModelWithConfig): - """Pydantic model for `object.definition.extensions` field. - - Attributes: - school (str): Consists of the name of the school. - course (str): Consists of the name of the course. - module (str): Consists of the name of the module. - """ - - school: Optional[str] = Field(alias=EXTENSION_SCHOOL_ID) - course: Optional[str] = Field(alias=EXTENSION_COURSE_ID) - module: Optional[str] = Field(alias=EXTENSION_MODULE_ID) diff --git a/src/ralph/models/xapi/fields/verbs.py b/src/ralph/models/xapi/fields/verbs.py deleted file mode 100644 index 475bc85f2..000000000 --- a/src/ralph/models/xapi/fields/verbs.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Common xAPI verb field definitions.""" - -from typing import Dict, Optional - -from ..config import BaseModelWithConfig -from ..constants import ( - LANG_EN_US_DISPLAY, - VERB_TERMINATED_DISPLAY, - VERB_TERMINATED_ID, - VERB_VIEWED_DISPLAY, - VERB_VIEWED_ID, -) -from .common import IRI, LanguageMap - - -class VerbField(BaseModelWithConfig): - """Pydantic model for core `verb` field. - - Attributes: - id (IRI): Consists of an identifier for the verb. - display (LanguageMap): Consists of a human readable representation of the verb. - """ - - id: IRI - display: Optional[LanguageMap] - - -class ViewedVerbField(VerbField): - """Pydantic model for viewed `verb` field. - - Attributes: - id (str): Consists of the value `http://id.tincanapi.com/verb/viewed`. - display (dict): Consists of the dictionary `{"en-US": "viewed"}`. - """ - - id: VERB_VIEWED_ID = VERB_VIEWED_ID.__args__[0] - display: Dict[LANG_EN_US_DISPLAY, VERB_VIEWED_DISPLAY] = { - LANG_EN_US_DISPLAY.__args__[0]: VERB_VIEWED_DISPLAY.__args__[0] - } - - -class TerminatedVerbField(VerbField): - """Pydantic model for terminated `verb` field. - - Attributes: - id (str): Consists of the value `http://adlnet.gov/expapi/verbs/terminated`. - display (dict): Consists of the dictionary `{"en-US": "terminated"}`. - """ - - id: VERB_TERMINATED_ID = VERB_TERMINATED_ID.__args__[0] - display: Dict[LANG_EN_US_DISPLAY, VERB_TERMINATED_DISPLAY] = { - LANG_EN_US_DISPLAY.__args__[0]: VERB_TERMINATED_DISPLAY.__args__[0] - } diff --git a/src/ralph/models/xapi/navigation/fields/objects.py b/src/ralph/models/xapi/navigation/fields/objects.py deleted file mode 100644 index 23f6a4073..000000000 --- a/src/ralph/models/xapi/navigation/fields/objects.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Navigation xAPI events object fields definitions.""" - -from typing import Dict, Optional - -from ...constants import ACTIVITY_PAGE_DISPLAY, ACTIVITY_PAGE_ID, LANG_EN_US_DISPLAY -from ...fields.objects import ObjectDefinitionExtensionsField -from ...fields.unnested_objects import ActivityObjectField, ObjectDefinitionField - - -class PageObjectDefinitionField(ObjectDefinitionField): - """Pydantic model for page viewed `object`.`definition` field. - - Attributes: - type (str): Consists of the value `http://activitystrea.ms/schema/1.0/page`. - name (dict): Consists of the dictionary `{"en-US": "page"}`. - extensions (dict): See ObjectDefinitionExtensionsField. - """ - - name: Dict[LANG_EN_US_DISPLAY, ACTIVITY_PAGE_DISPLAY] = { - LANG_EN_US_DISPLAY.__args__[0]: ACTIVITY_PAGE_DISPLAY.__args__[0] - } - type: ACTIVITY_PAGE_ID = ACTIVITY_PAGE_ID.__args__[0] - extensions: Optional[ObjectDefinitionExtensionsField] - - -class PageObjectField(ActivityObjectField): - """Pydantic model for page viewed `object` field. - - Attributes: - definition (dict): See PageObjectDefinitionField. - """ - - definition: PageObjectDefinitionField = PageObjectDefinitionField() diff --git a/src/ralph/models/xapi/navigation/statements.py b/src/ralph/models/xapi/navigation/statements.py index 5332b997e..57e2d1541 100644 --- a/src/ralph/models/xapi/navigation/statements.py +++ b/src/ralph/models/xapi/navigation/statements.py @@ -1,19 +1,20 @@ """Navigation xAPI event definitions.""" from ...selector import selector -from ..base import BaseXapiModel -from ..fields.verbs import TerminatedVerbField, ViewedVerbField -from .fields.objects import PageObjectField +from ..base.statements import BaseXapiStatement +from ..concepts.activity_types.activity_streams_vocabulary import PageActivity +from ..concepts.verbs.scorm_profile import TerminatedVerb +from ..concepts.verbs.tincan_vocabulary import ViewedVerb -class PageViewed(BaseXapiModel): +class PageViewed(BaseXapiStatement): """Pydantic model for page viewed statement. Example: John viewed the https://www.fun-mooc.fr/ page. Attributes: - object (PageObjectField): See PageObjectField. - verb (PageViewedVerbField): See PageViewedVerbField. + object (dict): See PageActivity. + verb (dict): See ViewedVerb. """ __selector__ = selector( @@ -21,18 +22,18 @@ class PageViewed(BaseXapiModel): verb__id="http://id.tincanapi.com/verb/viewed", ) - object: PageObjectField - verb: ViewedVerbField = ViewedVerbField() + object: PageActivity + verb: ViewedVerb = ViewedVerb() -class PageTerminated(BaseXapiModel): +class PageTerminated(BaseXapiStatement): """Pydantic model for page terminated statement. Example: John terminated the https://www.fun-mooc.fr/ page. Attributes: - object (PageObjectField): See PageObjectField. - verb (PageTerminatedVerbField): See PageTerminatedVerbField. + object (dict): See PageActivity. + verb (dict): See TerminatedVerb. """ __selector__ = selector( @@ -40,5 +41,5 @@ class PageTerminated(BaseXapiModel): verb__id="http://adlnet.gov/expapi/verbs/terminated", ) - object: PageObjectField - verb: TerminatedVerbField = TerminatedVerbField() + object: PageActivity + verb: TerminatedVerb = TerminatedVerb() diff --git a/src/ralph/models/xapi/video/constants.py b/src/ralph/models/xapi/video/constants.py deleted file mode 100644 index 1f47a465d..000000000 --- a/src/ralph/models/xapi/video/constants.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Constants for xAPI video specifications.""" - -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal - -# xAPI video extensions -VIDEO_EXTENSION_CC_SUBTITLE_LANG = ( - "https://w3id.org/xapi/video/extensions/cc-subtitle-lang" -) -VIDEO_EXTENSION_CC_ENABLED = "https://w3id.org/xapi/video/extensions/cc-enabled" -VIDEO_EXTENSION_COMPLETION_THRESHOLD = ( - "https://w3id.org/xapi/video/extensions/completion-threshold" -) -VIDEO_EXTENSION_FRAME_RATE = "https://w3id.org/xapi/video/extensions/frame-rate" -VIDEO_EXTENSION_FULL_SCREEN = "https://w3id.org/xapi/video/extensions/full-screen" -VIDEO_EXTENSION_LENGTH = "https://w3id.org/xapi/video/extensions/length" -VIDEO_EXTENSION_PLAYED_SEGMENTS = ( - "https://w3id.org/xapi/video/extensions/played-segments" -) -VIDEO_EXTENSION_PROGRESS = "https://w3id.org/xapi/video/extensions/progress" -VIDEO_EXTENSION_QUALITY = "https://w3id.org/xapi/video/extensions/quality" - -VIDEO_EXTENSION_SCREEN_SIZE = "https://w3id.org/xapi/video/extensions/screen-size" -VIDEO_EXTENSION_SESSION_ID = "https://w3id.org/xapi/video/extensions/session-id" -VIDEO_EXTENSION_SPEED = "https://w3id.org/xapi/video/extensions/speed" -VIDEO_EXTENSION_TIME = "https://w3id.org/xapi/video/extensions/time" -VIDEO_EXTENSION_TIME_FROM = "https://w3id.org/xapi/video/extensions/time-from" -VIDEO_EXTENSION_TIME_TO = "https://w3id.org/xapi/video/extensions/time-to" -VIDEO_EXTENSION_TRACK = "https://w3id.org/xapi/video/extensions/track" -VIDEO_EXTENSION_USER_AGENT = "https://w3id.org/xapi/video/extensions/user-agent" -VIDEO_EXTENSION_VIDEO_PLAYBACK_SIZE = ( - "https://w3id.org/xapi/video/extensions/video-playback-size" -) -VIDEO_EXTENSION_VOLUME = "https://w3id.org/xapi/video/extensions/volume" - - -# xAPI video object definition type -VIDEO_OBJECT_DEFINITION_TYPE = Literal[ # pylint:disable=invalid-name - "https://w3id.org/xapi/video/activity-type/video" -] - -# Video context category -VIDEO_CONTEXT_CATEGORY = Literal[ # pylint:disable=invalid-name - "https://w3id.org/xapi/video" -] - -# xAPI video verbs -VERB_VIDEO_PAUSED_ID = Literal[ # pylint:disable=invalid-name - "https://w3id.org/xapi/video/verbs/paused" -] -VERB_VIDEO_PLAYED_ID = Literal[ # pylint:disable=invalid-name - "https://w3id.org/xapi/video/verbs/played" -] -VERB_VIDEO_SEEKED_ID = Literal[ # pylint:disable=invalid-name - "https://w3id.org/xapi/video/verbs/seeked" -] diff --git a/src/ralph/models/xapi/video/contexts.py b/src/ralph/models/xapi/video/contexts.py new file mode 100644 index 000000000..90f51567d --- /dev/null +++ b/src/ralph/models/xapi/video/contexts.py @@ -0,0 +1,247 @@ +"""Video xAPI events context fields definitions.""" + +from typing import Dict, List, Optional + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +from uuid import UUID + +from pydantic import Field, NonNegativeFloat + +from ..base.contexts import BaseXapiContext, BaseXapiContextContextActivities +from ..concepts.constants.video import ( + CONTEXT_EXTENSION_CC_ENABLED, + CONTEXT_EXTENSION_CC_SUBTITLE_LANG, + CONTEXT_EXTENSION_COMPLETION_THRESHOLD, + CONTEXT_EXTENSION_FULL_SCREEN, + CONTEXT_EXTENSION_LENGTH, + CONTEXT_EXTENSION_SCREEN_SIZE, + CONTEXT_EXTENSION_SESSION_ID, + CONTEXT_EXTENSION_SPEED, + CONTEXT_EXTENSION_USER_AGENT, + CONTEXT_EXTENSION_VIDEO_PLAYBACK_SIZE, + CONTEXT_EXTENSION_VOLUME, + PROFILE_ID_VIDEO, +) +from ..config import BaseExtensionModelWithConfig + + +class VideoContextContextActivities(BaseXapiContextContextActivities): + """Pydantic model for video context `contextActivities` property. + + Attributes: + category (List): Consists of a list containing the dictionary + {"id": "https://w3id.org/xapi/video"}. + """ + + category: List[Dict[Literal["id"], PROFILE_ID_VIDEO]] = [ + {"id": PROFILE_ID_VIDEO.__args__[0]} + ] + + +class BaseVideoContext(BaseXapiContext): + """Pydantic model for video core `context` property. + + Attributes: + contextActivities (dict): see VideoContextContextActivities. + """ + + contextActivities: Optional[VideoContextContextActivities] + + +class VideoContextExtensions(BaseExtensionModelWithConfig): + """Pydantic model for video core context `extensions` property. + + Attributes: + session (uuid): Consists of the ID of the active session. + """ + + session_id: Optional[UUID] = Field(alias=CONTEXT_EXTENSION_SESSION_ID) + + +class VideoInitializedContextExtensions(VideoContextExtensions): + """Pydantic model for video initialized `context` `extensions` property. + + Attributes: + length (float): Consists of the length of the video. + ccSubtitleEnabled (bool): Indicates whether subtitle or closed captioning is + enabled. + ccSubtitleLanguage (str): Consists of the language of subtitle or closed + captioning. + fullScreen (bool): Indicates whether the video is played in full screen mode. + screenSize (str): Consists of the device playback screen size or the maximum + available screen size for Video playback. + videoPlaybackSize (str): Consists of the size in Width x Height of the video as + viewed by the user. + speed (str): Consists of the play back speed. + userAgent (str): Consists of the User Agent string of the browser, + if the video is launched in browser. + volume (int): Consists of the volume of the video. + completionThreshold (float): Consists of the percentage of media that should be + consumed to trigger a completion. + """ + + length: NonNegativeFloat = Field(alias=CONTEXT_EXTENSION_LENGTH) + ccSubtitleEnabled: Optional[bool] = Field(alias=CONTEXT_EXTENSION_CC_ENABLED) + ccSubtitleLang: Optional[str] = Field(alias=CONTEXT_EXTENSION_CC_SUBTITLE_LANG) + fullScreen: Optional[bool] = Field(alias=CONTEXT_EXTENSION_FULL_SCREEN) + screenSize: Optional[str] = Field(alias=CONTEXT_EXTENSION_SCREEN_SIZE) + videoPlaybackSize: Optional[str] = Field( + alias=CONTEXT_EXTENSION_VIDEO_PLAYBACK_SIZE + ) + speed: Optional[str] = Field(alias=CONTEXT_EXTENSION_SPEED) + userAgent: Optional[str] = Field(alias=CONTEXT_EXTENSION_USER_AGENT) + volume: Optional[int] = Field(alias=CONTEXT_EXTENSION_VOLUME) + completionThreshold: Optional[float] = Field( + alias=CONTEXT_EXTENSION_COMPLETION_THRESHOLD + ) + + +class VideoBrowsingContextExtensions(VideoContextExtensions): + """Pydantic model for video browsing `context`.`extensions` property. + + Such field is used in `paused`, `completed` and `terminated` events. + + Attributes: + completionThreshold (float): Consists of the percentage of media that should + be consumed to trigger a completion. + length (float): Consists of the length of the video. + """ + + length: NonNegativeFloat = Field(alias=CONTEXT_EXTENSION_LENGTH) + completionThreshold: Optional[float] = Field( + alias=CONTEXT_EXTENSION_COMPLETION_THRESHOLD + ) + + +class VideoEnableClosedCaptioningContextExtensions(VideoContextExtensions): + """Represents the context.extensions field for video `interacted` xAPI statement. + + Attributes: + ccSubtitleLanguage (str): Consists of the language of subtitle or closed + captioning. + """ + + ccSubtitleLanguage: str = Field(alias=CONTEXT_EXTENSION_CC_SUBTITLE_LANG) + + +class VideoVolumeChangeInteractionContextExtensions(VideoContextExtensions): + # noqa: D205, D415 + """Pydantic model for video volume change interaction `context`.`extensions` + property. + + Attributes: + volume (int): Consists of the volume of the video. + """ + + volume: int = Field(alias=CONTEXT_EXTENSION_VOLUME) + + +class VideoScreenChangeInteractionContextExtensions(VideoContextExtensions): + # noqa: D205, D415 + """Pydantic model for video screen change interaction `context`.`extensions` + property. + + Attributes: + fullScreen (bool): Indicates whether the video is played in full screen mode. + screenSize (str): Expresses the total available screen size for Video playback. + videoPlaybackSize (str): Consists of the size in Width x Height of the video as + viewed by the user. + """ + + fullScreen: bool = Field(alias=CONTEXT_EXTENSION_FULL_SCREEN) + screenSize: str = Field(alias=CONTEXT_EXTENSION_SCREEN_SIZE) + videoPlaybackSize: str = Field(alias=CONTEXT_EXTENSION_VIDEO_PLAYBACK_SIZE) + + +class VideoInitializedContext(BaseVideoContext): + """Pydantic model for video initialized `context` property. + + Attributes: + extensions (dict): See VideoInitializedContextExtensionsField. + """ + + extensions: VideoInitializedContextExtensions + + +class VideoPlayedContext(BaseVideoContext): + """Pydantic model for video played `context` property. + + Attributes: + extensions (dict): See VideoContextExtensionsField. + """ + + extensions: Optional[VideoContextExtensions] + + +class VideoPausedContext(BaseVideoContext): + """Pydantic model for video paused `context` property. + + Attributes: + extensions (dict): See VideoBrowsingContextExtensions. + """ + + extensions: VideoBrowsingContextExtensions + + +class VideoSeekedContext(BaseVideoContext): + """Pydantic model for video seeked `context` property. + + Attributes: + extensions (dict): See VideoContextExtensionsField. + """ + + extensions: Optional[VideoContextExtensions] + + +class VideoCompletedContext(BaseVideoContext): + """Pydantic model for video completed `context` property. + + Attributes: + extensions (dict): See VideoBrowsingContextExtensionsField. + """ + + extensions: VideoBrowsingContextExtensions + + +class VideoTerminatedContext(BaseVideoContext): + """Pydantic model for video terminated `context` property. + + Attributes: + extensions (dict): See VideoBrowsingContextExtensionsField. + """ + + extensions: VideoBrowsingContextExtensions + + +class VideoEnableClosedCaptioningContext(BaseVideoContext): + """Pydantic modle for video enable closed captioning `context` property. + + Attributes: + extensions (dict): See VideoEnableClosedCaptioningContextExtensionsField. + """ + + extensions: VideoEnableClosedCaptioningContextExtensions + + +class VideoVolumeChangeInteractionContext(BaseVideoContext): + """Pydantic model for video volume change interaction `context` property. + + Attributes: + extensions (dict): See VideoVolumeChangeInteractionContextExtensionsField. + """ + + extensions: VideoVolumeChangeInteractionContextExtensions + + +class VideoScreenChangeInteractionContext(BaseVideoContext): + """Pydantic model for video screen change interaction `context` property. + + Attributes: + extensions (dict): See VideoScreenChangeInteractionContextExtensionsField. + """ + + extensions: VideoScreenChangeInteractionContextExtensions diff --git a/src/ralph/models/xapi/video/fields/contexts.py b/src/ralph/models/xapi/video/fields/contexts.py deleted file mode 100644 index b3b0f4983..000000000 --- a/src/ralph/models/xapi/video/fields/contexts.py +++ /dev/null @@ -1,241 +0,0 @@ -"""Video xAPI events context fields definitions.""" - -from typing import Dict, List, Optional - -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal - -from uuid import UUID - -from pydantic import Field, NonNegativeFloat - -from ...config import BaseExtensionModelWithConfig -from ...fields.contexts import ContextActivitiesContextField, ContextField -from ..constants import ( - VIDEO_CONTEXT_CATEGORY, - VIDEO_EXTENSION_CC_ENABLED, - VIDEO_EXTENSION_CC_SUBTITLE_LANG, - VIDEO_EXTENSION_COMPLETION_THRESHOLD, - VIDEO_EXTENSION_FULL_SCREEN, - VIDEO_EXTENSION_LENGTH, - VIDEO_EXTENSION_SCREEN_SIZE, - VIDEO_EXTENSION_SESSION_ID, - VIDEO_EXTENSION_SPEED, - VIDEO_EXTENSION_USER_AGENT, - VIDEO_EXTENSION_VIDEO_PLAYBACK_SIZE, - VIDEO_EXTENSION_VOLUME, -) - - -class VideoContextActivitiesField(ContextActivitiesContextField): - """Pydantic model for video `contextActivities` field. - - Attributes: - category (List): Consists of a list containing the dictionary - {"id": "https://w3id.org/xapi/video"}. - """ - - category: List[Dict[Literal["id"], VIDEO_CONTEXT_CATEGORY]] = [ - {"id": VIDEO_CONTEXT_CATEGORY.__args__[0]} - ] - - -class BaseVideoContextField(ContextField): - """Pydantic model for video core `context` field. - - Attributes: - contextActivities (dict): see VideoContextActivitiesField. - """ - - contextActivities: Optional[VideoContextActivitiesField] - - -class VideoContextExtensionsField(BaseExtensionModelWithConfig): - """Pydantic model for video core `context`.`extensions` field. - - Attributes: - session (uuid): Consists of the ID of the active session. - """ - - session_id: Optional[UUID] = Field(alias=VIDEO_EXTENSION_SESSION_ID) - - -class VideoInitializedContextExtensionsField(VideoContextExtensionsField): - """Pydantic model for video initialized `context`.`extensions` field. - - Attributes: - length (float): Consists of the length of the video. - ccSubtitleEnabled (bool): Indicates whether subtitle or closed captioning is - enabled. - ccSubtitleLanguage (str): Consists of the language of subtitle or closed - captioning. - fullScreen (bool): Indicates whether the video is played in full screen mode. - screenSize (str): Consists of the device playback screen size or the maximum - available screen size for Video playback. - videoPlaybackSize (str): Consists of the size in Width x Height of the video as - viewed by the user. - speed (str): Consists of the play back speed. - userAgent (str): Consists of the User Agent string of the browser, - if the video is launched in browser. - volume (int): Consists of the volume of the video. - completionThreshold (float): Consists of the percentage of media that should be - consumed to trigger a completion. - """ - - length: NonNegativeFloat = Field(alias=VIDEO_EXTENSION_LENGTH) - ccSubtitleEnabled: Optional[bool] = Field(alias=VIDEO_EXTENSION_CC_ENABLED) - ccSubtitleLang: Optional[str] = Field(alias=VIDEO_EXTENSION_CC_SUBTITLE_LANG) - fullScreen: Optional[bool] = Field(alias=VIDEO_EXTENSION_FULL_SCREEN) - screenSize: Optional[str] = Field(alias=VIDEO_EXTENSION_SCREEN_SIZE) - videoPlaybackSize: Optional[str] = Field(alias=VIDEO_EXTENSION_VIDEO_PLAYBACK_SIZE) - speed: Optional[str] = Field(alias=VIDEO_EXTENSION_SPEED) - userAgent: Optional[str] = Field(alias=VIDEO_EXTENSION_USER_AGENT) - volume: Optional[int] = Field(alias=VIDEO_EXTENSION_VOLUME) - completionThreshold: Optional[float] = Field( - alias=VIDEO_EXTENSION_COMPLETION_THRESHOLD - ) - - -class VideoBrowsingContextExtensionsField(VideoContextExtensionsField): - """Pydantic model for video browsing `context`.`extensions` field. - - Such field is used in `paused`, `completed` and `terminated` events. - - Attributes: - completionThreshold (float): Consists of the percentage of media that should - be consumed to trigger a completion. - length (float): Consists of the length of the video. - """ - - length: NonNegativeFloat = Field(alias=VIDEO_EXTENSION_LENGTH) - completionThreshold: Optional[float] = Field( - alias=VIDEO_EXTENSION_COMPLETION_THRESHOLD - ) - - -class VideoEnableClosedCaptioningContextExtensionsField(VideoContextExtensionsField): - """Represents the context.extensions field for video `interacted` xAPI statement. - - Attributes: - ccSubtitleLanguage (str): Consists of the language of subtitle or closed - captioning. - """ - - ccSubtitleLanguage: str = Field(alias=VIDEO_EXTENSION_CC_SUBTITLE_LANG) - - -class VideoVolumeChangeInteractionContextExtensionsField(VideoContextExtensionsField): - """Pydantic model for video volume change interaction `context`.`extensions` field. - - Attributes: - volume (int): Consists of the volume of the video. - """ - - volume: int = Field(alias=VIDEO_EXTENSION_VOLUME) - - -class VideoScreenChangeInteractionContextExtensionsField(VideoContextExtensionsField): - """Pydantic model for video screen change interaction `context`.`extensions` field. - - Attributes: - fullScreen (bool): Indicates whether the video is played in full screen mode. - screenSize (str): Expresses the total available screen size for Video playback. - videoPlaybackSize (str): Consists of the size in Width x Height of the video as - viewed by the user. - """ - - fullScreen: bool = Field(alias=VIDEO_EXTENSION_FULL_SCREEN) - screenSize: str = Field(alias=VIDEO_EXTENSION_SCREEN_SIZE) - videoPlaybackSize: str = Field(alias=VIDEO_EXTENSION_VIDEO_PLAYBACK_SIZE) - - -class VideoInitializedContextField(BaseVideoContextField): - """Pydantic model for video initialized `context` field. - - Attributes: - extensions (dict): See VideoInitializedContextExtensionsField. - """ - - extensions: VideoInitializedContextExtensionsField - - -class VideoPlayedContextField(BaseVideoContextField): - """Pydantic model for video played `context` field. - - Attributes: - extensions (dict): See VideoContextExtensionsField. - """ - - extensions: Optional[VideoContextExtensionsField] - - -class VideoPausedContextField(BaseVideoContextField): - """Pydantic model for video paused `context` field. - - Attributes: - extensions (dict): See VideoBrowsingContextExtensionsField. - """ - - extensions: VideoBrowsingContextExtensionsField - - -class VideoSeekedContextField(BaseVideoContextField): - """Pydantic model for video seeked `context` field. - - Attributes: - extensions (dict): See VideoContextExtensionsField. - """ - - extensions: Optional[VideoContextExtensionsField] - - -class VideoCompletedContextField(BaseVideoContextField): - """Pydantic model for video completed `context` field. - - Attributes: - extensions (dict): See VideoBrowsingContextExtensionsField. - """ - - extensions: VideoBrowsingContextExtensionsField - - -class VideoTerminatedContextField(BaseVideoContextField): - """Pydantic model for video terminated `context` field. - - Attributes: - extensions (dict): See VideoBrowsingContextExtensionsField. - """ - - extensions: VideoBrowsingContextExtensionsField - - -class VideoEnableClosedCaptioningContextField(BaseVideoContextField): - """Pydantic modle for video enable closed captioning `context` field. - - Attributes: - extensions (dict): See VideoEnableClosedCaptioningContextExtensionsField. - """ - - extensions: VideoEnableClosedCaptioningContextExtensionsField - - -class VideoVolumeChangeInteractionContextField(BaseVideoContextField): - """Pydantic model for video volume change interaction `context` field. - - Attributes: - extensions (dict): See VideoVolumeChangeInteractionContextExtensionsField. - """ - - extensions: VideoVolumeChangeInteractionContextExtensionsField - - -class VideoScreenChangeInteractionContextField(BaseVideoContextField): - """Pydantic model for video screen change interaction `context` field. - - Attributes: - extensions (dict): See VideoScreenChangeInteractionContextExtensionsField. - """ - - extensions: VideoScreenChangeInteractionContextExtensionsField diff --git a/src/ralph/models/xapi/video/fields/objects.py b/src/ralph/models/xapi/video/fields/objects.py deleted file mode 100644 index 0485fcbc5..000000000 --- a/src/ralph/models/xapi/video/fields/objects.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Video xAPI events object fields definitions.""" - -from typing import Dict, Optional - -from ...constants import LANG_EN_US_DISPLAY -from ...fields.objects import ObjectDefinitionExtensionsField -from ...fields.unnested_objects import ActivityObjectField, ObjectDefinitionField -from ..constants import VIDEO_OBJECT_DEFINITION_TYPE - - -class VideoObjectDefinitionField(ObjectDefinitionField): - """Pydantic model for video `object`.`definition` field. - - Attributes: - type (str): Consists of the value - `https://w3id.org/xapi/video/activity-type/video`. - extensions (dict): See ObjectDefinitionExtensionsField. - """ - - type: VIDEO_OBJECT_DEFINITION_TYPE = VIDEO_OBJECT_DEFINITION_TYPE.__args__[0] - extensions: Optional[ObjectDefinitionExtensionsField] - - -class VideoObjectField(ActivityObjectField): - """Pydantic model for video `object` field. - - WARNING: Contains an optional name property, this is not a violation of - conformity but goes against xAPI specification recommendations. - - Attributes: - name (dict): Consists of the dictionary `{"en-US": }`. - definition (dict): See VideoObjectDefinitionField. - """ - - name: Optional[Dict[LANG_EN_US_DISPLAY, str]] - definition: VideoObjectDefinitionField = VideoObjectDefinitionField() diff --git a/src/ralph/models/xapi/video/fields/results.py b/src/ralph/models/xapi/video/fields/results.py deleted file mode 100644 index 2f12309e6..000000000 --- a/src/ralph/models/xapi/video/fields/results.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Video xAPI events result fields definitions.""" - -from datetime import timedelta -from typing import Optional - -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal - -from pydantic import Field, NonNegativeFloat - -from ...config import BaseExtensionModelWithConfig -from ...fields.results import ResultField -from ..constants import ( - VIDEO_EXTENSION_CC_ENABLED, - VIDEO_EXTENSION_PLAYED_SEGMENTS, - VIDEO_EXTENSION_PROGRESS, - VIDEO_EXTENSION_TIME, - VIDEO_EXTENSION_TIME_FROM, - VIDEO_EXTENSION_TIME_TO, -) - - -class VideoResultExtensionsField(BaseExtensionModelWithConfig): - """Pydantic model for video `result`.`extensions` field. - - Attributes: - playedSegments (str): Consists of parts of the video the actor watched during - current registration in chronological order (for example, - "0[.]5[,]12[.]22[,]15[.]55[,]55[.]99.33[,]99.33"). - time (float): Consists of the video time code when the event was emitted. - """ - - time: NonNegativeFloat = Field(alias=VIDEO_EXTENSION_TIME) - playedSegments: Optional[str] = Field(alias=VIDEO_EXTENSION_PLAYED_SEGMENTS) - - -class VideoPausedResultExtensionsField(VideoResultExtensionsField): - """Pydantic model for video paused `result`.`extensions` field. - - Attributes: - progress (float): Consists of the ratio of media consumed by the actor. - """ - - progress: Optional[NonNegativeFloat] = Field(alias=VIDEO_EXTENSION_PROGRESS) - - -class VideoSeekedResultExtensionsField(BaseExtensionModelWithConfig): - """Pydantic model for video seeked `result`.`extensions` field. - - Attributes: - timeFrom (float): Consists of the point in time the actor changed from in a - media object during a seek operation. - timeTo (float): Consists of the point in time the actor changed to in a media - object during a seek operation. - """ - - timeFrom: NonNegativeFloat = Field(alias=VIDEO_EXTENSION_TIME_FROM) - timeTo: NonNegativeFloat = Field(alias=VIDEO_EXTENSION_TIME_TO) - - -class VideoCompletedResultExtensionsField(VideoResultExtensionsField): - """Pydantic model for video completed `result`.`extensions` field. - - Attributes: - progress (float): Consists of the percentage of media consumed by the actor. - """ - - progress: NonNegativeFloat = Field(alias=VIDEO_EXTENSION_PROGRESS) - - -class VideoTerminatedResultExtensionsField(VideoResultExtensionsField): - """Pydantic model for video terminated `result`.`extensions` field. - - Attributes: - progress (float): Consists of the percentage of media consumed by the actor. - """ - - progress: NonNegativeFloat = Field(alias=VIDEO_EXTENSION_PROGRESS) - - -class VideoEnableClosedCaptioningResultExtensionsField(VideoResultExtensionsField): - """Pydantic model for video enable closed captioning `result`.`extensions` field. - - Attributes: - ccEnabled (bool): Indicates whether subtitles are enabled. - """ - - ccEnabled: bool = Field(alias=VIDEO_EXTENSION_CC_ENABLED) - - -class VideoPlayedResultField(ResultField): - """Pydantic model for video played `result` field. - - Attributes: - extensions (dict): See VideoResultExtensionsField. - """ - - extensions: VideoResultExtensionsField - - -class VideoPausedResultField(ResultField): - """Pydantic model for video paused `result` field. - - Attributes: - extensions (dict): See VideoPausedResultExtensionsField. - """ - - extensions: VideoPausedResultExtensionsField - - -class VideoSeekedResultField(ResultField): - """Pydantic model for video seeked `result` field. - - Attributes: - extensions (dict): See VideoSeekedResultExtensionsField. - """ - - extensions: VideoSeekedResultExtensionsField - - -class VideoCompletedResultField(ResultField): - """Pydantic model for video completed `result` field. - - Attributes: - extensions (dict): See VideoCompletedResultExtensionsField. - completion (bool): Consists of the value `True`. - duration (str): Consists of the total time spent consuming the video under - current registration. - """ - - extensions: VideoCompletedResultExtensionsField - completion: Optional[Literal[True]] - duration: Optional[timedelta] - - -class VideoTerminatedResultField(ResultField): - """Pydantic model for video terminated `result` field. - - Attributes: - extensions (dict): See VideoTerminatedResultExtensionsField. - """ - - extensions: VideoTerminatedResultExtensionsField - - -class VideoEnableClosedCaptioningResultField(ResultField): - """Pydantic model for video enable closed captioning `result` field. - - Attributes: - extensions (dict): See VideoEnableClosedCaptioningResultExtensionsField. - """ - - extensions: VideoEnableClosedCaptioningResultExtensionsField - - -class VideoVolumeChangeInteractionResultField(ResultField): - """Pydantic model for video volume change interaction `result` field. - - Attributes: - extensions (dict): See VideoResultExtensionsField. - """ - - extensions: VideoResultExtensionsField - - -class VideoScreenChangeInteractionResultField(ResultField): - """Pydantic model for video screen change interaction `result` field. - - Attributes: - extensions (dict): See VideoResultExtensionsField. - """ - - extensions: VideoResultExtensionsField diff --git a/src/ralph/models/xapi/video/fields/verbs.py b/src/ralph/models/xapi/video/fields/verbs.py deleted file mode 100644 index 81021b32c..000000000 --- a/src/ralph/models/xapi/video/fields/verbs.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Video xAPI events verb fields definitions.""" - -from typing import Dict - -from ...constants import ( - LANG_EN_US_DISPLAY, - VERB_COMPLETED_DISPLAY, - VERB_COMPLETED_ID, - VERB_INITIALIZED_DISPLAY, - VERB_INITIALIZED_ID, - VERB_INTERACTED_DISPLAY, - VERB_INTERACTED_ID, - VERB_PAUSED_DISPLAY, - VERB_PLAYED_DISPLAY, - VERB_SEEKED_DISPLAY, - VERB_TERMINATED_DISPLAY, - VERB_TERMINATED_ID, -) -from ...fields.verbs import VerbField -from ..constants import VERB_VIDEO_PAUSED_ID, VERB_VIDEO_PLAYED_ID, VERB_VIDEO_SEEKED_ID - - -class VideoInitializedVerbField(VerbField): - """Pydantic model for video initialized `verb` field. - - Attributes: - id (str): Consists of the value `http://adlnet.gov/expapi/verbs/initialized`. - display (Dict): Consists of the dictionary `{"en-US": "initialized"}`. - """ - - id: VERB_INITIALIZED_ID = VERB_INITIALIZED_ID.__args__[0] - display: Dict[LANG_EN_US_DISPLAY, VERB_INITIALIZED_DISPLAY] = { - LANG_EN_US_DISPLAY.__args__[0]: VERB_INITIALIZED_DISPLAY.__args__[0] - } - - -class VideoPlayedVerbField(VerbField): - """Pydantic model for video played `verb` field. - - Attributes: - id (str): Consists of the value `https://w3id.org/xapi/video/verbs/played`. - display (dict): Consists of the dictionary `{"en-US": "played"}`. - """ - - id: VERB_VIDEO_PLAYED_ID = VERB_VIDEO_PLAYED_ID.__args__[0] - display: Dict[LANG_EN_US_DISPLAY, VERB_PLAYED_DISPLAY] = { - LANG_EN_US_DISPLAY.__args__[0]: VERB_PLAYED_DISPLAY.__args__[0] - } - - -class VideoPausedVerbField(VerbField): - """Pydantic model for video paused `verb` field. - - Attributes: - id (str): Consists of the value `https://w3id.org/xapi/video/verbs/paused`. - display (dict): Consists of the dictionary `{"en-US": "paused"}`. - """ - - id: VERB_VIDEO_PAUSED_ID = VERB_VIDEO_PAUSED_ID.__args__[0] - display: Dict[LANG_EN_US_DISPLAY, VERB_PAUSED_DISPLAY] = { - LANG_EN_US_DISPLAY.__args__[0]: VERB_PAUSED_DISPLAY.__args__[0] - } - - -class VideoSeekedVerbField(VerbField): - """Pydantic model for video seeked `verb` field. - - Attributes: - id (str): Consists of the value `https://w3id.org/xapi/video/verbs/seeked`. - display (dict): Consists of the dictionary `{"en-US": "seeked"}`. - """ - - id: VERB_VIDEO_SEEKED_ID = VERB_VIDEO_SEEKED_ID.__args__[0] - display: Dict[LANG_EN_US_DISPLAY, VERB_SEEKED_DISPLAY] = { - LANG_EN_US_DISPLAY.__args__[0]: VERB_SEEKED_DISPLAY.__args__[0] - } - - -class VideoCompletedVerbField(VerbField): - """Pydantic model for video completed `verb` field. - - Attributes: - id (str): Consists of the value `http://adlnet.gov/expapi/verbs/completed`. - display (dict): Consists of the dictionary `{"en-US": "completed"}`. - """ - - id: VERB_COMPLETED_ID = VERB_COMPLETED_ID.__args__[0] - display: Dict[LANG_EN_US_DISPLAY, VERB_COMPLETED_DISPLAY] = { - LANG_EN_US_DISPLAY.__args__[0]: VERB_COMPLETED_DISPLAY.__args__[0] - } - - -class VideoTerminatedVerbField(VerbField): - """Pydantic model for video termainated `verb` field. - - Attributes: - id (str): Consists of the value `http://adlnet.gov/expapi/verbs/terminated`. - display (dict): Consists of the dictionary `{"en-US": "terminated"}`. - """ - - id: VERB_TERMINATED_ID = VERB_TERMINATED_ID.__args__[0] - display: Dict[LANG_EN_US_DISPLAY, VERB_TERMINATED_DISPLAY] = { - LANG_EN_US_DISPLAY.__args__[0]: VERB_TERMINATED_DISPLAY.__args__[0] - } - - -class VideoInteractedVerbField(VerbField): - """Pydantic model for video interacted `verb` field. - - Attributes: - id (str): Consists of the value `http://adlnet.gov/expapi/verbs/interacted`. - display (dict): Consists of the dictionary `{"en-US": "interacted"}`. - """ - - id: VERB_INTERACTED_ID = VERB_INTERACTED_ID.__args__[0] - display: Dict[LANG_EN_US_DISPLAY, VERB_INTERACTED_DISPLAY] = { - LANG_EN_US_DISPLAY.__args__[0]: VERB_INTERACTED_DISPLAY.__args__[0] - } diff --git a/src/ralph/models/xapi/video/results.py b/src/ralph/models/xapi/video/results.py new file mode 100644 index 000000000..796242f8d --- /dev/null +++ b/src/ralph/models/xapi/video/results.py @@ -0,0 +1,175 @@ +"""Video xAPI events result fields definitions.""" + +from datetime import timedelta +from typing import Optional + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +from pydantic import Field, NonNegativeFloat + +from ..base.results import BaseXapiResult +from ..concepts.constants.video import ( + CONTEXT_EXTENSION_CC_ENABLED, + CONTEXT_EXTENSION_PLAYED_SEGMENTS, + CONTEXT_EXTENSION_PROGRESS, + CONTEXT_EXTENSION_TIME, + CONTEXT_EXTENSION_TIME_FROM, + CONTEXT_EXTENSION_TIME_TO, +) +from ..config import BaseExtensionModelWithConfig + + +class VideoResultExtensions(BaseExtensionModelWithConfig): + """Pydantic model for video `result`.`extensions` property. + + Attributes: + playedSegments (str): Consists of parts of the video the actor watched during + current registration in chronological order (for example, + "0[.]5[,]12[.]22[,]15[.]55[,]55[.]99.33[,]99.33"). + time (float): Consists of the video time code when the event was emitted. + """ + + time: NonNegativeFloat = Field(alias=CONTEXT_EXTENSION_TIME) + playedSegments: Optional[str] = Field(alias=CONTEXT_EXTENSION_PLAYED_SEGMENTS) + + +class VideoPausedResultExtensions(VideoResultExtensions): + """Pydantic model for video paused `result`.`extensions` property. + + Attributes: + progress (float): Consists of the ratio of media consumed by the actor. + """ + + progress: Optional[NonNegativeFloat] = Field(alias=CONTEXT_EXTENSION_PROGRESS) + + +class VideoSeekedResultExtensions(BaseExtensionModelWithConfig): + """Pydantic model for video seeked `result`.`extensions` property. + + Attributes: + timeFrom (float): Consists of the point in time the actor changed from in a + media object during a seek operation. + timeTo (float): Consists of the point in time the actor changed to in a media + object during a seek operation. + """ + + timeFrom: NonNegativeFloat = Field(alias=CONTEXT_EXTENSION_TIME_FROM) + timeTo: NonNegativeFloat = Field(alias=CONTEXT_EXTENSION_TIME_TO) + + +class VideoCompletedResultExtensions(VideoResultExtensions): + """Pydantic model for video completed `result`.`extensions` property. + + Attributes: + progress (float): Consists of the percentage of media consumed by the actor. + """ + + progress: NonNegativeFloat = Field(alias=CONTEXT_EXTENSION_PROGRESS) + + +class VideoTerminatedResultExtensions(VideoResultExtensions): + """Pydantic model for video terminated `result`.`extensions` property. + + Attributes: + progress (float): Consists of the percentage of media consumed by the actor. + """ + + progress: NonNegativeFloat = Field(alias=CONTEXT_EXTENSION_PROGRESS) + + +class VideoEnableClosedCaptioningResultExtensions(VideoResultExtensions): + """Pydantic model for video enable closed captioning `result`.`extensions` property. + + Attributes: + ccEnabled (bool): Indicates whether subtitles are enabled. + """ + + ccEnabled: bool = Field(alias=CONTEXT_EXTENSION_CC_ENABLED) + + +class VideoPlayedResult(BaseXapiResult): + """Pydantic model for video played `result` property. + + Attributes: + extensions (dict): See VideoResultExtensions. + """ + + extensions: VideoResultExtensions + + +class VideoPausedResult(BaseXapiResult): + """Pydantic model for video paused `result` property. + + Attributes: + extensions (dict): See VideoPausedResultExtensions. + """ + + extensions: VideoPausedResultExtensions + + +class VideoSeekedResult(BaseXapiResult): + """Pydantic model for video seeked `result` property. + + Attributes: + extensions (dict): See VideoSeekedResultExtensions. + """ + + extensions: VideoSeekedResultExtensions + + +class VideoCompletedResult(BaseXapiResult): + """Pydantic model for video completed `result` property. + + Attributes: + extensions (dict): See VideoCompletedResultExtensions. + completion (bool): Consists of the value `True`. + duration (str): Consists of the total time spent consuming the video under + current registration. + """ + + extensions: VideoCompletedResultExtensions + completion: Optional[Literal[True]] + duration: Optional[timedelta] + + +class VideoTerminatedResult(BaseXapiResult): + """Pydantic model for video terminated `result` property. + + Attributes: + extensions (dict): See VideoTerminatedResultExtensions. + """ + + extensions: VideoTerminatedResultExtensions + + +class VideoEnableClosedCaptioningResult(BaseXapiResult): + """Pydantic model for video enable closed captioning `result` property. + + Attributes: + extensions (dict): See VideoEnableClosedCaptioningResultExtensions. + """ + + extensions: VideoEnableClosedCaptioningResultExtensions + + +class VideoVolumeChangeInteractionResult(BaseXapiResult): + """Pydantic model for video volume change interaction `result` property. + + Attributes: + extensions (dict): See VideoResultExtensions. + """ + + extensions: VideoResultExtensions + + +class VideoScreenChangeInteractionResult(BaseXapiResult): + """Pydantic model for video screen change interaction `result` property. + + Attributes: + extensions (dict): See VideoResultExtensions. + """ + + extensions: VideoResultExtensions diff --git a/src/ralph/models/xapi/video/statements.py b/src/ralph/models/xapi/video/statements.py index 42010dd76..0c3072ae8 100644 --- a/src/ralph/models/xapi/video/statements.py +++ b/src/ralph/models/xapi/video/statements.py @@ -3,48 +3,46 @@ from typing import Optional from ...selector import selector -from ..base import BaseXapiModel -from .fields.contexts import ( - VideoCompletedContextField, - VideoEnableClosedCaptioningContextField, - VideoInitializedContextField, - VideoPausedContextField, - VideoPlayedContextField, - VideoScreenChangeInteractionContextField, - VideoSeekedContextField, - VideoTerminatedContextField, - VideoVolumeChangeInteractionContextField, +from ..base.statements import BaseXapiStatement +from ..concepts.activity_types.video import VideoActivity +from ..concepts.verbs.scorm_profile import ( + CompletedVerb, + InitializedVerb, + InteractedVerb, + TerminatedVerb, ) -from .fields.objects import VideoObjectField -from .fields.results import ( - VideoCompletedResultField, - VideoEnableClosedCaptioningResultField, - VideoPausedResultField, - VideoPlayedResultField, - VideoScreenChangeInteractionResultField, - VideoSeekedResultField, - VideoTerminatedResultField, - VideoVolumeChangeInteractionResultField, +from ..concepts.verbs.video import PausedVerb, PlayedVerb, SeekedVerb +from .contexts import ( + VideoCompletedContext, + VideoEnableClosedCaptioningContext, + VideoInitializedContext, + VideoPausedContext, + VideoPlayedContext, + VideoScreenChangeInteractionContext, + VideoSeekedContext, + VideoTerminatedContext, + VideoVolumeChangeInteractionContext, ) -from .fields.verbs import ( - VideoCompletedVerbField, - VideoInitializedVerbField, - VideoInteractedVerbField, - VideoPausedVerbField, - VideoPlayedVerbField, - VideoSeekedVerbField, - VideoTerminatedVerbField, +from .results import ( + VideoCompletedResult, + VideoEnableClosedCaptioningResult, + VideoPausedResult, + VideoPlayedResult, + VideoScreenChangeInteractionResult, + VideoSeekedResult, + VideoTerminatedResult, + VideoVolumeChangeInteractionResult, ) -class BaseVideoStatement(BaseXapiModel): +class BaseVideoStatement(BaseXapiStatement): """Pydantic model for video core statements. Attributes: - object (dict): See VideoObjectField. + object (dict): See VideoActivity. """ - object: VideoObjectField + object: VideoActivity class VideoInitialized(BaseVideoStatement): @@ -53,8 +51,8 @@ class VideoInitialized(BaseVideoStatement): Example: A video has been fully initialized. Attributes: - verb (dict): See VideoInitializedVerbField. - context (dict): See VideoInitializedContextField. + verb (dict): See InitializedVerb. + context (dict): See VideoInitializedContext. """ __selector__ = selector( @@ -62,8 +60,8 @@ class VideoInitialized(BaseVideoStatement): verb__id="http://adlnet.gov/expapi/verbs/initialized", ) - verb: VideoInitializedVerbField = VideoInitializedVerbField() - context: VideoInitializedContextField + verb: InitializedVerb = InitializedVerb() + context: VideoInitializedContext class VideoPlayed(BaseVideoStatement): @@ -72,9 +70,9 @@ class VideoPlayed(BaseVideoStatement): Example: John played the video or clicked the play button. Attributes: - verb (dict): See VideoPlayedVerbField. - result (dict): See VideoPlayedResultField. - context (dict): See VideoPlayedContextField. + verb (dict): See PlayedVerb. + result (dict): See VideoPlayedResult. + context (dict): See VideoPlayedContext. """ __selector__ = selector( @@ -82,9 +80,9 @@ class VideoPlayed(BaseVideoStatement): verb__id="https://w3id.org/xapi/video/verbs/played", ) - verb: VideoPlayedVerbField = VideoPlayedVerbField() - result: VideoPlayedResultField - context: Optional[VideoPlayedContextField] + verb: PlayedVerb = PlayedVerb() + result: VideoPlayedResult + context: Optional[VideoPlayedContext] class VideoPaused(BaseVideoStatement): @@ -93,9 +91,9 @@ class VideoPaused(BaseVideoStatement): Example: John paused the video or clicked the pause button. Attributes: - verb (dict): See VideoPausedVerbField. - result (dict): See VideoPausedResultField. - context (dict): See VideoPausedContextField. + verb (dict): See PausedVerb. + result (dict): See VideoPausedResult. + context (dict): See VideoPausedContext. """ __selector__ = selector( @@ -103,9 +101,9 @@ class VideoPaused(BaseVideoStatement): verb__id="https://w3id.org/xapi/video/verbs/paused", ) - verb: VideoPausedVerbField = VideoPausedVerbField() - result: VideoPausedResultField - context: VideoPausedContextField + verb: PausedVerb = PausedVerb() + result: VideoPausedResult + context: VideoPausedContext class VideoSeeked(BaseVideoStatement): @@ -115,9 +113,9 @@ class VideoSeeked(BaseVideoStatement): video. Attributes: - verb (dict): See VideoSeekedVerbField. - result (dict): See VideoSeekedResultField. - context (dict): See VideoSeekedContextField. + verb (dict): See SeekedVerb. + result (dict): See VideoSeekedResult. + context (dict): See VideoSeekedContext. """ __selector__ = selector( @@ -125,9 +123,9 @@ class VideoSeeked(BaseVideoStatement): verb__id="https://w3id.org/xapi/video/verbs/seeked", ) - verb: VideoSeekedVerbField = VideoSeekedVerbField() - result: VideoSeekedResultField - context: Optional[VideoSeekedContextField] + verb: SeekedVerb = SeekedVerb() + result: VideoSeekedResult + context: Optional[VideoSeekedContext] class VideoCompleted(BaseVideoStatement): @@ -136,9 +134,9 @@ class VideoCompleted(BaseVideoStatement): Example: John completed a video by watching major parts of the video at least once. Attributes: - verb (dict): See VideoCompletedVerbField. - result (dict): See VideoCompletedResultField. - context (dict): See VideoCompletedContextField. + verb (dict): See CompletedVerb. + result (dict): See VideoCompletedResult. + context (dict): See VideoCompletedContext. """ __selector__ = selector( @@ -146,9 +144,9 @@ class VideoCompleted(BaseVideoStatement): verb__id="http://adlnet.gov/expapi/verbs/completed", ) - verb: VideoCompletedVerbField = VideoCompletedVerbField() - result: VideoCompletedResultField - context: VideoCompletedContextField + verb: CompletedVerb = CompletedVerb() + result: VideoCompletedResult + context: VideoCompletedContext class VideoTerminated(BaseVideoStatement): @@ -157,9 +155,9 @@ class VideoTerminated(BaseVideoStatement): Example: John ended a video (quit the player). Attributes: - verb (dict): See VideoTerminatedVerbField. - result (dict): See VideoTerminatedResultField. - context (dict): See VideoTerminatedContextField. + verb (dict): See TerminatedVerb. + result (dict): See VideoTerminatedResult. + context (dict): See VideoTerminatedContext. """ __selector__ = selector( @@ -167,9 +165,9 @@ class VideoTerminated(BaseVideoStatement): verb__id="http://adlnet.gov/expapi/verbs/terminated", ) - verb: VideoTerminatedVerbField = VideoTerminatedVerbField() - result: VideoTerminatedResultField - context: VideoTerminatedContextField + verb: TerminatedVerb = TerminatedVerb() + result: VideoTerminatedResult + context: VideoTerminatedContext class VideoEnableClosedCaptioning(BaseVideoStatement): @@ -178,9 +176,9 @@ class VideoEnableClosedCaptioning(BaseVideoStatement): Example: John interacted with the player to enable closed captioning. Attributes: - verb (dict): See VideoInteractedVerbField. - result (dict): See VideoEnableClosedCaptioningResultField. - context (dict): See VideoEnableClosedCaptioningContextField. + verb (dict): See InteractedVerb. + result (dict): See VideoEnableClosedCaptioningResult. + context (dict): See VideoEnableClosedCaptioningContext. """ __selector__ = selector( @@ -188,9 +186,9 @@ class VideoEnableClosedCaptioning(BaseVideoStatement): verb__id="http://adlnet.gov/expapi/verbs/interacted", ) - verb: VideoInteractedVerbField = VideoInteractedVerbField() - result: VideoEnableClosedCaptioningResultField - context: VideoEnableClosedCaptioningContextField + verb: InteractedVerb = InteractedVerb() + result: VideoEnableClosedCaptioningResult + context: VideoEnableClosedCaptioningContext class VideoVolumeChangeInteraction(BaseVideoStatement): @@ -198,10 +196,10 @@ class VideoVolumeChangeInteraction(BaseVideoStatement): Example: John interacted with the player to change the volume. - Attributes: - verb (dict): See VideoInteractedVerbField. - result (dict): See VideoVolumeChangeInteractionResultField. - context (dict): See VideoVolumeChangeInteractionContextField. + Attributes : + verb (dict): See InteractedVerb. + result (dict): See VideoVolumeChangeInteractionResult. + context (dict): See VideoVolumeChangeInteractionContext. """ __selector__ = selector( @@ -209,9 +207,9 @@ class VideoVolumeChangeInteraction(BaseVideoStatement): verb__id="http://adlnet.gov/expapi/verbs/interacted", ) - verb: VideoInteractedVerbField = VideoInteractedVerbField() - result: VideoVolumeChangeInteractionResultField - context: VideoVolumeChangeInteractionContextField + verb: InteractedVerb = InteractedVerb() + result: VideoVolumeChangeInteractionResult + context: VideoVolumeChangeInteractionContext class VideoScreenChangeInteraction(BaseVideoStatement): @@ -220,9 +218,9 @@ class VideoScreenChangeInteraction(BaseVideoStatement): Example: John interacted with the player to activate or deactivate full screen. Attributes: - verb (dict): See VideoInteractedVerbField. - result (dict): See VideoScreenChangeInteractionResultField. - context (dict): See VideoScreenChangeInteractionContextField. + verb (dict): See InteractedVerb. + result (dict): See VideoScreenChangeInteractionResult. + context (dict): See VideoScreenChangeInteractionContext. """ __selector__ = selector( @@ -230,6 +228,6 @@ class VideoScreenChangeInteraction(BaseVideoStatement): verb__id="http://adlnet.gov/expapi/verbs/interacted", ) - verb: VideoInteractedVerbField = VideoInteractedVerbField() - result: VideoScreenChangeInteractionResultField - context: VideoScreenChangeInteractionContextField + verb: InteractedVerb = InteractedVerb() + result: VideoScreenChangeInteractionResult + context: VideoScreenChangeInteractionContext diff --git a/tests/fixtures/hypothesis_configuration.py b/tests/fixtures/hypothesis_configuration.py index 58d7542f8..f7c7844b0 100644 --- a/tests/fixtures/hypothesis_configuration.py +++ b/tests/fixtures/hypothesis_configuration.py @@ -6,7 +6,7 @@ from hypothesis import strategies as st from pydantic import AnyHttpUrl, AnyUrl, StrictStr -from ralph.models.xapi.fields.common import IRI, LanguageTag, MailtoEmail +from ralph.models.xapi.base.common import IRI, LanguageTag, MailtoEmail settings.register_profile("development", max_examples=1) settings.load_profile("development") diff --git a/tests/fixtures/hypothesis_strategies.py b/tests/fixtures/hypothesis_strategies.py index 2de77a204..a393eec3c 100644 --- a/tests/fixtures/hypothesis_strategies.py +++ b/tests/fixtures/hypothesis_strategies.py @@ -9,8 +9,8 @@ from ralph.models.edx.navigational.fields.events import NavigationalEventField from ralph.models.edx.navigational.statements import UISeqNext, UISeqPrev -from ralph.models.xapi.fields.contexts import ContextField -from ralph.models.xapi.fields.results import ScoreResultField +from ralph.models.xapi.base.contexts import BaseXapiContext +from ralph.models.xapi.base.results import BaseXapiResultScore OVERWRITTEN_STRATEGIES = {} @@ -98,11 +98,11 @@ def custom_given(*args: Union[st.SearchStrategy, BaseModel], **kwargs): UISeqNext: { # pylint: disable=unhashable-member "event": custom_builds(NavigationalEventField, old=st.just(0), new=st.just(1)) }, - ContextField: { # pylint: disable=unhashable-member + BaseXapiContext: { # pylint: disable=unhashable-member "revision": False, "platform": False, }, - ScoreResultField: { # pylint: disable=unhashable-member + BaseXapiResultScore: { # pylint: disable=unhashable-member "raw": False, "min": False, "max": False, diff --git a/tests/models/edx/converters/xapi/test_navigational.py b/tests/models/edx/converters/xapi/test_navigational.py index 5d7b8ec28..53768ae69 100644 --- a/tests/models/edx/converters/xapi/test_navigational.py +++ b/tests/models/edx/converters/xapi/test_navigational.py @@ -37,14 +37,12 @@ def test_navigational_ui_page_close_to_page_terminated( }, "object": { "definition": { - "name": {"en-US": "page"}, "type": "http://activitystrea.ms/schema/1.0/page", }, "id": event["page"], }, "timestamp": event["time"], "verb": { - "display": {"en-US": "terminated"}, "id": "http://adlnet.gov/expapi/verbs/terminated", }, "version": "1.0.0", diff --git a/tests/models/edx/converters/xapi/test_server.py b/tests/models/edx/converters/xapi/test_server.py index 3d4ccbc24..cac13e3ec 100644 --- a/tests/models/edx/converters/xapi/test_server.py +++ b/tests/models/edx/converters/xapi/test_server.py @@ -58,14 +58,12 @@ def test_models_edx_converters_xapi_server_server_event_to_xapi_convert_with_val }, "object": { "definition": { - "name": {"en-US": "page"}, "type": "http://activitystrea.ms/schema/1.0/page", }, "id": platform_url + "/main/blog", }, "timestamp": event["time"], "verb": { - "display": {"en-US": "viewed"}, "id": "http://id.tincanapi.com/verb/viewed", }, "version": "1.0.0", diff --git a/tests/models/edx/converters/xapi/test_video.py b/tests/models/edx/converters/xapi/test_video.py index 1b871e853..06342df16 100644 --- a/tests/models/edx/converters/xapi/test_video.py +++ b/tests/models/edx/converters/xapi/test_video.py @@ -45,10 +45,7 @@ def test_ui_load_video_to_video_initialized(uuid_namespace, event, platform_url) assert xapi_event_dict == { "id": str(uuid5(UUID(uuid_namespace), event_str)), "actor": {"account": {"homePage": platform_url, "name": "1"}}, - "verb": { - "id": "http://adlnet.gov/expapi/verbs/initialized", - "display": {"en-US": "initialized"}, - }, + "verb": {"id": "http://adlnet.gov/expapi/verbs/initialized"}, "context": { "extensions": { "https://w3id.org/xapi/video/extensions/length": 0.0, @@ -93,10 +90,7 @@ def test_ui_play_video_to_video_played(uuid_namespace, event, platform_url): assert xapi_event_dict == { "id": str(uuid5(UUID(uuid_namespace), event_str)), "actor": {"account": {"homePage": platform_url, "name": "1"}}, - "verb": { - "id": "https://w3id.org/xapi/video/verbs/played", - "display": {"en-US": "played"}, - }, + "verb": {"id": "https://w3id.org/xapi/video/verbs/played"}, "object": { "id": platform_url + "/xblock/block-v1:" @@ -146,10 +140,7 @@ def test_ui_pause_video_to_video_paused(uuid_namespace, event, platform_url): assert xapi_event_dict == { "id": str(uuid5(UUID(uuid_namespace), event_str)), "actor": {"account": {"homePage": platform_url, "name": "1"}}, - "verb": { - "id": "https://w3id.org/xapi/video/verbs/paused", - "display": {"en-US": "paused"}, - }, + "verb": {"id": "https://w3id.org/xapi/video/verbs/paused"}, "object": { "id": platform_url + "/xblock/block-v1:" @@ -200,10 +191,7 @@ def test_ui_stop_video_to_video_terminated(uuid_namespace, event, platform_url): assert xapi_event_dict == { "id": str(uuid5(UUID(uuid_namespace), event_str)), "actor": {"account": {"homePage": platform_url, "name": "1"}}, - "verb": { - "id": "http://adlnet.gov/expapi/verbs/terminated", - "display": {"en-US": "terminated"}, - }, + "verb": {"id": "http://adlnet.gov/expapi/verbs/terminated"}, "object": { "id": platform_url + "/xblock/block-v1:" @@ -255,10 +243,7 @@ def test_ui_seek_video_to_video_seeked(uuid_namespace, event, platform_url): assert xapi_event_dict == { "id": str(uuid5(UUID(uuid_namespace), event_str)), "actor": {"account": {"homePage": platform_url, "name": "1"}}, - "verb": { - "id": "https://w3id.org/xapi/video/verbs/seeked", - "display": {"en-US": "seeked"}, - }, + "verb": {"id": "https://w3id.org/xapi/video/verbs/seeked"}, "object": { "id": platform_url + "/xblock/block-v1:" diff --git a/tests/models/test_converter.py b/tests/models/test_converter.py index 5b9b58e72..74fc127e7 100644 --- a/tests/models/test_converter.py +++ b/tests/models/test_converter.py @@ -23,7 +23,7 @@ ) from ralph.models.edx.converters.xapi.base import BaseConversionSet from ralph.models.edx.navigational.statements import UIPageClose -from ralph.models.xapi.constants import VERB_TERMINATED_ID +from ralph.models.xapi.concepts.constants.scorm_profile import VERB_ID_TERMINATED from tests.fixtures.hypothesis_strategies import custom_given @@ -425,7 +425,7 @@ def test_converter_convert_with_valid_events( result = Converter( platform_url="https://fun-mooc.fr", uuid_namespace=valid_uuid ).convert([event_str], ignore_errors, fail_on_unknown) - assert json.loads(next(result))["verb"]["id"] == VERB_TERMINATED_ID.__args__[0] + assert json.loads(next(result))["verb"]["id"] == VERB_ID_TERMINATED.__args__[0] @settings(suppress_health_check=(HealthCheck.function_scoped_fixture,)) diff --git a/tests/models/xapi/fields/__init__.py b/tests/models/xapi/base/__init__.py similarity index 100% rename from tests/models/xapi/fields/__init__.py rename to tests/models/xapi/base/__init__.py diff --git a/tests/models/xapi/base/test_agents.py b/tests/models/xapi/base/test_agents.py new file mode 100644 index 000000000..739c1f6be --- /dev/null +++ b/tests/models/xapi/base/test_agents.py @@ -0,0 +1,47 @@ +"""Tests for the base xAPI `Agent` definitions.""" + +import json +import re + +import pytest +from pydantic import ValidationError + +from ralph.models.xapi.base.agents import BaseXapiAgentWithMboxSha1SumIFI + +from tests.fixtures.hypothesis_strategies import custom_given + + +@custom_given(BaseXapiAgentWithMboxSha1SumIFI) +def test_models_xapi_base_agent_with_mbox_sha1_sum_ifi_with_valid_field( + field, +): + """Tests a valid BaseXapiAgentWithMboxSha1SumIFI has the expected + `mbox_sha1sum` regex. + """ + + assert re.match(r"^[0-9a-f]{40}$", field.mbox_sha1sum) + + +@pytest.mark.parametrize( + "mbox_sha1sum", + [ + "1baccll9xkidkd4re9n24djgfh939g7dhyjm3li3", + "1baccde9", + "1baccdd9abcdfd4ae9b24dedfa939c7deffa3db3a7", + ], +) +@custom_given(BaseXapiAgentWithMboxSha1SumIFI) +def test_models_xapi_base_agent_with_mbox_sha1_sum_ifi_with_invalid_field( + mbox_sha1sum, field +): + """Tests an invalid `mbox_sha1sum` property in + BaseXapiAgentWithMboxSha1SumIFI raises a `ValidationError`. + """ + + invalid_field = json.loads(field.json()) + invalid_field["mbox_sha1sum"] = mbox_sha1sum + + with pytest.raises( + ValidationError, match="mbox_sha1sum\n string does not match regex" + ): + BaseXapiAgentWithMboxSha1SumIFI(**invalid_field) diff --git a/tests/models/xapi/fields/test_common.py b/tests/models/xapi/base/test_common.py similarity index 98% rename from tests/models/xapi/fields/test_common.py rename to tests/models/xapi/base/test_common.py index 43a883382..0beae8880 100644 --- a/tests/models/xapi/fields/test_common.py +++ b/tests/models/xapi/base/test_common.py @@ -3,7 +3,7 @@ import pytest from pydantic import BaseModel, ValidationError -from ralph.models.xapi.fields.common import IRI, LanguageMap, LanguageTag +from ralph.models.xapi.base.common import IRI, LanguageMap, LanguageTag @pytest.mark.parametrize( diff --git a/tests/models/xapi/base/test_groups.py b/tests/models/xapi/base/test_groups.py new file mode 100644 index 000000000..9486e00cd --- /dev/null +++ b/tests/models/xapi/base/test_groups.py @@ -0,0 +1,16 @@ +"""Tests for the base xAPI `Grou` definitions.""" + +from ralph.models.xapi.base.groups import BaseXapiGroupCommonProperties + +from tests.fixtures.hypothesis_strategies import custom_given + + +@custom_given(BaseXapiGroupCommonProperties) +def test_models_xapi_base_actor_group_common_properties_with_valid_field( + field, +): + """Tests a valid BaseXapiGroupCommonProperties has the expected + `objectType` value. + """ + + assert field.objectType == "Group" diff --git a/tests/models/xapi/base/test_objects.py b/tests/models/xapi/base/test_objects.py new file mode 100644 index 000000000..3c30cea0f --- /dev/null +++ b/tests/models/xapi/base/test_objects.py @@ -0,0 +1,12 @@ +"""Tests for the base xAPI `Object` definitions.""" + +from ralph.models.xapi.base.objects import BaseXapiSubStatement + +from tests.fixtures.hypothesis_strategies import custom_given + + +@custom_given(BaseXapiSubStatement) +def test_models_xapi_object_base_sub_statement_type_with_valid_field(field): + """Tests a valid BaseXapiSubStatement has the expected `objectType` value.""" + + assert field.objectType == "SubStatement" diff --git a/tests/models/xapi/base/test_results.py b/tests/models/xapi/base/test_results.py new file mode 100644 index 000000000..7fa39001b --- /dev/null +++ b/tests/models/xapi/base/test_results.py @@ -0,0 +1,35 @@ +"""Tests for the base xAPI `Actor` definitions.""" + +import json + +import pytest +from pydantic import ValidationError + +from ralph.models.xapi.base.results import BaseXapiResultScore + +from tests.fixtures.hypothesis_strategies import custom_given + + +@pytest.mark.parametrize( + "raw_value, min_value, max_value, error_msg", + [ + (2, 5, 10, "min cannot be greater than raw"), + (2, 10, 5, "min cannot be greater than max"), + (12, 5, 10, "raw cannot be greater than max"), + ], +) +@custom_given(BaseXapiResultScore) +def test_models_xapi_base_result_score_with_invalid_raw_min_max_relation( + raw_value, min_value, max_value, error_msg, field +): + """Tests invalids `raw`,`min`,`max` relation in BaseXapiResultScore raises + ValidationError. + """ + + invalid_field = json.loads(field.json()) + invalid_field["raw"] = raw_value + invalid_field["min"] = min_value + invalid_field["max"] = max_value + + with pytest.raises(ValidationError, match=error_msg): + BaseXapiResultScore(**invalid_field) diff --git a/tests/models/xapi/test_base.py b/tests/models/xapi/base/test_statements.py similarity index 77% rename from tests/models/xapi/test_base.py rename to tests/models/xapi/base/test_statements.py index 9a658dbc9..b28090fd7 100644 --- a/tests/models/xapi/test_base.py +++ b/tests/models/xapi/base/test_statements.py @@ -1,4 +1,4 @@ -"""Tests for the BaseXapiModel.""" +"""Tests for the BaseXapiStatement.""" import pytest from hypothesis import settings @@ -6,22 +6,23 @@ from pydantic import ValidationError from ralph.models.selector import ModelSelector -from ralph.models.xapi.base import BaseXapiModel -from ralph.models.xapi.fields.actors import ( - AccountActorField, - AccountGroupActorField, - AnonymousGroupActorField, - MboxGroupActorField, - MboxSha1SumGroupActorField, - OpenIdGroupActorField, +from ralph.models.xapi.base.agents import BaseXapiAgentWithAccountIFI +from ralph.models.xapi.base.groups import ( + BaseXapiAnonymousGroup, + BaseXapiIdentifiedGroupWithAccountIFI, + BaseXapiIdentifiedGroupWithMboxIFI, + BaseXapiIdentifiedGroupWithMboxSha1SumIFI, + BaseXapiIdentifiedGroupWithOpenIdIFI, ) -from ralph.models.xapi.fields.objects import SubStatementObjectField -from ralph.models.xapi.fields.unnested_objects import ( - ActivityObjectField, - InteractionObjectDefinitionField, - StatementRefObjectField, +from ralph.models.xapi.base.objects import BaseXapiSubStatement +from ralph.models.xapi.base.statements import BaseXapiStatement +from ralph.models.xapi.base.unnested_objects import ( + BaseXapiActivity, + BaseXapiActivityInteractionDefinition, + BaseXapiStatementRef, ) from ralph.models.xapi.video.statements import BaseVideoStatement +from ralph.models.xapi.virtual_classroom.statements import BaseVirtualClassroomStatement from ralph.utils import set_dict_value_from_path from tests.fixtures.hypothesis_strategies import custom_builds, custom_given @@ -32,7 +33,7 @@ ["id", "stored", "verb__display", "context__contextActivities__parent"], ) @pytest.mark.parametrize("value", [None, "", {}]) -@custom_given(BaseXapiModel) +@custom_given(BaseXapiStatement) def test_models_xapi_base_statement_with_invalid_null_values(path, value, statement): """Tests that the statement does not accept any null values. @@ -44,7 +45,7 @@ def test_models_xapi_base_statement_with_invalid_null_values(path, value, statem statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) with pytest.raises(ValidationError, match="invalid empty value"): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) @pytest.mark.parametrize( @@ -56,7 +57,7 @@ def test_models_xapi_base_statement_with_invalid_null_values(path, value, statem ], ) @pytest.mark.parametrize("value", [None, "", {}]) -@custom_given(custom_builds(BaseXapiModel, object=custom_builds(ActivityObjectField))) +@custom_given(custom_builds(BaseXapiStatement, object=custom_builds(BaseXapiActivity))) def test_models_xapi_base_statement_with_valid_null_values(path, value, statement): """Tests that the statement does accept valid null values in extensions fields. @@ -68,7 +69,7 @@ def test_models_xapi_base_statement_with_valid_null_values(path, value, statemen statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) try: - BaseXapiModel(**statement) + BaseXapiStatement(**statement) except ValidationError as err: pytest.fail(f"Valid statement should not raise exceptions: {err}") @@ -76,10 +77,10 @@ def test_models_xapi_base_statement_with_valid_null_values(path, value, statemen @pytest.mark.parametrize("path", ["object__definition__correctResponsesPattern"]) @custom_given( custom_builds( - BaseXapiModel, + BaseXapiStatement, object=custom_builds( - ActivityObjectField, - definition=custom_builds(InteractionObjectDefinitionField), + BaseXapiActivity, + definition=custom_builds(BaseXapiActivityInteractionDefinition), ), ) ) @@ -92,7 +93,7 @@ def test_models_xapi_base_statement_with_valid_empty_array(path, statement): statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), []) try: - BaseXapiModel(**statement) + BaseXapiStatement(**statement) except ValidationError as err: pytest.fail(f"Valid statement should not raise exceptions: {err}") @@ -101,7 +102,7 @@ def test_models_xapi_base_statement_with_valid_empty_array(path, statement): "field", ["actor", "verb", "object"], ) -@custom_given(BaseXapiModel) +@custom_given(BaseXapiStatement) def test_models_xapi_base_statement_must_use_actor_verb_and_object(field, statement): """Tests that the statement raises an exception if required fields are missing. @@ -118,7 +119,7 @@ def test_models_xapi_base_statement_must_use_actor_verb_and_object(field, statem statement = statement.dict(exclude_none=True) del statement[field] with pytest.raises(ValidationError, match="field required"): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) @pytest.mark.parametrize( @@ -133,7 +134,9 @@ def test_models_xapi_base_statement_must_use_actor_verb_and_object(field, statem ("object__id", ["foo"]), # Should be an IRI ], ) -@custom_given(custom_builds(BaseXapiModel, actor=custom_builds(AccountActorField))) +@custom_given( + custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAgentWithAccountIFI)) +) def test_models_xapi_base_statement_with_invalid_data_types(path, value, statement): """Tests that the statement does not accept values with wrong types. @@ -145,7 +148,7 @@ def test_models_xapi_base_statement_with_invalid_data_types(path, value, stateme set_dict_value_from_path(statement, path.split("__"), value) err = "(type expected|not a valid dict|expected string )" with pytest.raises(ValidationError, match=err): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) @pytest.mark.parametrize( @@ -157,7 +160,9 @@ def test_models_xapi_base_statement_with_invalid_data_types(path, value, stateme ("object__id", ["This is not an IRI"]), # Should be an IRI ], ) -@custom_given(custom_builds(BaseXapiModel, actor=custom_builds(AccountActorField))) +@custom_given( + custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAgentWithAccountIFI)) +) def test_models_xapi_base_statement_with_invalid_data_format(path, value, statement): """Tests that the statement does not accept values having a wrong format. @@ -171,11 +176,13 @@ def test_models_xapi_base_statement_with_invalid_data_format(path, value, statem set_dict_value_from_path(statement, path.split("__"), value) err = "(Invalid `mailto:email`|Invalid RFC 5646 Language tag|not a valid uuid)" with pytest.raises(ValidationError, match=err): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) @pytest.mark.parametrize("path,value", [("actor__objecttype", "Agent")]) -@custom_given(custom_builds(BaseXapiModel, actor=custom_builds(AccountActorField))) +@custom_given( + custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAgentWithAccountIFI)) +) def test_models_xapi_base_statement_with_invalid_letter_cases(path, value, statement): """Tests that the statement does not accept keys having invalid letter cases. @@ -189,11 +196,11 @@ def test_models_xapi_base_statement_with_invalid_letter_cases(path, value, state set_dict_value_from_path(statement, path.split("__"), value) err = "(unexpected value|extra fields not permitted)" with pytest.raises(ValidationError, match=err): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) -@custom_given(BaseXapiModel) -def test_models_xapi_base_statement_should_not_accept_additional_properies(statement): +@custom_given(BaseXapiStatement) +def test_models_xapi_base_statement_should_not_accept_additional_properties(statement): """Tests that the statement does not accept additional properties. XAPI-00010 @@ -203,11 +210,11 @@ def test_models_xapi_base_statement_should_not_accept_additional_properies(state invalid_statement = statement.dict(exclude_none=True) invalid_statement["NEW_INVALID_FIELD"] = "some value" with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiModel(**invalid_statement) + BaseXapiStatement(**invalid_statement) @pytest.mark.parametrize("path,value", [("object__id", "w3id.org/xapi/video")]) -@custom_given(BaseXapiModel) +@custom_given(BaseXapiStatement) def test_models_xapi_base_statement_with_iri_wihout_scheme(path, value, statement): """Tests that the statement does not accept IRIs without a scheme. @@ -218,7 +225,7 @@ def test_models_xapi_base_statement_with_iri_wihout_scheme(path, value, statemen statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) with pytest.raises(ValidationError, match="is not a valid 'IRI'"): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) @pytest.mark.parametrize( @@ -229,7 +236,7 @@ def test_models_xapi_base_statement_with_iri_wihout_scheme(path, value, statemen "context__extensions__w3id.org/xapi/video", ], ) -@custom_given(custom_builds(BaseXapiModel, object=custom_builds(ActivityObjectField))) +@custom_given(custom_builds(BaseXapiStatement, object=custom_builds(BaseXapiActivity))) def test_models_xapi_base_statement_with_invalid_extensions(path, statement): """Tests that the statement does not accept extensions keys with invalid IRIs. @@ -240,11 +247,13 @@ def test_models_xapi_base_statement_with_invalid_extensions(path, statement): statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), "") with pytest.raises(ValidationError, match="is not a valid 'IRI'"): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) @pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) -@custom_given(custom_builds(BaseXapiModel, actor=custom_builds(AccountActorField))) +@custom_given( + custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAgentWithAccountIFI)) +) def test_models_xapi_base_statement_with_two_agent_types(path, value, statement): """Tests that the statement does not accept multiple agent types. @@ -253,11 +262,11 @@ def test_models_xapi_base_statement_with_two_agent_types(path, value, statement) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) @custom_given( - custom_builds(BaseXapiModel, actor=custom_builds(AnonymousGroupActorField)) + custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAnonymousGroup)) ) def test_models_xapi_base_statement_missing_member_property(statement): """Tests that the statement does not accept group agents with missing members. @@ -267,26 +276,38 @@ def test_models_xapi_base_statement_missing_member_property(statement): statement = statement.dict(exclude_none=True) del statement["actor"]["member"] with pytest.raises(ValidationError, match="member\n field required"): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) @pytest.mark.parametrize( "value", [ - AnonymousGroupActorField, - MboxGroupActorField, - MboxSha1SumGroupActorField, - OpenIdGroupActorField, - AccountGroupActorField, + BaseXapiAnonymousGroup, + BaseXapiIdentifiedGroupWithMboxIFI, + BaseXapiIdentifiedGroupWithMboxSha1SumIFI, + BaseXapiIdentifiedGroupWithOpenIdIFI, + BaseXapiIdentifiedGroupWithAccountIFI, ], ) @custom_given( st.one_of( - custom_builds(BaseXapiModel, actor=custom_builds(AnonymousGroupActorField)), - custom_builds(BaseXapiModel, actor=custom_builds(MboxGroupActorField)), - custom_builds(BaseXapiModel, actor=custom_builds(MboxSha1SumGroupActorField)), - custom_builds(BaseXapiModel, actor=custom_builds(OpenIdGroupActorField)), - custom_builds(BaseXapiModel, actor=custom_builds(AccountGroupActorField)), + custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAnonymousGroup)), + custom_builds( + BaseXapiStatement, + actor=custom_builds(BaseXapiIdentifiedGroupWithMboxIFI), + ), + custom_builds( + BaseXapiStatement, + actor=custom_builds(BaseXapiIdentifiedGroupWithMboxSha1SumIFI), + ), + custom_builds( + BaseXapiStatement, + actor=custom_builds(BaseXapiIdentifiedGroupWithOpenIdIFI), + ), + custom_builds( + BaseXapiStatement, + actor=custom_builds(BaseXapiIdentifiedGroupWithAccountIFI), + ), ), st.data(), ) @@ -301,11 +322,16 @@ def test_models_xapi_base_statement_with_invalid_group_objects(value, statement, statement["actor"]["member"] = [data.draw(custom_builds(value)).dict(**kwargs)] err = "actor -> member -> 0 -> objectType\n unexpected value; permitted: 'Agent'" with pytest.raises(ValidationError, match=err): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) @pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) -@custom_given(custom_builds(BaseXapiModel, actor=custom_builds(AccountGroupActorField))) +@custom_given( + custom_builds( + BaseXapiStatement, + actor=custom_builds(BaseXapiIdentifiedGroupWithAccountIFI), + ) +) def test_models_xapi_base_statement_with_two_group_identifiers(path, value, statement): """Tests that the statement does not accept multiple group identifiers. @@ -314,7 +340,7 @@ def test_models_xapi_base_statement_with_two_group_identifiers(path, value, stat statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) @pytest.mark.parametrize( @@ -327,7 +353,7 @@ def test_models_xapi_base_statement_with_two_group_identifiers(path, value, stat ], ) @custom_given( - custom_builds(BaseXapiModel, object=custom_builds(SubStatementObjectField)) + custom_builds(BaseXapiStatement, object=custom_builds(BaseXapiSubStatement)) ) def test_models_xapi_base_statement_with_sub_statement_ref(path, value, statement): """Tests that the sub-statement does not accept invalid properties. @@ -338,7 +364,7 @@ def test_models_xapi_base_statement_with_sub_statement_ref(path, value, statemen statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) @pytest.mark.parametrize( @@ -351,10 +377,10 @@ def test_models_xapi_base_statement_with_sub_statement_ref(path, value, statemen ) @custom_given( custom_builds( - BaseXapiModel, + BaseXapiStatement, object=custom_builds( - ActivityObjectField, - definition=custom_builds(InteractionObjectDefinitionField), + BaseXapiActivity, + definition=custom_builds(BaseXapiActivityInteractionDefinition), ), ) ) @@ -369,7 +395,7 @@ def test_models_xapi_base_statement_with_invalid_interaction_object(value, state set_dict_value_from_path(statement, path, value) err = "(Duplicate InteractionComponents are not valid|string does not match regex)" with pytest.raises(ValidationError, match=err): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) @pytest.mark.parametrize( @@ -381,8 +407,8 @@ def test_models_xapi_base_statement_with_invalid_interaction_object(value, state ) @custom_given( st.one_of( - custom_builds(BaseXapiModel, object=custom_builds(SubStatementObjectField)), - custom_builds(BaseXapiModel, object=custom_builds(StatementRefObjectField)), + custom_builds(BaseXapiStatement, object=custom_builds(BaseXapiSubStatement)), + custom_builds(BaseXapiStatement, object=custom_builds(BaseXapiStatementRef)), ), ) def test_models_xapi_base_statement_with_invalid_context_value(path, value, statement): @@ -395,11 +421,11 @@ def test_models_xapi_base_statement_with_invalid_context_value(path, value, stat set_dict_value_from_path(statement, path.split("__"), value) err = "properties can only be used if the Statement's Object is an Activity" with pytest.raises(ValidationError, match=err): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) @pytest.mark.parametrize("path", [("context.contextActivities.not_parent")]) -@custom_given(BaseXapiModel) +@custom_given(BaseXapiStatement) def test_models_xapi_base_statement_with_invalid_context_activities(path, statement): """Tests that the statement does not accept invalid context activity properties. @@ -409,7 +435,7 @@ def test_models_xapi_base_statement_with_invalid_context_activities(path, statem statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("."), {"id": "http://w3id.org/xapi"}) with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) @pytest.mark.parametrize( @@ -420,7 +446,7 @@ def test_models_xapi_base_statement_with_invalid_context_activities(path, statem [{"id": "http://w3id.org/xapi"}, {"id": "http://w3id.org/xapi/video"}], ], ) -@custom_given(BaseXapiModel) +@custom_given(BaseXapiStatement) def test_models_xapi_base_statement_with_valid_context_activities(value, statement): """Tests that the statement does accept valid context activities fields. @@ -432,13 +458,13 @@ def test_models_xapi_base_statement_with_valid_context_activities(value, stateme for activity in ["parent", "grouping", "category", "other"]: set_dict_value_from_path(statement, path + [activity], value) try: - BaseXapiModel(**statement) + BaseXapiStatement(**statement) except ValidationError as err: pytest.fail(f"Valid statement should not raise exceptions: {err}") @pytest.mark.parametrize("value", ["0.0.0", "1.1.0", "1", "2", "1.10.1", "1.0.1.1"]) -@custom_given(BaseXapiModel) +@custom_given(BaseXapiStatement) def test_models_xapi_base_statement_with_invalid_version(value, statement): """Tests that the statement does not accept an invalid version field. @@ -448,10 +474,10 @@ def test_models_xapi_base_statement_with_invalid_version(value, statement): statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, ["version"], value) with pytest.raises(ValidationError, match="version\n string does not match regex"): - BaseXapiModel(**statement) + BaseXapiStatement(**statement) -@custom_given(BaseXapiModel) +@custom_given(BaseXapiStatement) def test_models_xapi_base_statement_with_valid_version(statement): """Tests that the statement does accept a valid version field. @@ -460,9 +486,9 @@ def test_models_xapi_base_statement_with_valid_version(statement): """ statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, ["version"], "1.0.3") - assert "1.0.3" == BaseXapiModel(**statement).dict()["version"] + assert "1.0.3" == BaseXapiStatement(**statement).dict()["version"] del statement["version"] - assert "1.0.0" == BaseXapiModel(**statement).dict()["version"] + assert "1.0.0" == BaseXapiStatement(**statement).dict()["version"] @settings(deadline=None) @@ -471,9 +497,10 @@ def test_models_xapi_base_statement_with_valid_version(statement): [ model for model in ModelSelector("ralph.models.xapi").model_rules - # We have to bypass Video Statements in this test because we want to support + # We have to bypass Video and Virtual Classroom Statements in + # this test because we want to support # invalid values (non IRI keys) in their extension fields. - if not issubclass(model, BaseVideoStatement) + if not issubclass(model, (BaseVideoStatement, BaseVirtualClassroomStatement)) ], ) @custom_given(st.data()) @@ -481,10 +508,10 @@ def test_models_xapi_base_statement_should_consider_valid_all_defined_xapi_model model, data ): """Tests that all defined xAPI models in the ModelSelector make valid statements.""" - # All specific xAPI models should inherit BaseXapiModel - assert issubclass(model, BaseXapiModel) + # All specific xAPI models should inherit BaseXapiStatement + assert issubclass(model, BaseXapiStatement) statement = data.draw(custom_builds(model)).dict(exclude_none=True) try: - BaseXapiModel(**statement) + BaseXapiStatement(**statement) except ValidationError as err: - pytest.fail(f"Specific xAPI models should be valid BaseXapiModels: {err}") + pytest.fail(f"Specific xAPI models should be valid BaseXapiStatements: {err}") diff --git a/tests/models/xapi/base/test_unnested_objects.py b/tests/models/xapi/base/test_unnested_objects.py new file mode 100644 index 000000000..1b96c42c8 --- /dev/null +++ b/tests/models/xapi/base/test_unnested_objects.py @@ -0,0 +1,72 @@ +"""Tests for the base xAPI `Object` definitions.""" + +import json +import re + +import pytest +from pydantic import ValidationError + +from ralph.models.xapi.base.unnested_objects import ( + BaseXapiActivityInteractionDefinition, + BaseXapiInteractionComponent, + BaseXapiStatementRef, +) + +from tests.fixtures.hypothesis_strategies import custom_given + + +@custom_given(BaseXapiStatementRef) +def test_models_xapi_base_object_statement_ref_type_with_valid_field(field): + """Tests a valid BaseXapiStatementRef has the expected `objectType` value.""" + + assert field.objectType == "StatementRef" + + +@custom_given(BaseXapiInteractionComponent) +def test_models_xapi_base_object_interaction_component_with_valid_field( + field, +): + """Tests a valid BaseXapiInteractionComponent has the expected `id` regex.""" + + assert re.match(r"^[^\s]+$", field.id) + + +@pytest.mark.parametrize( + "id_value", + [" test_id", "\ntest"], +) +@custom_given(BaseXapiInteractionComponent) +def test_models_xapi_base_object_interaction_component_with_invalid_field( + id_value, field +): + """Tests a invalid `id` property in + BaseXapiInteractionComponent raises a `ValidationError`. + """ + + invalid_property = json.loads(field.json()) + invalid_property["id"] = id_value + + with pytest.raises(ValidationError, match="id\n string does not match regex"): + BaseXapiInteractionComponent(**invalid_property) + + +@custom_given(BaseXapiActivityInteractionDefinition) +def test_models_xapi_base_object_activity_type_interaction_definition_with_valid_field( + field, +): + """Tests a valid BaseXapiActivityInteractionDefinition has the expected + `objectType` value. + """ + + assert field.interactionType in ( + "true-false", + "choice", + "fill-in", + "long-fill-in", + "matching", + "performance", + "sequencing", + "likert", + "numeric", + "other", + ) diff --git a/tests/models/xapi/concepts/__init__.py b/tests/models/xapi/concepts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/models/xapi/concepts/test_activity_types.py b/tests/models/xapi/concepts/test_activity_types.py new file mode 100644 index 000000000..84d36d343 --- /dev/null +++ b/tests/models/xapi/concepts/test_activity_types.py @@ -0,0 +1,74 @@ +"""Tests for the xAPI activity types concepts.""" + +from ralph.models.xapi.concepts.activity_types.acrossx_profile import MessageActivity +from ralph.models.xapi.concepts.activity_types.activity_streams_vocabulary import ( + PageActivity, +) +from ralph.models.xapi.concepts.activity_types.scorm_profile import ( + CMIInteractionActivity, +) +from ralph.models.xapi.concepts.activity_types.video import VideoActivity +from ralph.models.xapi.concepts.activity_types.virtual_classroom import ( + VirtualClassroomActivity, +) + +from tests.fixtures.hypothesis_strategies import custom_given + +# AcrossX profile + + +@custom_given(MessageActivity) +def test_models_xapi_concept_activity_type_message_with_valid_field(field): + """Tests that a valid message activity type has the expected `definition`.`type` + property value. + """ + assert field.definition.type == "https://w3id.org/xapi/acrossx/activities/message" + + +# Activity Streams Vocabulary + + +@custom_given(PageActivity) +def test_models_xapi_concept_activity_type_page_with_valid_field(field): + """Tests that a valid page activity type has the expected `definition`.`type` + property value. + """ + assert field.definition.type == "http://activitystrea.ms/schema/1.0/page" + + +# Scorm profile + + +@custom_given(CMIInteractionActivity) +def test_models_xapi_concept_activity_type_cmi_interaction_with_valid_field(field): + """Tests that a valid CMI interaction activity type has the expected + `definition`.`type` property value. + """ + assert ( + field.definition.type == "http://adlnet.gov/expapi/activities/cmi.interaction" + ) + + +# Video + + +@custom_given(VideoActivity) +def test_models_xapi_concept_activity_type_video_with_valid_field(field): + """Tests that a valid video activity type has the expected `definition`.`type` + property value. + """ + assert field.definition.type == "https://w3id.org/xapi/video/activity-type/video" + + +# Virtual classroom + + +@custom_given(VirtualClassroomActivity) +def test_models_xapi_concept_activity_type_virtual_classroom_with_valid_field(field): + """Tests that a valid virtual classroom activity type has the expected + `definition`.`type` property value. + """ + assert ( + field.definition.type + == "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom" + ) diff --git a/tests/models/xapi/concepts/test_verbs.py b/tests/models/xapi/concepts/test_verbs.py new file mode 100644 index 000000000..e6d07fd0b --- /dev/null +++ b/tests/models/xapi/concepts/test_verbs.py @@ -0,0 +1,112 @@ +"""Tests for the xAPI verbs concepts.""" +from ralph.models.xapi.concepts.verbs.acrossx_profile import PostedVerb +from ralph.models.xapi.concepts.verbs.activity_streams_vocabulary import ( + JoinVerb, + LeaveVerb, +) +from ralph.models.xapi.concepts.verbs.adl_vocabulary import AnsweredVerb, AskedVerb +from ralph.models.xapi.concepts.verbs.scorm_profile import ( + CompletedVerb, + InitializedVerb, + InteractedVerb, + TerminatedVerb, +) +from ralph.models.xapi.concepts.verbs.tincan_vocabulary import ViewedVerb +from ralph.models.xapi.concepts.verbs.video import PausedVerb, PlayedVerb, SeekedVerb + +from tests.fixtures.hypothesis_strategies import custom_given + +# AcrossX Profile + + +@custom_given(PostedVerb) +def test_models_xapi_concept_verb_posted_with_valid_field(field): + """Tests that a valid posted verb has the expected `id` property value.""" + assert field.id == "https://w3id.org/xapi/acrossx/verbs/posted" + + +# Activity streams vocabulary + + +@custom_given(JoinVerb) +def test_models_xapi_concept_verb_join_with_valid_field(field): + """Tests that a valid join verb has the expected `id` property value.""" + assert field.id == "http://activitystrea.ms/join" + + +@custom_given(LeaveVerb) +def test_models_xapi_concept_verb_leave_with_valid_field(field): + """Tests that a valid leave verb has the expected `id` property value.""" + assert field.id == "http://activitystrea.ms/leave" + + +# ADL Vocabulary + + +@custom_given(AnsweredVerb) +def test_models_xapi_concept_verb_answered_with_valid_field(field): + """Tests that a valid answered verb has the expected `id` property value.""" + assert field.id == "http://adlnet.gov/expapi/verbs/answered" + + +@custom_given(AskedVerb) +def test_models_xapi_concept_verb_asked_with_valid_field(field): + """Tests that a valid asked verb has the expected `id` property value.""" + assert field.id == "http://adlnet.gov/expapi/verbs/asked" + + +# Scorm profile + + +@custom_given(CompletedVerb) +def test_models_xapi_concept_verb_completed_with_valid_field(field): + """Tests that a valid completed verb has the expected `id` property value.""" + assert field.id == "http://adlnet.gov/expapi/verbs/completed" + + +@custom_given(InitializedVerb) +def test_models_xapi_concept_verb_initialized_with_valid_field(field): + """Tests that a valid initialized verb has the expected `id` property value.""" + assert field.id == "http://adlnet.gov/expapi/verbs/initialized" + + +@custom_given(InteractedVerb) +def test_models_xapi_concept_verb_interacted_with_valid_field(field): + """Tests that a valid interacted verb has the expected `id` property value.""" + assert field.id == "http://adlnet.gov/expapi/verbs/interacted" + + +@custom_given(TerminatedVerb) +def test_models_xapi_concept_verb_terminated_with_valid_field(field): + """Tests that a valid terminated verb has the expected `id` property value.""" + assert field.id == "http://adlnet.gov/expapi/verbs/terminated" + + +# TinCan Vocabulary + + +@custom_given(ViewedVerb) +def test_models_xapi_concept_verb_viewed_with_valid_field(field): + """Tests that a valid viewed verb has the expected `id` property value.""" + assert field.id == "http://id.tincanapi.com/verb/viewed" + + +# Video + + +@custom_given(PlayedVerb) +def test_models_xapi_concept_verb_played_with_valid_field(field): + """Tests that a valid played verb has the expected `id` property value.""" + assert field.id == "https://w3id.org/xapi/video/verbs/played" + + +@custom_given(PausedVerb) +def test_models_xapi_concept_verb_paused_with_valid_field(field): + """Tests that a valid paused verb has the expected `id` property value.""" + assert field.id == "https://w3id.org/xapi/video/verbs/paused" + + +@custom_given(SeekedVerb) +def test_models_xapi_concept_verb_seeked_with_valid_field(field): + """Tests that a valid seeked verb has the expected `id` property value.""" + assert field.id == "https://w3id.org/xapi/video/verbs/seeked" diff --git a/tests/models/xapi/fields/test_actors.py b/tests/models/xapi/fields/test_actors.py deleted file mode 100644 index 0831379ba..000000000 --- a/tests/models/xapi/fields/test_actors.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Tests for the xAPI actor fields.""" - -from ralph.models.xapi.fields.actors import AccountActorField - -from tests.fixtures.hypothesis_strategies import custom_given - - -@custom_given(AccountActorField) -def test_models_xapi_fields_actor_account_field_with_valid_content(actor): - """Tests that an actor field contains an account field.""" - assert hasattr(actor.account, "name") - assert hasattr(actor.account, "homePage") diff --git a/tests/models/xapi/fields/test_objects.py b/tests/models/xapi/fields/test_objects.py deleted file mode 100644 index 0c71b9935..000000000 --- a/tests/models/xapi/fields/test_objects.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Tests for the xAPI object fields.""" - -from ralph.models.xapi.navigation.fields.objects import PageObjectField - -from tests.fixtures.hypothesis_strategies import custom_given - - -@custom_given(PageObjectField) -def test_models_xapi_fields_object_page_object_field(field): - """Tests that a page object field contains a definition with the expected values.""" - assert field.definition.type == "http://activitystrea.ms/schema/1.0/page" - assert field.definition.name == {"en-US": "page"} diff --git a/tests/models/xapi/fields/test_verbs.py b/tests/models/xapi/fields/test_verbs.py deleted file mode 100644 index dc4a7d13b..000000000 --- a/tests/models/xapi/fields/test_verbs.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Tests for the xAPI verb fields.""" - -import json - -from ralph.models.xapi.fields.verbs import TerminatedVerbField, ViewedVerbField - - -def test_models_xapi_fields_verb_viewed_verb_field(): - """Tests that the ViewedVerbField returns the expected dictionary.""" - assert json.loads(ViewedVerbField().json()) == { - "id": "http://id.tincanapi.com/verb/viewed", - "display": {"en-US": "viewed"}, - } - - -def test_models_xapi_fields_verb_terminated_verb_field(): - """Tests that the TerminatedVerbField returns the expected dictionary.""" - assert json.loads(TerminatedVerbField().json()) == { - "id": "http://adlnet.gov/expapi/verbs/terminated", - "display": {"en-US": "terminated"}, - } diff --git a/tests/models/xapi/test_navigation.py b/tests/models/xapi/test_navigation.py index 70fbbc7d4..9c4de56cf 100644 --- a/tests/models/xapi/test_navigation.py +++ b/tests/models/xapi/test_navigation.py @@ -15,8 +15,8 @@ @settings(deadline=None) @pytest.mark.parametrize("class_", [PageTerminated, PageViewed]) @custom_given(st.data()) -def test_models_xapi_navigational_selectors_with_valid_statements(class_, data): - """Tests given a valid navigational xAPI statement the `get_first_model` +def test_models_xapi_navigation_selectors_with_valid_statements(class_, data): + """Tests given a valid navigation xAPI statement the `get_first_model` selector method should return the expected model. """ statement = json.loads(data.draw(custom_builds(class_)).json()) @@ -25,18 +25,18 @@ def test_models_xapi_navigational_selectors_with_valid_statements(class_, data): @custom_given(PageTerminated) -def test_models_xapi_page_terminated_statement(statement): - """Tests that a page_terminated statement has the expected verb.id and - object.definition. +def test_models_xapi_navigation_page_terminated_with_valid_statement(statement): + """Tests that a valide page_terminated statement has the expected `verb`.`id` and + `object`.`definition`.`type` property values. """ assert statement.verb.id == "http://adlnet.gov/expapi/verbs/terminated" assert statement.object.definition.type == "http://activitystrea.ms/schema/1.0/page" @custom_given(PageViewed) -def test_models_xapi_page_viewed_statement(statement): - """Tests that a page_viewed statement has the expected verb.id and - object.definition. +def test_models_xapi_page_viewed_with_valid_statement(statement): + """Tests that a valid page_viewed statement has the expected `verb`.`id` and + `object`.`definition`.`type` property values. """ assert statement.verb.id == "http://id.tincanapi.com/verb/viewed" assert statement.object.definition.type == "http://activitystrea.ms/schema/1.0/page" diff --git a/tests/models/xapi/test_video.py b/tests/models/xapi/test_video.py index 87edff17b..302aefd7e 100644 --- a/tests/models/xapi/test_video.py +++ b/tests/models/xapi/test_video.py @@ -1,4 +1,4 @@ -"""Tests for the xAPI played statement.""" +"""Tests for the `video` xAPI profile.""" import json @@ -73,59 +73,115 @@ def test_models_xapi_video_interaction_validator_with_valid_statements(class_, d @custom_given(VideoInitialized) def test_models_xapi_video_initialized_with_valid_statement(statement): - """Tests that a video initialized statement has the expected verb.id.""" + """Tests that a valid video initialized statement has the expected `verb`.`id` and + `object`.`definition`.`type` property values. + """ + assert statement.verb.id == "http://adlnet.gov/expapi/verbs/initialized" + assert ( + statement.object.definition.type + == "https://w3id.org/xapi/video/activity-type/video" + ) @custom_given(VideoPlayed) def test_models_xapi_video_played_with_valid_statement(statement): - """Tests that a video played statement has the expected verb.id.""" + """Tests that a valid video played statement has the expected `verb`.`id` and + `object`.`definition`.`type` property values. + """ + assert statement.verb.id == "https://w3id.org/xapi/video/verbs/played" + assert ( + statement.object.definition.type + == "https://w3id.org/xapi/video/activity-type/video" + ) @custom_given(VideoPaused) def test_models_xapi_video_paused_with_valid_statement(statement): - """Tests that a video paused statement has the expected verb.id.""" + """Tests that a video paused statement has the expected `verb`.`id` and + `object`.`definition`.`type` property values. + """ + assert statement.verb.id == "https://w3id.org/xapi/video/verbs/paused" + assert ( + statement.object.definition.type + == "https://w3id.org/xapi/video/activity-type/video" + ) @custom_given(VideoSeeked) def test_models_xapi_video_seeked_with_valid_statement(statement): - """Tests that a video seeked statement has the expected verb.id.""" + """Tests that a video seeked statement has the expected `verb`.`id` and + `object`.`definition`.`type` property values.""" + assert statement.verb.id == "https://w3id.org/xapi/video/verbs/seeked" + assert ( + statement.object.definition.type + == "https://w3id.org/xapi/video/activity-type/video" + ) @custom_given(VideoCompleted) def test_models_xapi_video_completed_with_valid_statement(statement): - """Tests that a video completed statement has the expected verb.id.""" + """Tests that a video completed statement has the expected `verb`.`id` and + `object`.`definition`.`type` property values. + """ + assert statement.verb.id == "http://adlnet.gov/expapi/verbs/completed" + assert ( + statement.object.definition.type + == "https://w3id.org/xapi/video/activity-type/video" + ) @custom_given(VideoTerminated) def test_models_xapi_video_terminated_with_valid_statement(statement): - """Tests that a video terminated statement has the expected verb.id.""" + """Tests that a video terminated statement has the expected `verb`.`id` and + `object`.`definition`.`type` property values. + """ + assert statement.verb.id == "http://adlnet.gov/expapi/verbs/terminated" + assert ( + statement.object.definition.type + == "https://w3id.org/xapi/video/activity-type/video" + ) @custom_given(VideoEnableClosedCaptioning) def test_models_xapi_video_enable_closed_captioning_with_valid_statement(statement): """Tests that a video enable closed captioning statement has the expected - verb.id.""" + `verb`.`id` and `object`.`definition`.`type` property values. + """ assert statement.verb.id == "http://adlnet.gov/expapi/verbs/interacted" + assert ( + statement.object.definition.type + == "https://w3id.org/xapi/video/activity-type/video" + ) @custom_given(VideoVolumeChangeInteraction) def test_models_xapi_video_volume_change_interaction_with_valid_statement(statement): """Tests that a video volume change interaction statement has the expected - verb.id.""" + `verb`.`id` and `object`.`definition`.`type` property values. + """ assert statement.verb.id == "http://adlnet.gov/expapi/verbs/interacted" + assert ( + statement.object.definition.type + == "https://w3id.org/xapi/video/activity-type/video" + ) @custom_given(VideoScreenChangeInteraction) def test_models_xapi_video_screen_change_interaction_with_valid_statement(statement): """Tests that a video screen change interaction statement has the expected - verb.id.""" + `verb`.`id` and `object`.`definition`.`type` property values. + """ assert statement.verb.id == "http://adlnet.gov/expapi/verbs/interacted" + assert ( + statement.object.definition.type + == "https://w3id.org/xapi/video/activity-type/video" + )