-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: implement server-package API changes
- Loading branch information
Showing
25 changed files
with
398 additions
and
228 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,7 @@ description = "QuestionPy application server" | |
authors = ["Technische Universität Berlin, innoCampus <[email protected]>"] | ||
license = "MIT" | ||
homepage = "https://questionpy.org" | ||
version = "0.2.5" | ||
version = "0.3.0" | ||
packages = [ | ||
{ include = "questionpy_common" }, | ||
{ include = "questionpy_server" } | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,23 @@ | ||
# This file is part of QuestionPy. (https://questionpy.org) | ||
# QuestionPy is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
from collections.abc import ( | ||
Mapping, | ||
Sequence, # noqa: F401 | ||
) | ||
from typing import ( | ||
TypeAlias, | ||
Union, # noqa: F401, | ||
) | ||
|
||
from pydantic import BaseModel | ||
from typing_extensions import TypeAliasType | ||
|
||
|
||
class Localized(BaseModel): | ||
lang: str | ||
|
||
|
||
# "Regular" recursive type aliases break Pydantic: https://github.com/pydantic/pydantic/issues/8346 | ||
PlainValue: TypeAlias = TypeAliasType("PlainValue", "Union[None, int, str, bool, Sequence[PlainValue], PlainMapping]") | ||
PlainMapping: TypeAlias = TypeAliasType("PlainMapping", Mapping[str, PlainValue]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,19 +2,19 @@ | |
# QuestionPy is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
|
||
from abc import ABC, abstractmethod | ||
from collections.abc import Sequence | ||
from enum import Enum | ||
from typing import Annotated | ||
|
||
from pydantic import BaseModel, Field | ||
|
||
from . import Localized | ||
|
||
__all__ = [ | ||
"AttemptFile", | ||
"AttemptModel", | ||
"AttemptScoredModel", | ||
"AttemptStartedModel", | ||
"AttemptUi", | ||
"BaseAttempt", | ||
"CacheControl", | ||
"ClassifiedResponse", | ||
"ScoreModel", | ||
|
@@ -46,16 +46,20 @@ class AttemptUi(BaseModel): | |
|
||
placeholders: dict[str, str] = {} | ||
"""Names and values of the ``<?p`` placeholders that appear in content.""" | ||
css_files: list[str] | ||
css_files: list[str] = [] | ||
files: dict[str, AttemptFile] = {} | ||
cache_control: CacheControl = CacheControl.PRIVATE_CACHE | ||
|
||
|
||
class AttemptModel(BaseModel): | ||
class AttemptModel(Localized): | ||
variant: int | ||
ui: AttemptUi | ||
|
||
|
||
class AttemptStartedModel(AttemptModel): | ||
attempt_state: str | ||
|
||
|
||
class ScoringCode(Enum): | ||
AUTOMATICALLY_SCORED = "AUTOMATICALLY_SCORED" | ||
NEEDS_MANUAL_SCORING = "NEEDS_MANUAL_SCORING" | ||
|
@@ -70,31 +74,40 @@ class ClassifiedResponse(BaseModel): | |
score: float | ||
|
||
|
||
class ScoredInputState(Enum): | ||
CORRECT = "CORRECT" | ||
CONSEQUENTIAL_SCORE = "CONSEQUENTIAL_SCORE" | ||
PARTIALLY_CORRECT = "PARTIALLY_CORRECT" | ||
WRONG = "WRONG" | ||
|
||
|
||
class ScoredInputModel(BaseModel): | ||
state: ScoredInputState | ||
score: float | None = None | ||
score_max: float | None = None | ||
specific_feedback: str | None = None | ||
right_answer: str | None = None | ||
|
||
|
||
class ScoredSubquestionModel(BaseModel): | ||
score: float | None = None | ||
score_final: float | None = None | ||
scoring_code: ScoringCode | None = None | ||
response_summary: str | ||
response_class: str | ||
|
||
|
||
class ScoreModel(BaseModel): | ||
scoring_state: str = "{}" | ||
scoring_state: str | None = None | ||
scoring_code: ScoringCode | ||
score: float | None | ||
"""The score for this question attempt, must lie between the `score_min` and `score_max` set by the question.""" | ||
classification: Sequence[ClassifiedResponse] | None = None | ||
score_final: float | None | ||
scored_inputs: dict[str, ScoredInputModel] = {} | ||
"""Maps input names to their individual scores.""" | ||
scored_subquestions: dict[str, ScoredSubquestionModel] = {} | ||
"""Maps subquestion IDs to their individual scores.""" | ||
|
||
|
||
class AttemptScoredModel(AttemptModel, ScoreModel): | ||
pass | ||
|
||
|
||
class BaseAttempt(ABC): | ||
@abstractmethod | ||
def export_attempt_state(self) -> str: | ||
"""Serialize this attempt's relevant data. | ||
A future call to :meth:`BaseQuestion.view_attempt` should result in an attempt object identical to the one which | ||
exported the state. | ||
""" | ||
|
||
@abstractmethod | ||
def export(self) -> AttemptModel: | ||
"""Get metadata about this attempt.""" | ||
|
||
@abstractmethod | ||
def export_scored_attempt(self) -> AttemptScoredModel: | ||
"""Score this attempt.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# This file is part of QuestionPy. (https://questionpy.org) | ||
# QuestionPy is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
from abc import abstractmethod | ||
from typing import TYPE_CHECKING, Protocol, TypeAlias | ||
|
||
from questionpy_common.manifest import PackageFile | ||
|
||
if TYPE_CHECKING: | ||
from questionpy_common.api.qtype import QuestionTypeInterface | ||
|
||
|
||
class BasePackageInterface(Protocol): | ||
@abstractmethod | ||
def get_static_files(self) -> dict[str, PackageFile]: | ||
pass | ||
|
||
|
||
class LibraryPackageInterface(BasePackageInterface, Protocol): | ||
pass | ||
|
||
|
||
QPyPackageInterface: TypeAlias = "LibraryPackageInterface | QuestionTypeInterface" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,25 @@ | ||
# This file is part of QuestionPy. (https://questionpy.org) | ||
# QuestionPy is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
from __future__ import annotations | ||
|
||
from abc import ABC, abstractmethod | ||
from abc import abstractmethod | ||
from typing import TYPE_CHECKING, Protocol | ||
|
||
from questionpy_common.elements import OptionsFormDefinition | ||
from questionpy_common.api.package import BasePackageInterface | ||
|
||
from .question import BaseQuestion | ||
if TYPE_CHECKING: | ||
from questionpy_common.elements import OptionsFormDefinition | ||
|
||
__all__ = ["BaseQuestionType", "InvalidQuestionStateError", "OptionsFormValidationError"] | ||
from . import PlainMapping | ||
from .question import QuestionInterface | ||
|
||
__all__ = ["InvalidQuestionStateError", "OptionsFormValidationError", "QuestionTypeInterface"] | ||
|
||
class BaseQuestionType(ABC): | ||
|
||
class QuestionTypeInterface(BasePackageInterface, Protocol): | ||
@abstractmethod | ||
def get_options_form(self, question_state: str | None) -> tuple[OptionsFormDefinition, dict[str, object]]: | ||
def get_options_form(self, question_state: str | None) -> tuple[OptionsFormDefinition, PlainMapping]: | ||
"""Get the form used to create a new or edit an existing question. | ||
Args: | ||
|
@@ -24,7 +30,7 @@ def get_options_form(self, question_state: str | None) -> tuple[OptionsFormDefin | |
""" | ||
|
||
@abstractmethod | ||
def create_question_from_options(self, old_state: str | None, form_data: dict[str, object]) -> BaseQuestion: | ||
def create_question_from_options(self, old_state: str | None, form_data: PlainMapping) -> QuestionInterface: | ||
"""Create or update the question (state) with the form data from a submitted question edit form. | ||
Args: | ||
|
@@ -39,7 +45,7 @@ def create_question_from_options(self, old_state: str | None, form_data: dict[st | |
""" | ||
|
||
@abstractmethod | ||
def create_question_from_state(self, question_state: str) -> BaseQuestion: | ||
def create_question_from_state(self, question_state: str) -> QuestionInterface: | ||
"""Deserialize the given question state, returning a question object equivalent to the one which exported it. | ||
Raises: | ||
|
Oops, something went wrong.