Skip to content

Commit

Permalink
Public API for creating and loading answer from student name
Browse files Browse the repository at this point in the history
The existing functions did not allow to specify the student name without
changing the value of the dropdown menu. With the new function structure
we can now create and load new files from the student name.

One can create and load now with Python API the ExerciseWidget

```python
exercise_widget.create_new_file_from_student_name(name)
exercise_widget.load_file_from_student_name(name)
```
  • Loading branch information
agoscinski committed Dec 18, 2024
1 parent 1a00805 commit 7ddda88
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 44 deletions.
92 changes: 63 additions & 29 deletions src/scwidgets/exercise/_widget_exercise_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,14 @@ def answer(self, answer: dict):
def handle_save_result(self, result: Union[str, Exception]) -> None:
"""
Function that controls how a save result is handled. If the result is a string,
the saving was succesfull. The result contains a string that can be outputed.
the saving was successfull. The result contains a string that can be outputed.
"""
raise NotImplementedError("handle_save_result has not been implemented")

def handle_load_result(self, result: Union[str, Exception]) -> None:
"""
Function that controls how a load result is handled. If the result is a string,
the loading was succesfull. The result contains a string that can be outputed.
the loading was successfull. The result contains a string that can be outputed.
"""
raise NotImplementedError("handle_load_result has not been implemented")

Expand Down Expand Up @@ -271,7 +271,7 @@ def registered_widgets(self):
return self._widgets.copy()

@property
def loaded_file_name(self):
def loaded_file_name(self) -> Union[str, None]:
return self._loaded_file_name

def register_widget(self, widget: ExerciseWidget, exercise_key: Hashable):
Expand All @@ -284,24 +284,46 @@ def register_widget(self, widget: ExerciseWidget, exercise_key: Hashable):
"""
self._widgets[exercise_key] = widget

def create_new_file(self) -> str:
FilenameParser.verify_valid_student_name(self._student_name_text.value)
def create_new_file_from_dropdown(self) -> str:
"""Creates a new file containing all students answers from the selected
file in the dropdown menu.
:raises FileExistsError: If the file arleady exists
:return: A message to print
"""
self.create_new_file_from_student_name(self._student_name_text.value)
return f"File {self._loaded_file_name!r} created and loaded."

def get_answer_filename(self, student_name: str) -> str:
"""Returns the filename containing all answers for the student name.
:param student_name: The name of the student used for the filename
:raises ValueError: If student name is not valid
:return: The filename
"""
FilenameParser.verify_valid_student_name(student_name)

answers_filename = ""
# if prefix is defined, it is added to the filename
if self._filename_prefix is not None:
answers_filename += self._filename_prefix + "-"
student_name_standardized = FilenameParser.standardize_filename(
self._student_name_text.value
)
student_name_standardized = FilenameParser.standardize_filename(student_name)
answers_filename += student_name_standardized + ".json"
return answers_filename

def create_new_file_from_student_name(self, student_name: str):
"""Creates a new exercise file containing all students answers.
:param student_name: The name of the student used for the exercise file
:raises FileExistsError: If the file arleady exists
:return: A message to print
"""
answers_filename = self.get_answer_filename(student_name)

if os.path.exists(answers_filename):
raise ValueError(
"The name "
f"{student_name_standardized!r} "
f"is already used in file {answers_filename!r}. Please provide a "
"new name."
raise FileExistsError(
f"The name is already used for file {answers_filename!r}."
" Please provide a new name."
)
else:
answers = {key: widget.answer for key, widget in self._widgets.items()}
Expand All @@ -318,7 +340,6 @@ def create_new_file(self) -> str:
self._show_lower_panel_box()

self._loaded_file_name = answers_filename
return f"File {answers_filename!r} created and loaded."

def load_answer(self, exercise_key: Hashable) -> str:
"""
Expand Down Expand Up @@ -356,29 +377,41 @@ def load_answer(self, exercise_key: Hashable) -> str:
self._loaded_file_name = answers_filename
return f"Exercise has been loaded from file {answers_filename!r}."

def load_file(self) -> str:
self._answers_files_dropdown.value

def load_file_from_dropdown(self) -> str:
"""
Loads all answers from selected file in the dropdown menu
Loads all answers from the selected file in the dropdown menu.
"""
if (
self._answers_files_dropdown.value
== self._create_new_file_dropdown_option()
):
raise ValueError("No file has been selected in the dropdown list.")
if self._loaded_file_name is None:
if (
self._answers_files_dropdown.value
== self._create_new_file_dropdown_option()
):
raise ValueError("No file has been selected in the dropdown list.")
answers_filename = self._answers_files_dropdown.value
else:
answers_filename = self._loaded_file_name

self.load_file(answers_filename)
return f"All answers loaded from file {self._loaded_file_name!r}."

def load_file_from_student_name(self, student_name: str):
self.load_file(self.get_answer_filename(student_name))

def load_file(self, answers_filename: str):
"""
Loads all answers from the selected file in the dropdown menu.
:raises FileNotFoundError: If the file cannot be found
:raises ValueError: If the loaded file contains an answer with key that
has not been registered
"""

if not (os.path.exists(answers_filename)):
raise FileNotFoundError(
"Selected file does not exist anymore. Maybe you have renamed "
"or deleted it?Please choose another file or create a new one."
"or deleted it? Please choose another file or create a new one."
)

with open(answers_filename, "r") as answers_file:
Expand All @@ -393,11 +426,15 @@ def load_file(self) -> str:
self._widgets[exercise_key].answer = answer
self._loaded_file_name = answers_filename

# only notifiy all widgets when result was successful
# only notify all widgets when result was successful
for widget in self._widgets.values():
result = f"Exercise has been loaded from file {self._loaded_file_name!r}."
widget.handle_load_result(result)
return f"All answers loaded from file {answers_filename!r}."

self._answers_files_dropdown.value = answers_filename
self._disable_upper_panel_box()
self._enable_lower_panel_box()
self._show_lower_panel_box()

def save_answer(self, exercise_key: Hashable) -> str:
if not (exercise_key in self._widgets.keys()):
Expand Down Expand Up @@ -482,10 +519,7 @@ def _on_click_load_file_button(self, change: dict):
self._output.clear_output(wait=True)
with self._output:
try:
result = self.load_file()
self._disable_upper_panel_box()
self._enable_lower_panel_box()
self._show_lower_panel_box()
result = self.load_file_from_dropdown()
print(Formatter.color_success_message(result))
except Exception as exception:
print(Formatter.color_error_message("Error raised while loading file:"))
Expand All @@ -510,7 +544,7 @@ def _on_click_confirm_create_new_file_button(self, change: dict):
self._output.clear_output(wait=True)
with self._output:
try:
result = self.create_new_file()
result = self.create_new_file_from_dropdown()
print(Formatter.color_success_message(result))
except Exception as exception:
print(
Expand Down
28 changes: 14 additions & 14 deletions tests/test_answer.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def teardown_method(self, method):
for filename in glob.glob(os.getcwd() + f"/{self.prefix}-*.json"):
os.remove(filename)

def test_create_new_file(self):
def test_create_new_file_from_dropdown(self):
answers = {"exercise_1": "update", "exercise_2": "answer_2"}
with open(f"{self.prefix}-{self.student_name}-2.json", "w") as answer_file:
json.dump(answers, answer_file)
Expand All @@ -86,18 +86,18 @@ def test_create_new_file(self):
# simulating typing of name into text widget
answer_registry._student_name_text.value = self.student_name
assert not (os.path.exists(f"{self.prefix}-{self.student_name}.json"))
answer_registry.create_new_file()
answer_registry.create_new_file_from_dropdown()

with open(f"{self.prefix}-{self.student_name}.json", "r") as answer_file:
answers = json.load(answer_file)
assert answers == {"exercise_1": "answer_1"}

with pytest.raises(
ValueError,
match=r".*The name 'test-answer-registry' "
"is already used in file 'pytest-test-answer-registry.json'.*",
FileExistsError,
match=r".*The name is already used for file "
"'pytest-test-answer-registry.json'.*",
):
answer_registry.create_new_file()
answer_registry.create_new_file_from_dropdown()

def test_save_answer(self):
answer_registry = ExerciseRegistry(filename_prefix=self.prefix)
Expand All @@ -118,7 +118,7 @@ def test_save_answer(self):

# create file
answer_registry._student_name_text.value = self.student_name
answer_registry.create_new_file()
answer_registry.create_new_file_from_dropdown()

# Tests if error is raised on moved file
os.rename(f"{self.prefix}-{self.student_name}.json", "tmp.json")
Expand Down Expand Up @@ -157,7 +157,7 @@ def test_save_all_answers(self):

# create file
answer_registry._student_name_text.value = self.student_name
answer_registry.create_new_file()
answer_registry.create_new_file_from_dropdown()

# Tests if error is raised on moved file
os.rename(f"{self.prefix}-{self.student_name}.json", "tmp.json")
Expand All @@ -182,7 +182,7 @@ def test_save_all_answers(self):
answers = json.load(answer_file)
assert answers == {"exercise_1": "update", "exercise_2": "answer_2"}

def test_load_file(self):
def test_load_file_from_dropdown(self):
answers = {"exercise_1": "update_1", "exercise_2": "update_2"}
with open(f"{self.prefix}-{self.student_name}.json", "w") as answer_file:
json.dump(answers, answer_file)
Expand All @@ -204,7 +204,7 @@ def test_load_file(self):
with pytest.raises(
ValueError, match=r".*No file has been selected in the dropdown list.*"
):
answer_registry.load_file()
answer_registry.load_file_from_dropdown()
# select back file to load
answer_registry._answers_files_dropdown.value = (
f"{self.prefix}-{self.student_name}.json"
Expand All @@ -215,7 +215,7 @@ def test_load_file(self):
with pytest.raises(
FileNotFoundError, match=r".*Selected file does not exist anymore.*"
):
answer_registry.load_file()
answer_registry.load_file_from_dropdown()
os.rename("tmp.json", f"{self.prefix}-{self.student_name}.json")

# Test that file is contains only the updated answer
Expand All @@ -224,12 +224,12 @@ def test_load_file(self):
match=r".*Your file contains an answer with key 'exercise_2' "
r"with no corresponding registered widget.*",
):
answer_registry.load_file()
answer_registry.load_file_from_dropdown()

exercise_key_2 = "exercise_2"
answer_widget_2 = mock_answer_widget(answer_registry, exercise_key_2)
answer_widget_2.answer = "answer_2"
result = answer_registry.load_file()
result = answer_registry.load_file_from_dropdown()
assert (
result == "All answers loaded from file 'pytest-test-answer-registry.json'."
)
Expand Down Expand Up @@ -267,7 +267,7 @@ def test_load_answer(self):
answer_registry._answers_files_dropdown.value = (
f"{self.prefix}-{self.student_name}.json"
)
answer_registry.load_file()
answer_registry.load_file_from_dropdown()

# Tests if error is raised on moved file
os.rename(f"{self.prefix}-{self.student_name}.json", "tmp.json")
Expand Down
2 changes: 1 addition & 1 deletion tests/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ def print_success(code_ex: CodeExercise | None):
)

exercise_registry._student_name_text.value = "test_save_registry-student_name"
exercise_registry.create_new_file()
exercise_registry.create_new_file_from_dropdown()
code_ex._save_button.click()
os.remove("test_save_registry-student_name.json")

Expand Down

0 comments on commit 7ddda88

Please sign in to comment.