|
| 1 | +"""Represents a list of questions.""" |
| 2 | + |
| 3 | +from dataclasses import dataclass, field |
| 4 | +from typing import Union |
| 5 | + |
| 6 | +import panflute as pf |
| 7 | + |
| 8 | +from in2lambda.api.question import Question |
| 9 | +from in2lambda.api.visibility_status import VisibilityController, VisibilityStatus |
| 10 | + |
| 11 | + |
| 12 | +@dataclass |
| 13 | +class Set: |
| 14 | + """Represents a list of questions.""" |
| 15 | + |
| 16 | + _name: str = field(default="set") |
| 17 | + _description: str = field(default="") |
| 18 | + _finalAnswerVisibility: VisibilityController = field( |
| 19 | + default_factory=lambda: VisibilityController( |
| 20 | + VisibilityStatus.OPEN_WITH_WARNINGS |
| 21 | + ) |
| 22 | + ) |
| 23 | + _workedSolutionVisibility: VisibilityController = field( |
| 24 | + default_factory=lambda: VisibilityController( |
| 25 | + VisibilityStatus.OPEN_WITH_WARNINGS |
| 26 | + ) |
| 27 | + ) |
| 28 | + _structuredTutorialVisibility: VisibilityController = field( |
| 29 | + default_factory=lambda: VisibilityController(VisibilityStatus.OPEN) |
| 30 | + ) |
| 31 | + |
| 32 | + questions: list[Question] = field(default_factory=list) |
| 33 | + _current_question_index = -1 |
| 34 | + |
| 35 | + @property |
| 36 | + def current_question(self) -> Question: |
| 37 | + """The current question being modified, or Question("INVALID") if there are no questions. |
| 38 | +
|
| 39 | + The reasoning behind returning Question("INVALID") is in case filter logic is being applied |
| 40 | + on text before the first question (e.g. intro paragraphs). In that case, there is no effect. |
| 41 | +
|
| 42 | + Returns: |
| 43 | + The current question or Question("INVALID") if there are no questions. |
| 44 | +
|
| 45 | + Examples: |
| 46 | + >>> from in2lambda.api.set import Set |
| 47 | + >>> Set().current_question |
| 48 | + Question(title='INVALID', parts=[], images=[], main_text='') |
| 49 | + >>> s = Set() |
| 50 | + >>> s.add_question() |
| 51 | + >>> s.current_question |
| 52 | + Question(title='', parts=[], images=[], main_text='') |
| 53 | + """ |
| 54 | + return ( |
| 55 | + self.questions[self._current_question_index] |
| 56 | + if self.questions |
| 57 | + else Question("INVALID") |
| 58 | + ) |
| 59 | + |
| 60 | + def add_question( |
| 61 | + self, title: str = "", main_text: Union[pf.Element, str] = pf.Str("") |
| 62 | + ) -> None: |
| 63 | + """Inserts a new question into the set. |
| 64 | +
|
| 65 | + Args: |
| 66 | + title: An optional string for the title of the question. If no title |
| 67 | + is provided, the question title auto-increments i.e. Question 1, 2, etc. |
| 68 | + main_text: An optional string or panflute element for the main question text. |
| 69 | +
|
| 70 | + Examples: |
| 71 | + >>> from in2lambda.api.set import Set |
| 72 | + >>> import panflute as pf |
| 73 | + >>> s = Set() |
| 74 | + >>> s.add_question("Some title", pf.Para(pf.Str("hello"), pf.Space, pf.Str("there"))) |
| 75 | + >>> s.questions |
| 76 | + [Question(title='Some title', parts=[], images=[], main_text='hello there')] |
| 77 | + >>> s.add_question(main_text="Normal string text") |
| 78 | + >>> s.questions[1].main_text |
| 79 | + 'Normal string text' |
| 80 | + """ |
| 81 | + question = Question(title=title) |
| 82 | + question.main_text = main_text |
| 83 | + self.questions.append(question) |
| 84 | + |
| 85 | + def increment_current_question(self) -> None: |
| 86 | + """Manually overrides the current question being modified. |
| 87 | +
|
| 88 | + The default (-1) indicates the last question added. Incrementing for the |
| 89 | + first time sets to 0 i.e. the first question. |
| 90 | +
|
| 91 | + The is useful if adding question text first and answers later. |
| 92 | +
|
| 93 | + Examples: |
| 94 | + >>> from in2lambda.api.set import Set |
| 95 | + >>> s = Set() |
| 96 | + >>> # Imagine adding the questions from a question file first... |
| 97 | + >>> s.add_question("Question 1") |
| 98 | + >>> s.add_question("Question 2") |
| 99 | + >>> # ...and then adding solutions from an answer file later |
| 100 | + >>> s.increment_current_question() # Loop back to question 1 |
| 101 | + >>> s.current_question.add_solution("Question 1 answer") |
| 102 | + >>> s.increment_current_question() |
| 103 | + >>> s.current_question.add_solution("Question 2 answer") |
| 104 | + >>> s.questions |
| 105 | + [Question(title='Question 1', parts=[Part(text='', worked_solution='Question 1 answer')], images=[], main_text=''),\ |
| 106 | + Question(title='Question 2', parts=[Part(text='', worked_solution='Question 2 answer')], images=[], main_text='')] |
| 107 | + """ |
| 108 | + self._current_question_index += 1 |
| 109 | + |
| 110 | + def to_json(self, output_dir: str) -> None: |
| 111 | + """Turns this set into Lambda Feedback JSON/ZIP files. |
| 112 | +
|
| 113 | + WARNING: This will overwrite any existing files in the directory. |
| 114 | +
|
| 115 | + Args: |
| 116 | + output_dir: Where to output the final Lambda Feedback JSON/ZIP files. |
| 117 | +
|
| 118 | + Examples: |
| 119 | + >>> import tempfile |
| 120 | + >>> import os |
| 121 | + >>> import json |
| 122 | + >>> # Create a set with two questions |
| 123 | + >>> s = Set() |
| 124 | + >>> s.add_question("Question 1") |
| 125 | + >>> s.add_question("Question 2") |
| 126 | + >>> with tempfile.TemporaryDirectory() as temp_dir: |
| 127 | + ... # Write the JSON files to the temporary directory |
| 128 | + ... s.to_json(temp_dir) |
| 129 | + ... # Check the contents of the directory |
| 130 | + ... sorted(os.listdir(temp_dir)) |
| 131 | + ... # Check the contents of the set directory |
| 132 | + ... sorted(os.listdir(f"{temp_dir}/set")) |
| 133 | + ... # Check the title of the first question |
| 134 | + ... with open(f"{temp_dir}/set/question_000_Question_1.json") as file: |
| 135 | + ... print(f"Question 1's title: {json.load(file)['title']}") |
| 136 | + ['set', 'set.zip'] |
| 137 | + ['question_000_Question_1.json', 'question_001_Question_2.json', 'set_set.json'] |
| 138 | + Question 1's title: Question 1 |
| 139 | + """ |
| 140 | + from in2lambda.json_convert import json_convert |
| 141 | + |
| 142 | + json_convert.main(self, output_dir) |
| 143 | + |
| 144 | + def set_name(self, name: str) -> None: |
| 145 | + """Sets the name of the set. |
| 146 | +
|
| 147 | + Args: |
| 148 | + name: The name to set for the set. |
| 149 | +
|
| 150 | + Examples: |
| 151 | + >>> from in2lambda.api.set import Set |
| 152 | + >>> s = Set() |
| 153 | + >>> s.set_name("My Question Set") |
| 154 | + >>> s._name |
| 155 | + 'My Question Set' |
| 156 | + """ |
| 157 | + self._name = name |
| 158 | + |
| 159 | + def set_description(self, description: str) -> None: |
| 160 | + """Sets the description of the set. |
| 161 | +
|
| 162 | + Args: |
| 163 | + description: The description to set for the set. |
| 164 | +
|
| 165 | + Examples: |
| 166 | + >>> from in2lambda.api.set import Set |
| 167 | + >>> s = Set() |
| 168 | + >>> s.set_description("This is my question set.") |
| 169 | + >>> s._description |
| 170 | + 'This is my question set.' |
| 171 | + """ |
| 172 | + self._description = description |
| 173 | + |
| 174 | + def __repr__(self): |
| 175 | + """Custom representation showing visibility status values instead of memory addresses.""" |
| 176 | + return ( |
| 177 | + f"Set(" |
| 178 | + f"_name={self._name!r}, " |
| 179 | + f"_description={self._description!r}, " |
| 180 | + f"_finalAnswerVisibility={str(self._finalAnswerVisibility)!r}, " |
| 181 | + f"_workedSolutionVisibility={str(self._workedSolutionVisibility)!r}, " |
| 182 | + f"_structuredTutorialVisibility={str(self._structuredTutorialVisibility)!r}, " |
| 183 | + f"questions={self.questions!r}" |
| 184 | + f")" |
| 185 | + ) |
0 commit comments