Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.10', '3.11']
python-version: ['3.11', '3.12']
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
Expand Down
25 changes: 25 additions & 0 deletions docs/source/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,28 @@ Click on a set in teacher mode. The arrow next to the "Add Question" button allo
Choose the zip file you wish to upload, and the question should appear! 🎉

![Importing Question from file in Teacher Mode](_static/images/import-teacher.png)

## Changing the Defaults

A setting the document doesn't give, such as whether a question is published, takes in2lambda's default. To change some of the defaults, write a TOML file holding only those, laid out like in2lambda's own (below), and pass it with `--defaults`:

```toml
# ours.toml
[question]
publish = false

[response_area.MATH_MULTI_LINES]
evaluation_function = "evaluateProof (experimental)"
```

```bash
$ in2lambda questions.tex PartSolPartSol --defaults ours.toml
```

From Python, call {func}`in2lambda.defaults.override` before creating the set. A file that sets something in2lambda has no default for, or a value of the wrong type, is refused.

:::{dropdown} in2lambda's defaults
```{literalinclude} ../../in2lambda/defaults.toml
:language: toml
```
:::
19 changes: 13 additions & 6 deletions in2lambda/api/question.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

import panflute as pf

from in2lambda import defaults
from in2lambda.api.part import Part
from in2lambda.defaults import UNSET


@dataclass
Expand Down Expand Up @@ -39,19 +41,24 @@ class Question:
text."""

# The settings below are left out of the repr, so that printing a question shows its
# content rather than nine lines of configuration.
# content rather than nine lines of configuration. Those set to UNSET default to
# what in2lambda's defaults.toml gives them.

# The skill level may be an int as well as a float, because Lambda Feedback writes
# its exports in JavaScript, which writes the levels 0.0 and 1.0 as 0 and 1.
skill: Optional[Union[int, float]] = field(default=None, repr=False)
guidance: Optional[str] = field(default=None, repr=False)
duration_lower_bound: Optional[int] = field(default=None, repr=False)
duration_upper_bound: Optional[int] = field(default=None, repr=False)
publish: bool = field(default=False, repr=False)
display_final_answer: bool = field(default=True, repr=False)
display_worked_solution: bool = field(default=True, repr=False)
display_structured_tutorial: bool = field(default=True, repr=False)
display_chatbot: bool = field(default=False, repr=False)
publish: bool = field(default=UNSET, repr=False)
display_final_answer: bool = field(default=UNSET, repr=False)
display_worked_solution: bool = field(default=UNSET, repr=False)
display_structured_tutorial: bool = field(default=UNSET, repr=False)
display_chatbot: bool = field(default=UNSET, repr=False)

def __post_init__(self) -> None:
"""Gives each setting left unset its default."""
defaults.fill(self, "question")

@property
def master_content(self) -> str:
Expand Down
92 changes: 55 additions & 37 deletions in2lambda/api/response_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
from enum import Enum
from typing import Any, ClassVar, Optional, Union

from in2lambda import defaults
from in2lambda.defaults import UNSET


def _new_id() -> str:
return str(uuid.uuid4())
Expand Down Expand Up @@ -83,7 +86,7 @@ class ResponseArea:

response_type: ClassVar[str]
answer: Any = None
evaluation_function: str
evaluation_function: str = UNSET
grade_params: Optional[dict[str, Any]] = None
pre_text: str = ""
post_text: str = ""
Expand All @@ -92,19 +95,23 @@ class ResponseArea:
check_button_text: str = ""
"""Replaces the label of the check button; left out of the JSON when empty."""
input_symbols: list[InputSymbol] = field(default_factory=list)
display_input_symbols: bool = False
live_preview: bool = False
include_in_pdf: bool = False
save_allowed: bool = False
separate_feedback: bool = True
common_feedback_color: str = "#C4CDD5"
correct_feedback_color: str = "#22C55E"
correct_feedback_prefix: str = "Correct"
incorrect_feedback_color: str = "#ff5630"
incorrect_feedback_prefix: str = "Incorrect"
display_input_symbols: bool = UNSET
live_preview: bool = UNSET
include_in_pdf: bool = UNSET
save_allowed: bool = UNSET
separate_feedback: bool = UNSET
common_feedback_color: str = UNSET
correct_feedback_color: str = UNSET
correct_feedback_prefix: str = UNSET
incorrect_feedback_color: str = UNSET
incorrect_feedback_prefix: str = UNSET
tests: list[Test] = field(default_factory=list)
cases: list[Case] = field(default_factory=list)

def __post_init__(self) -> None:
"""Gives each setting left unset its default."""
defaults.fill(self, "response_area", self.response_type)

def _config(self) -> Optional[dict[str, Any]]:
"""The response type's settings, as Lambda Feedback writes them."""
return None
Expand All @@ -117,9 +124,9 @@ def _grade_params(self) -> Optional[dict[str, Any]]:
@dataclass(kw_only=True)
class _Maths(ResponseArea):
answer: str = ""
allow_handwrite: bool = True
allow_photo: bool = True
enable_refinement: bool = True
allow_handwrite: bool = UNSET
allow_photo: bool = UNSET
enable_refinement: bool = UNSET

def _config(self) -> Optional[dict[str, Any]]:
return {
Expand All @@ -134,7 +141,6 @@ class MathSingleLine(_Maths):
"""A single symbolic expression."""

response_type: ClassVar[str] = "MATH_SINGLE_LINE"
evaluation_function: str = "compareExpressions"


@dataclass(kw_only=True)
Expand All @@ -150,7 +156,6 @@ class Number(ResponseArea):

response_type: ClassVar[str] = "NUMBER"
answer: Optional[Union[int, float]] = None
evaluation_function: str = "isSimilar"


@dataclass(kw_only=True)
Expand All @@ -159,7 +164,6 @@ class NumericUnits(ResponseArea):

response_type: ClassVar[str] = "NUMERIC_UNITS"
answer: str = ""
evaluation_function: str = "compareExpressions"

def _grade_params(self) -> Optional[dict[str, Any]]:
# compareExpressions compares units only when told to.
Expand All @@ -180,7 +184,6 @@ class Boolean(ResponseArea):

response_type: ClassVar[str] = "BOOLEAN"
answer: bool = False
evaluation_function: str = "isExactEqual"


@dataclass(kw_only=True)
Expand All @@ -189,10 +192,9 @@ class MultipleChoice(ResponseArea):

response_type: ClassVar[str] = "MULTIPLE_CHOICE"
answer: list[bool] = field(default_factory=list)
evaluation_function: str = "arrayEqual"
options: list[str] = field(default_factory=list)
single: bool = True
randomise: bool = False
single: bool = UNSET
randomise: bool = UNSET

def _config(self) -> Optional[dict[str, Any]]:
return {
Expand All @@ -208,7 +210,6 @@ class Table(ResponseArea):

response_type: ClassVar[str] = "TABLE"
answer: list[list[str]] = field(default_factory=list)
evaluation_function: str = "arraySymbolicEqual"
row_names: list[str] = field(default_factory=list)
col_names: list[str] = field(default_factory=list)

Expand All @@ -227,7 +228,6 @@ class Matrix(ResponseArea):

response_type: ClassVar[str] = "MATRIX"
answer: list[list[str]] = field(default_factory=list)
evaluation_function: str = "arraySymbolicEqual"

def _config(self) -> Optional[dict[str, Any]]:
return {
Expand All @@ -242,8 +242,7 @@ class Code(ResponseArea):

response_type: ClassVar[str] = "CODE"
answer: str = ""
evaluation_function: str = "evaluatePython"
language: str = "python"
language: str = UNSET

def _config(self) -> Optional[dict[str, Any]]:
return {"language": self.language}
Expand All @@ -255,10 +254,10 @@ class Essay(ResponseArea):

response_type: ClassVar[str] = "ESSAY"
answer: Union[str, dict[str, str]] = ""
allow_draw: bool = False
allow_scan: bool = False
repeat_for_subjects: bool = False
allow_empty_submission: bool = False
allow_draw: bool = UNSET
allow_scan: bool = UNSET
repeat_for_subjects: bool = UNSET
allow_empty_submission: bool = UNSET

def _config(self) -> Optional[dict[str, Any]]:
return {
Expand All @@ -281,8 +280,8 @@ class Likert(ResponseArea):
] = None
statements: list[str] = field(default_factory=list)
options: Optional[list[str]] = None
repeat_for_subjects: bool = False
allow_empty_submission: bool = False
repeat_for_subjects: bool = UNSET
allow_empty_submission: bool = UNSET

def _config(self) -> Optional[dict[str, Any]]:
config: dict[str, Any] = {
Expand All @@ -300,12 +299,10 @@ class Images(ResponseArea):
"""Upload images."""

response_type: ClassVar[str] = "IMAGES"
max_images: int = 3
allowed_types: list[str] = field(
default_factory=lambda: ["image/jpg", "image/jpeg", "image/png"]
)
max_size_mb: int = 10
resize_max_side: int = 0
max_images: int = UNSET
allowed_types: list[str] = UNSET
max_size_mb: int = UNSET
resize_max_side: int = UNSET
"""The longest side images are resized to; 0 leaves them as they are."""

def _config(self) -> Optional[dict[str, Any]]:
Expand All @@ -315,3 +312,24 @@ def _config(self) -> Optional[dict[str, Any]]:
"maxSizeMb": self.max_size_mb,
"resizeMaxSide": self.resize_max_side,
}


RESPONSE_TYPES: dict[str, type[ResponseArea]] = {
cls.response_type: cls
for cls in (
MathSingleLine,
MathMultiLines,
Number,
NumericUnits,
Text,
Boolean,
MultipleChoice,
Table,
Matrix,
Code,
Essay,
Likert,
Images,
)
}
"""Each response type's class, by the name Lambda Feedback gives the type."""
33 changes: 14 additions & 19 deletions in2lambda/api/set.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

import panflute as pf

from in2lambda import defaults
from in2lambda.api.question import Question
from in2lambda.api.visibility_status import VisibilityController, VisibilityStatus
from in2lambda.api.visibility_status import VisibilityController
from in2lambda.defaults import UNSET


@dataclass
Expand All @@ -19,30 +21,23 @@ class Set:

_name: str = field(default="set")
_description: str = field(default="")
_finalAnswerVisibility: VisibilityController = field(
default_factory=lambda: VisibilityController(
VisibilityStatus.OPEN_WITH_WARNINGS
)
)
_workedSolutionVisibility: VisibilityController = field(
default_factory=lambda: VisibilityController(
VisibilityStatus.OPEN_WITH_WARNINGS
)
)
_structuredTutorialVisibility: VisibilityController = field(
default_factory=lambda: VisibilityController(VisibilityStatus.OPEN)
)
_chatbotVisibility: VisibilityController = field(
default_factory=lambda: VisibilityController(VisibilityStatus.HIDE)
)
is_survey: bool = False
# The settings below default to what in2lambda's defaults.toml gives them.
_finalAnswerVisibility: VisibilityController = UNSET
_workedSolutionVisibility: VisibilityController = UNSET
_structuredTutorialVisibility: VisibilityController = UNSET
_chatbotVisibility: VisibilityController = UNSET
is_survey: bool = UNSET
released_at: Optional[str] = None
hidden_at: Optional[str] = None
manually_hidden: bool = True
manually_hidden: bool = UNSET

questions: list[Question] = field(default_factory=list)
_current_question_index = -1

def __post_init__(self) -> None:
"""Gives each setting left unset its default."""
defaults.fill(self, "set")

@property
def current_question(self) -> Question:
"""The current question being modified, or Question("INVALID") if there are no questions.
Expand Down
Loading
Loading