Skip to content
Draft
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
25 changes: 22 additions & 3 deletions in2lambda/api/part.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
"""A part of a question."""

from dataclasses import dataclass
from dataclasses import dataclass, field

from in2lambda.api.response_area import ResponseArea


@dataclass
class Branch:
"""A (titled) branch of a structured tutorial or worked solution, which may branch further."""

title: str
content: str
branches: list["Branch"] = field(default_factory=list)


@dataclass
class Part:
"""A part of a question as represented on Lambda Feedback."""
"""A part of a question as represented on Lambda Feedback.

Markdown content for the question and supporting materials. Note that in ``tutorial`` and ``worked_solution`` fields, steps can by separated by a line with only ``---`` (or ``***``).
"""

text: str = ""
content: str = ""
response_areas: list[ResponseArea] = field(default_factory=list)
tutorial: str = ""
tutorial_branches: list[Branch] = field(default_factory=list)
answer: str = ""
worked_solution: str = ""
worked_solution_branches: list[Branch] = field(default_factory=list)
77 changes: 47 additions & 30 deletions in2lambda/api/question.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""A full question with optional parts that's contained in a set."""

from dataclasses import dataclass, field
from typing import Union
from typing import Optional, Union

import panflute as pf

Expand All @@ -14,30 +14,47 @@ class Question:

Each question has a title and is composed of a list of parts.

It also carries the settings Lambda Feedback keeps per question.

Examples:
>>> from in2lambda.api.question import Question
>>> Question(title="Some title", main_text="Some text")
Question(title='Some title', parts=[], images=[], main_text='Some text')
>>> Question(title="Some title", master_content="Some text")
Question(title='Some title', parts=[], images=[], master_content='Some text')
"""

title: str = ""

parts: list[Part] = field(default_factory=list)
images: list[str] = field(default_factory=list)

# Use getter/setter methods for main_text without exposing private attribute.
# Use getter/setter methods for master_content without exposing private attribute.
# See https://stackoverflow.com/a/61480946
main_text: str = ""
_main_text: str = field(init=False, repr=False, default="")
master_content: str = ""
_master_content: str = field(init=False, repr=False, default="")

_last_part: dict[str, int] = field(
default_factory=lambda: {"solution": 0, "text": 0}, repr=False
)
"""Keeps track of the last question part that contains a solution /
text."""

# The settings below are left out of the repr, so that printing a question shows its
# content rather than nine lines of configuration.

# 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)

@property
def main_text(self) -> str:
def master_content(self) -> str:
r"""Main top-level question text.

Setting the attribute multiple times appends to the current value with a newline.
Expand All @@ -46,21 +63,21 @@ def main_text(self) -> str:
Examples:
>>> from in2lambda.api.question import Question
>>> question = Question()
>>> question.main_text = "hello"
>>> question.main_text = "there"
>>> question.main_text
>>> question.master_content = "hello"
>>> question.master_content = "there"
>>> question.master_content
'hello\nthere'
"""
return self._main_text
return self._master_content

@main_text.setter
def main_text(self, value: Union[pf.Element, str, property]) -> None:
@master_content.setter
def master_content(self, value: Union[pf.Element, str, property]) -> None:
r"""Appends to the top-level main text, which starts off as an empty string.

Args:
value: A panflute element or string denoting what to append to the main text.

See example in main_text property.
See example in master_content property.
"""
# Converts the inputted value into a string, stored in text_value
match value:
Expand All @@ -69,13 +86,13 @@ def main_text(self, value: Union[pf.Element, str, property]) -> None:
case property():
# Use default value when no value set at initialisation.
# See https://stackoverflow.com/a/61480946
text_value = self.main_text
text_value = self.master_content
case pf.Element():
text_value = pf.stringify(value, False)

if self._main_text:
self._main_text += "\n"
self._main_text += text_value
if self._master_content:
self._master_content += "\n"
self._master_content += text_value

def add_solution(self, elem: Union[pf.Element, str]) -> None:
"""Adds a worked solution to all question parts without one, or inserts a new empty part with the solution if all parts already have a solution.
Expand All @@ -89,20 +106,20 @@ def add_solution(self, elem: Union[pf.Element, str]) -> None:
>>> question.add_part_text("part a")
>>> question.add_solution("part a solution")
>>> question
Question(title='', parts=[Part(text='part a', worked_solution='part a solution')], images=[], main_text='')
Question(title='', parts=[Part(content='part a', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='part a solution', worked_solution_branches=[])], images=[], master_content='')
>>> question.add_part_text("part b")
>>> question.add_part_text("part c")
>>> question.add_solution("Solution for b")
>>> # Note that since c doesn't have a solution, it's set to b's solution
>>> question
Question(title='', parts=[Part(text='part a', worked_solution='part a solution'), \
Part(text='part b', worked_solution='Solution for b'), \
Part(text='part c', worked_solution='Solution for b')], images=[], main_text='')
Question(title='', parts=[Part(content='part a', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='part a solution', worked_solution_branches=[]), \
Part(content='part b', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='Solution for b', worked_solution_branches=[]), \
Part(content='part c', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='Solution for b', worked_solution_branches=[])], images=[], master_content='')
>>> question.add_solution("We now have a solution for c!")
>>> question
Question(title='', parts=[Part(text='part a', worked_solution='part a solution'), \
Part(text='part b', worked_solution='Solution for b'), \
Part(text='part c', worked_solution='We now have a solution for c!')], images=[], main_text='')
Question(title='', parts=[Part(content='part a', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='part a solution', worked_solution_branches=[]), \
Part(content='part b', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='Solution for b', worked_solution_branches=[]), \
Part(content='part c', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='We now have a solution for c!', worked_solution_branches=[])], images=[], master_content='')
"""
elem_text = elem if isinstance(elem, str) else pf.stringify(elem)

Expand Down Expand Up @@ -131,19 +148,19 @@ def add_part_text(self, elem: Union[pf.Element, str]) -> None:
>>> question.add_part_text("part a")
>>> question.add_solution("part a solution")
>>> question
Question(title='', parts=[Part(text='part a', worked_solution='part a solution')], images=[], main_text='')
Question(title='', parts=[Part(content='part a', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='part a solution', worked_solution_branches=[])], images=[], master_content='')
>>> # Supports adding the answer first.
>>> question.add_solution("part b solution")
>>> question.add_part_text("part b")
>>> question
Question(title='', parts=[Part(text='part a', worked_solution='part a solution'), \
Part(text='part b', worked_solution='part b solution')], images=[], main_text='')
Question(title='', parts=[Part(content='part a', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='part a solution', worked_solution_branches=[]), \
Part(content='part b', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='part b solution', worked_solution_branches=[])], images=[], master_content='')
"""
elem_text = elem if isinstance(elem, str) else pf.stringify(elem)

if len(self.parts) == self._last_part["text"]:
self.parts.append(Part(text=elem_text))
self.parts.append(Part(content=elem_text))
else:
self.parts[self._last_part["text"]].text = elem_text
self.parts[self._last_part["text"]].content = elem_text

self._last_part["text"] += 1
Loading
Loading