Skip to content

Commit

Permalink
feat: javascript calls in QPPE, package deps and data structures
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinGauk committed Jan 16, 2025
1 parent 43a429c commit 8ca0cc1
Show file tree
Hide file tree
Showing 16 changed files with 272 additions and 138 deletions.
236 changes: 142 additions & 94 deletions docs/qppe-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,7 @@ paths:
content:
application/json:
schema:
type: object
properties:
definition:
$ref: "#/components/schemas/OptionsFormDefinition"
form_data:
$ref: "#/components/schemas/FormData"
required: [ definition, form_data ]
$ref: "#/components/schemas/Options"
404:
description: Package not found.
500:
Expand Down Expand Up @@ -591,6 +585,17 @@ components:
- OPT_1
- OPT_2

Options:
allOf:
- type: object
properties:
definition:
$ref: "#/components/schemas/OptionsFormDefinition"
form_data:
$ref: "#/components/schemas/FormData"
required: [ definition, form_data ]
- $ref: '#/components/schemas/PackageDependencies'

RequestBaseData:
type: object
properties:
Expand Down Expand Up @@ -757,98 +762,119 @@ components:
required: [ variant ]

Attempt:
type: object
properties:
variant:
type: integer
minimum: 1
description: Which variant of this question, between 1 and question.num_variants.
question_summary:
type: string
nullable: true
default: null
description: Plain-text summary of the question.
right_answer_summary:
type: string
nullable: true
default: null
description: Plain-text summary of the right answer.
ui:
type: object
allOf:
- type: object
properties:
fields:
type: array
items:
type: object
properties:
name:
type: string
type:
type: string
description: Type of the input field.
default:
type: string
nullable: true
validation_regex:
type: string
nullable: true
description: Validate response with this regex. If the response does not match, the response might not be scorable.
required:
type: boolean
description: This field is required. Otherwise the response will not be scorable.
correct_response:
type: string
nullable: true
required: [ name, type, default, validation_regex, required, correct_response ]
text:
type: string
format: text/html
description: Answer input area
example: '<label>Enter your answer: <input type="text"/></label>'
include_inline_css:
variant:
type: integer
minimum: 1
description: Which variant of this question, between 1 and question.num_variants.
question_summary:
type: string
nullable: true
include_css_file:
default: null
description: Plain-text summary of the question.
right_answer_summary:
type: string
nullable: true
include_javascript_modules:
type: array
default: [ ]
items:
type: string
call_javascript:
type: array
default: [ ]
items:
type: object
properties:
module:
type: string
function:
type: string
args:
type: string
required: [ module, function, args ]
files:
type: array
default: [ ]
items:
type: object
properties:
name:
type: string
description: file name
data:
type: string
format: base64
description: file data
mime_type:
type: string
nullable: true
required: [ name, data, mime_type ]
required: [ fields, text ]
readOnly: true
required: [ variant, ui ]
default: null
description: Plain-text summary of the right answer.
ui:
type: object
properties:
fields:
type: array
items:
type: object
properties:
name:
type: string
type:
type: string
description: Type of the input field.
default:
type: string
nullable: true
validation_regex:
type: string
nullable: true
description: Validate response with this regex. If the response does not match, the response might not be scorable.
required:
type: boolean
description: This field is required. Otherwise the response will not be scorable.
correct_response:
type: string
nullable: true
required: [ name, type, default, validation_regex, required, correct_response ]
text:
type: string
format: text/html
description: Answer input area
example: '<label>Enter your answer: <input type="text"/></label>'
include_inline_css:
type: string
nullable: true
include_css_file:
type: string
nullable: true
javascript_calls:
type: array
default: [ ]
items:
type: object
properties:
module:
type: string
pattern: '^[a-zA-Z0-9_$/@.]+$'
description: JS module name like @[package namespace]/[package short name]/[module].js
function:
type: string
pattern: '^[a-zA-Z0-9_$]+$'
description: Name of a callable value within the JS module.
data:
type: string
nullable: true
description: JSON data given as argument to the function
if_role:
type: string
nullable: true
description: Function is called only if the user has this role.
enum:
- TEACHER
- DEVELOPER
- SCORER
- PROCTOR
if_feedback_type:
type: string
nullable: true
description: Function is called only if the user is allowed to view this type of feedback.
enum:
- SPECIFIC_FEEDBACK
- GENERAL_FEEDBACK
- RIGHT_ANSWER
- HINT
required: [ module, function, data, if_role, if_feedback_type ]
files:
type: array
default: [ ]
items:
type: object
properties:
name:
type: string
description: file name
data:
type: string
format: base64
description: file data
mime_type:
type: string
nullable: true
required: [ name, data, mime_type ]
required: [ fields, text ]
readOnly: true
required: [ variant, ui ]
- $ref: "#/components/schemas/PackageDependencies"

AttemptStarted:
allOf:
Expand Down Expand Up @@ -999,6 +1025,28 @@ components:
required: [ scoring_state, scoring_code, score ]
- $ref: "#/components/schemas/Attempt"

PackageDependencies:
type: object
properties:
package_dependencies:
type: array
items:
type: object
properties:
namespace:
type: string
description: Namespace of a package.
short_name:
type: string
description: Short name of a package.
hash:
type: string
description: SHA256 hash of a package.
required: [ namespace, short_name, hash ]
description: Listing of all QuestionPy packages that were involved for this response. This information will
be used by the LMS to resolve a QPy-URI and other references.
required: [ package_dependencies ]

AttemptAsyncScoringStatus:
type: object
properties:
Expand Down
36 changes: 35 additions & 1 deletion questionpy_common/api/attempt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# QuestionPy is free software released under terms of the MIT license. See LICENSE.md.
# (c) Technische Universität Berlin, innoCampus <[email protected]>

from enum import Enum
from enum import Enum, StrEnum
from typing import Annotated

from pydantic import BaseModel, Field
Expand All @@ -17,7 +17,13 @@
"AttemptUi",
"CacheControl",
"ClassifiedResponse",
"DisplayRole",
"FeedbackType",
"JsModuleCall",
"ScoreModel",
"ScoredInputModel",
"ScoredInputState",
"ScoredSubquestionModel",
"ScoringCode",
]

Expand All @@ -28,6 +34,33 @@ class CacheControl(Enum):
NO_CACHE = "NO_CACHE"


class DisplayRole(StrEnum):
DEVELOPER = "DEVELOPER"
PROCTOR = "PROCTOR"
SCORER = "SCORER"
TEACHER = "TEACHER"


class FeedbackType(StrEnum):
GENERAL_FEEDBACK = "GENERAL_FEEDBACK"
SPECIFIC_FEEDBACK = "SPECIFIC_FEEDBACK"
RIGHT_ANSWER = "RIGHT_ANSWER"
HINT = "HINT"


class JsModuleCall(BaseModel):
module: Annotated[str, Field(pattern=r"^[a-zA-Z0-9_$/@.]+$")]
"""JS module name like @[package namespace]/[package short name]/[module].js"""
function: Annotated[str, Field(pattern=r"^[a-zA-Z0-9_$]+$")]
"""Name of a callable value within the JS module."""
data: str | None
"""JSON data given as argument to the function"""
if_role: DisplayRole | None
"""Function is only called if the user has this role."""
if_feedback_type: FeedbackType | None
"""Function is only called if the user is allowed to view this feedback type."""


class AttemptFile(BaseModel):
name: str
mime_type: str | None = None
Expand All @@ -47,6 +80,7 @@ class AttemptUi(BaseModel):
placeholders: dict[str, str] = {}
"""Names and values of the ``<?p`` placeholders that appear in content."""
css_files: list[str] = []
javascript_calls: list[JsModuleCall] = []
files: dict[str, AttemptFile] = {}
cache_control: CacheControl = CacheControl.PRIVATE_CACHE

Expand Down
4 changes: 3 additions & 1 deletion questionpy_server/collector/indexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ async def register_package(
# Create new package...
if isinstance(path_or_manifest, Path):
# ...from path.
async with self._worker_pool.get_worker(ZipPackageLocation(path_or_manifest), 0, None) as worker:
async with self._worker_pool.get_worker(
ZipPackageLocation(path_or_manifest, package_hash), 0, None
) as worker:
manifest = await worker.get_manifest()
package = Package(package_hash, manifest, source, path_or_manifest)
else:
Expand Down
25 changes: 24 additions & 1 deletion questionpy_server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from pydantic import BaseModel, ByteSize, ConfigDict, Field

from questionpy_common.api.attempt import AttemptModel, AttemptScoredModel, AttemptStartedModel
from questionpy_common.api.question import QuestionModel
from questionpy_common.elements import OptionsFormDefinition
from questionpy_common.manifest import PackageType
Expand Down Expand Up @@ -50,6 +51,12 @@ class PackageVersionsInfo(BaseModel):
versions: list[PackageVersionSpecificInfo]


class LoadedPackage(BaseModel):
namespace: str
short_name: str
hash: str | None


class MainBaseModel(BaseModel):
pass

Expand All @@ -58,7 +65,11 @@ class RequestBaseData(MainBaseModel):
context: int | None = None


class QuestionEditFormResponse(BaseModel):
class PackageDependenciesModel(BaseModel):
package_dependencies: list[LoadedPackage]


class QuestionEditFormResponse(PackageDependenciesModel):
definition: OptionsFormDefinition
form_data: dict[str, object]

Expand Down Expand Up @@ -91,6 +102,18 @@ class AttemptScoreArguments(AttemptViewArguments):
generate_hint: bool


class AttemptStartedResponse(AttemptStartedModel, PackageDependenciesModel):
pass


class AttemptResponse(AttemptModel, PackageDependenciesModel):
pass


class AttemptScoredResponse(AttemptScoredModel, PackageDependenciesModel):
pass


class RequestErrorCode(Enum):
QUEUE_WAITING_TIMEOUT = "QUEUE_WAITING_TIMEOUT"
WORKER_TIMEOUT = "WORKER_TIMEOUT"
Expand Down
Loading

0 comments on commit 8ca0cc1

Please sign in to comment.