Skip to content

Commit

Permalink
✨(models) add peer instruction events pydantic models
Browse files Browse the repository at this point in the history
Peer instruction events are emitted by the server to capture information about
peer instruction XBlock.
  • Loading branch information
wilbrdt committed Jun 20, 2023
1 parent 587578b commit 0735466
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to
### Added

- Implement edX open response assessment events pydantic models
- Implement edx peer instruction events pydantic models

### Changed

Expand Down
5 changes: 5 additions & 0 deletions src/ralph/models/edx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
ORASubmitFeedbackOnAssessments,
ORAUploadFile,
)
from .peer_instruction.statements import (
PeerInstructionAccessed,
PeerInstructionOriginalSubmitted,
PeerInstructionRevisedSubmitted,
)
from .problem_interaction.statements import (
EdxProblemHintDemandhintDisplayed,
EdxProblemHintFeedbackDisplayed,
Expand Down
1 change: 1 addition & 0 deletions src/ralph/models/edx/peer_instruction/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# noqa: D104
1 change: 1 addition & 0 deletions src/ralph/models/edx/peer_instruction/fields/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# noqa: D104
22 changes: 22 additions & 0 deletions src/ralph/models/edx/peer_instruction/fields/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Peer instruction event field definition."""

from pydantic import constr

from ...base import AbstractBaseEventField


class PeerInstructionEventField(AbstractBaseEventField):
"""Pydantic model for peer instruction `event` field.
Attributes:
answer (int): Consists of the index assigned to the answer choice selected by
the learner.
rationale (str): Consists of the text entered by the learner to explain why
they selected that answer choice.
truncated (bool): `True` only if the rationale was longer than 12,500
characters, which is the maximum included in the event.
"""

answer: int
rationale: constr(max_length=12500)
truncated: bool
86 changes: 86 additions & 0 deletions src/ralph/models/edx/peer_instruction/statements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Peer instruction events model definitions."""

from typing import Union

try:
from typing import Literal
except ImportError:
from typing_extensions import Literal

from pydantic import Json

from ralph.models.selector import selector

from ..server import BaseServerModel
from .fields.events import PeerInstructionEventField


class PeerInstructionAccessed(BaseServerModel):
"""Pydantic model for `ubc.peer_instruction.accessed` statement.
The server emits this event when a peer instruction question and its set of answer
choices is shown to a learner.
Attributes:
event_type (str): Consists of the value `ubc.peer_instruction.accessed`.
name (str): Consists of the value `ubc.peer_instruction.accessed`.
"""

__selector__ = selector(
event_source="server", event_type="ubc.peer_instruction.accessed"
)

event_type: Literal["ubc.peer_instruction.accessed"]
name: Literal["ubc.peer_instruction.accessed"]


class PeerInstructionOriginalSubmitted(BaseServerModel):
"""Pydantic model for `ubc.peer_instruction.original_submitted` statement.
The server emits this event when learners submit their initial responses. These
events record the answer choice the learner selected and the explanation given
for why that selection was made.
Attributes:
event (int): See PeerInstructionEventField.
event_type (str): Consists of the value
`ubc.peer_instruction.original_submitted`.
name (str): Consists of the value `ubc.peer_instruction.original_submitted`.
"""

__selector__ = selector(
event_source="server", event_type="ubc.peer_instruction.original_submitted"
)

event: Union[
Json[PeerInstructionEventField], # pylint: disable=unsubscriptable-object
PeerInstructionEventField,
]
event_type: Literal["ubc.peer_instruction.original_submitted"]
name: Literal["ubc.peer_instruction.original_submitted"]


class PeerInstructionRevisedSubmitted(BaseServerModel):
"""Pydantic model for `ubc.peer_instruction.revised_submitted` statement.
The server emits this event when learners submit their revised responses. These
events record the answer choice selected by the learner and the explanation for
why that selection was made.
Attributes:
event (int): See PeerInstructionEventField.
event_type (str): Consists of the value
`ubc.peer_instruction.revised_submitted`.
name (str): Consists of the value `ubc.peer_instruction.revised_submitted`.
"""

__selector__ = selector(
event_source="server", event_type="ubc.peer_instruction.revised_submitted"
)

event: Union[
Json[PeerInstructionEventField], # pylint: disable=unsubscriptable-object
PeerInstructionEventField,
]
event_type: Literal["ubc.peer_instruction.revised_submitted"]
name: Literal["ubc.peer_instruction.revised_submitted"]
Empty file.
32 changes: 32 additions & 0 deletions tests/models/edx/peer_instruction/test_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Tests for peer instruction models event fields."""

import json

import pytest
from pydantic.error_wrappers import ValidationError

from ralph.models.edx.peer_instruction.fields.events import PeerInstructionEventField

from tests.fixtures.hypothesis_strategies import custom_given


@custom_given(PeerInstructionEventField)
def test_models_edx_peer_instruction_event_field_with_valid_field(field):
"""Tests that a valid `PeerInstructionEventField` does not raise a
`ValidationError`.
"""
assert len(field.rationale) <= 12500


@custom_given(PeerInstructionEventField)
def test_models_edx_peer_instruction_event_field_with_invalid_rationale(field):
"""Tests that a valid `PeerInstructionEventField` does not raise a
`ValidationError`.
"""
invalid_field = json.loads(field.json())
invalid_field["rationale"] = "x" * 12501
with pytest.raises(
ValidationError,
match="rationale\n ensure this value has at most 12500 characters",
):
PeerInstructionEventField(**invalid_field)
63 changes: 63 additions & 0 deletions tests/models/edx/peer_instruction/test_statements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Tests for the peer_instruction event models."""

import json

import pytest
from hypothesis import strategies as st

from ralph.models.edx.peer_instruction.statements import (
PeerInstructionAccessed,
PeerInstructionOriginalSubmitted,
PeerInstructionRevisedSubmitted,
)
from ralph.models.selector import ModelSelector

from tests.fixtures.hypothesis_strategies import custom_builds, custom_given


@pytest.mark.parametrize(
"class_",
[
PeerInstructionAccessed,
PeerInstructionOriginalSubmitted,
PeerInstructionRevisedSubmitted,
],
)
@custom_given(st.data())
def test_models_edx_peer_instruction_selectors_with_valid_statements(class_, data):
"""Tests given a valid peer_instruction edX statement the `get_first_model`
selector method should return the expected model.
"""
statement = json.loads(data.draw(custom_builds(class_)).json())
model = ModelSelector(module="ralph.models.edx").get_first_model(statement)
assert model is class_


@custom_given(PeerInstructionAccessed)
def test_models_edx_peer_instruction_accessed_with_valid_statement(
statement,
):
"""Tests that a `ubc.peer_instruction.accessed` statement has the expected
`event_type`."""
assert statement.event_type == "ubc.peer_instruction.accessed"
assert statement.name == "ubc.peer_instruction.accessed"


@custom_given(PeerInstructionOriginalSubmitted)
def test_models_edx_peer_instruction_original_submitted_with_valid_statement(
statement,
):
"""Tests that a `ubc.peer_instruction.original_submitted` statement has the
expected `event_type`."""
assert statement.event_type == "ubc.peer_instruction.original_submitted"
assert statement.name == "ubc.peer_instruction.original_submitted"


@custom_given(PeerInstructionRevisedSubmitted)
def test_models_edx_peer_instruction_revised_submitted_with_valid_statement(
statement,
):
"""Tests that a `ubc.peer_instruction.revised_submitted` statement has the
expected `event_type`."""
assert statement.event_type == "ubc.peer_instruction.revised_submitted"
assert statement.name == "ubc.peer_instruction.revised_submitted"

0 comments on commit 0735466

Please sign in to comment.