|
| 1 | +import json |
1 | 2 | from abc import ABC, abstractmethod
|
2 | 3 | from collections.abc import Mapping, Sequence
|
3 | 4 | from functools import cached_property
|
|
6 | 7 | import jinja2
|
7 | 8 | from pydantic import BaseModel, JsonValue
|
8 | 9 |
|
9 |
| -from questionpy_common.api.attempt import AttemptFile, AttemptUi, CacheControl, ScoredInputModel, ScoringCode |
| 10 | +from questionpy_common.api.attempt import ( |
| 11 | + AttemptFile, |
| 12 | + AttemptUi, |
| 13 | + CacheControl, |
| 14 | + DisplayRole, |
| 15 | + FeedbackType, |
| 16 | + JsModuleCall, |
| 17 | + ScoredInputModel, |
| 18 | + ScoringCode, |
| 19 | +) |
10 | 20 |
|
11 | 21 | from ._ui import create_jinja2_environment
|
12 | 22 | from ._util import reify_type_hint
|
@@ -75,6 +85,10 @@ def placeholders(self) -> dict[str, str]:
|
75 | 85 | def css_files(self) -> list[str]:
|
76 | 86 | pass
|
77 | 87 |
|
| 88 | + @property |
| 89 | + def javascript_calls(self) -> list[JsModuleCall]: |
| 90 | + pass |
| 91 | + |
78 | 92 | @property
|
79 | 93 | def files(self) -> dict[str, AttemptFile]:
|
80 | 94 | pass
|
@@ -156,6 +170,9 @@ def __init__(
|
156 | 170 | self.cache_control = CacheControl.PRIVATE_CACHE
|
157 | 171 | self.placeholders: dict[str, str] = {}
|
158 | 172 | self.css_files: list[str] = []
|
| 173 | + self._javascript_calls: list[JsModuleCall] = [] |
| 174 | + """LMS has to call these JS modules/functions.""" |
| 175 | + |
159 | 176 | self.files: dict[str, AttemptFile] = {}
|
160 | 177 |
|
161 | 178 | self.scoring_code: ScoringCode | None = None
|
@@ -187,6 +204,11 @@ def __init__(
|
187 | 204 | only be viewed as an output.
|
188 | 205 | """
|
189 | 206 |
|
| 207 | + self._init_attempt() |
| 208 | + |
| 209 | + def _init_attempt(self) -> None: # noqa: B027 |
| 210 | + """A place for the question to initialize the attempt (set up fields, JavaScript calls, etc.).""" |
| 211 | + |
190 | 212 | @property
|
191 | 213 | @abstractmethod
|
192 | 214 | def formulation(self) -> str:
|
@@ -243,6 +265,38 @@ def jinja2(self) -> jinja2.Environment:
|
243 | 265 | def variant(self) -> int:
|
244 | 266 | return self.attempt_state.variant
|
245 | 267 |
|
| 268 | + def call_js( |
| 269 | + self, |
| 270 | + module: str, |
| 271 | + function: str, |
| 272 | + data: JsonValue = None, |
| 273 | + *, |
| 274 | + if_role: DisplayRole | None = None, |
| 275 | + if_feedback_type: FeedbackType | None = None, |
| 276 | + ) -> None: |
| 277 | + """Call a javascript function when the LMS displays this question attempt. |
| 278 | +
|
| 279 | + The function is called when both the `if_role` and `if_feedback_type` conditions are met. |
| 280 | +
|
| 281 | + Args: |
| 282 | + module: JS module name specified as: |
| 283 | + @[package namespace]/[package short name]/[subdir]/[module name] (full reference) or |
| 284 | + TODO [subdir]/[module name] (referencing a module within the package where this class is subclassed) or |
| 285 | + function: Name of a callable value within the JS module |
| 286 | + data: arbitrary data to pass to the function |
| 287 | + if_role: Function is only called if the user has this role. |
| 288 | + if_feedback_type: Function is only called if the user is allowed to view this feedback type. |
| 289 | + """ |
| 290 | + data_json = None if data is None else json.dumps(data) |
| 291 | + call = JsModuleCall( |
| 292 | + module=module, function=function, data=data_json, if_role=if_role, if_feedback_type=if_feedback_type |
| 293 | + ) |
| 294 | + self._javascript_calls.append(call) |
| 295 | + |
| 296 | + @property |
| 297 | + def javascript_calls(self) -> list[JsModuleCall]: |
| 298 | + return self._javascript_calls |
| 299 | + |
246 | 300 | def __init_subclass__(cls, *args: object, **kwargs: object):
|
247 | 301 | super().__init_subclass__(*args, **kwargs)
|
248 | 302 |
|
|
0 commit comments