diff --git a/dist/cyllene-0.4.5-py3-none-any.whl b/dist/cyllene-0.4.5-py3-none-any.whl index dc2505b..6b17a65 100644 Binary files a/dist/cyllene-0.4.5-py3-none-any.whl and b/dist/cyllene-0.4.5-py3-none-any.whl differ diff --git a/dist/cyllene-0.4.5.tar.gz b/dist/cyllene-0.4.5.tar.gz index b5cc533..9437798 100644 Binary files a/dist/cyllene-0.4.5.tar.gz and b/dist/cyllene-0.4.5.tar.gz differ diff --git a/dist/cyllene-0.5-py3-none-any.whl b/dist/cyllene-0.5-py3-none-any.whl new file mode 100644 index 0000000..f00bb46 Binary files /dev/null and b/dist/cyllene-0.5-py3-none-any.whl differ diff --git a/dist/cyllene-0.5.tar.gz b/dist/cyllene-0.5.tar.gz new file mode 100644 index 0000000..ce6ab8b Binary files /dev/null and b/dist/cyllene-0.5.tar.gz differ diff --git a/pyproject.toml b/pyproject.toml index 57e67b6..0669c6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "cyllene" -version = "0.4.5" +version = "0.5" description = "IPython package for math problem authoring" readme = "README.md" authors = [{ name = "Jan Reimann", email = "jan.reimann@psu.edu" }] diff --git a/src/cyllene.egg-info/PKG-INFO b/src/cyllene.egg-info/PKG-INFO index 67131c3..8591ba5 100644 --- a/src/cyllene.egg-info/PKG-INFO +++ b/src/cyllene.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: cyllene -Version: 0.4.5 +Version: 0.5 Summary: IPython package for math problem authoring Author-email: Jan Reimann License: MIT License diff --git a/src/cyllene.egg-info/SOURCES.txt b/src/cyllene.egg-info/SOURCES.txt index 625df8b..ab9005c 100644 --- a/src/cyllene.egg-info/SOURCES.txt +++ b/src/cyllene.egg-info/SOURCES.txt @@ -19,6 +19,7 @@ src/cyllene/MathFunctions/math_string.py src/cyllene/MathFunctions/math_table.py src/cyllene/MathProblems/problem_aux.py src/cyllene/MathProblems/problem_basic.py +src/cyllene/MathProblems/problem_handler.py src/cyllene/MathProblems/problem_instantiator.py src/cyllene/MathProblems/problem_parameter.py src/cyllene/MathProblems/problem_parametermultchoice.py @@ -36,6 +37,7 @@ src/cyllene/user/problem_stack.py src/cyllene/widgets/ipyvuetify_multiplechoice.py src/cyllene/widgets/vue_problem_basic.py src/cyllene/widgets/vue_problem_parameter.py +src/cyllene/widgets/widget_viewer.py src/cyllene/widgets/widgets_aux.py src/cyllene/widgets/widgets_problem_basic.py src/cyllene/widgets/widgets_problem_param.py \ No newline at end of file diff --git a/src/cyllene/MathProblems/__pycache__/problem_basic.cpython-310.pyc b/src/cyllene/MathProblems/__pycache__/problem_basic.cpython-310.pyc index 2f9de95..5d8f151 100644 Binary files a/src/cyllene/MathProblems/__pycache__/problem_basic.cpython-310.pyc and b/src/cyllene/MathProblems/__pycache__/problem_basic.cpython-310.pyc differ diff --git a/src/cyllene/MathProblems/__pycache__/problem_handler.cpython-310.pyc b/src/cyllene/MathProblems/__pycache__/problem_handler.cpython-310.pyc new file mode 100644 index 0000000..94bd8b6 Binary files /dev/null and b/src/cyllene/MathProblems/__pycache__/problem_handler.cpython-310.pyc differ diff --git a/src/cyllene/MathProblems/__pycache__/problem_parameter.cpython-310.pyc b/src/cyllene/MathProblems/__pycache__/problem_parameter.cpython-310.pyc index 5122555..f84d143 100644 Binary files a/src/cyllene/MathProblems/__pycache__/problem_parameter.cpython-310.pyc and b/src/cyllene/MathProblems/__pycache__/problem_parameter.cpython-310.pyc differ diff --git a/src/cyllene/MathProblems/problem_basic.py b/src/cyllene/MathProblems/problem_basic.py index 44c4d4e..12f5990 100644 --- a/src/cyllene/MathProblems/problem_basic.py +++ b/src/cyllene/MathProblems/problem_basic.py @@ -57,6 +57,10 @@ def check_answer(self, answer_string): return self.answer == answer_string + @property + def has_solution(self): + return bool(self.solution) + class MultipleChoice(Problem): @@ -82,6 +86,9 @@ def __init__(self, my_dict={}): if not isinstance(self.choices, list): self.choices = [self.choices] + # shuffle answers and store them in a separate list + self.shuffle_answers() + @property def num_choices(self): return len(self.choices) @@ -97,10 +104,15 @@ def load_dict(self, my_dict): if len(self.choices) > 0: self.answer = str(self.choices[0]) - def shuffle_choices(self): + # def shuffle_choices(self): + + # shuffled_choices = self.choices + # return random.shuffle(shuffled_choices) - shuffled_choices = self.choices - return random.shuffle(shuffled_choices) + def shuffle_answers(self): + self.indices = [i for i in range(self.num_choices)] + random.shuffle(self.indices) + self.correct = self.indices.index(0) class ExpressionProblem(Problem): diff --git a/src/cyllene/MathProblems/problem_handler.py b/src/cyllene/MathProblems/problem_handler.py new file mode 100644 index 0000000..d71fdf7 --- /dev/null +++ b/src/cyllene/MathProblems/problem_handler.py @@ -0,0 +1,43 @@ +import random +# from ..MathProblems.problem_basic import Problem, MultipleChoice +from ..MathProblems.problem_parameter import ParameterProblem +# from ..MathProblems.problem_parametermultchoice import MultipleChoiceParameterProblem +# from .widgets_aux import update_output_widget +""" +Defines a class for basic problem handling: statement, type, answer +and checking +""" + + +class ProblemHandler: + """ + Pass a problem instance (or a list thereof) and display it using corresponding widgets + """ + + def __init__(self, problems: list): + + self.problems = problems + self.status = 'undecided' + self.check = [] + self.regenerates = False + self.current_problem = None + + # determine whether several problems are present and whether they regenerate + if self.has_problem: + if len(problems) == 1: + if isinstance(problems[0], ParameterProblem): + self.regenerates = True + else: + self.regenerates = True + + @property + def has_problem(self): + return bool(self.problems) + + def select_current_problem(self): + if not self.has_problem: + return + + self.current_problem = random.choice(self.problems) + if isinstance(self.current_problem, ParameterProblem): + self.current_problem = self.current_problem.get_problem() diff --git a/src/cyllene/__init__.py b/src/cyllene/__init__.py index dd06686..beae2dd 100644 --- a/src/cyllene/__init__.py +++ b/src/cyllene/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.4.5" +__version__ = "0.5" from .user.problem_stack import ProbStack from .magics import problem_magics diff --git a/src/cyllene/widgets/__pycache__/widget_viewer.cpython-310.pyc b/src/cyllene/widgets/__pycache__/widget_viewer.cpython-310.pyc new file mode 100644 index 0000000..1e2b90e Binary files /dev/null and b/src/cyllene/widgets/__pycache__/widget_viewer.cpython-310.pyc differ diff --git a/src/cyllene/widgets/__pycache__/widgets_problem_basic.cpython-310.pyc b/src/cyllene/widgets/__pycache__/widgets_problem_basic.cpython-310.pyc index e1c6ab9..90afba3 100644 Binary files a/src/cyllene/widgets/__pycache__/widgets_problem_basic.cpython-310.pyc and b/src/cyllene/widgets/__pycache__/widgets_problem_basic.cpython-310.pyc differ diff --git a/src/cyllene/widgets/__pycache__/widgets_problem_param.cpython-310.pyc b/src/cyllene/widgets/__pycache__/widgets_problem_param.cpython-310.pyc index 7960d8e..47a2d6b 100644 Binary files a/src/cyllene/widgets/__pycache__/widgets_problem_param.cpython-310.pyc and b/src/cyllene/widgets/__pycache__/widgets_problem_param.cpython-310.pyc differ diff --git a/src/cyllene/widgets/widget_viewer.py b/src/cyllene/widgets/widget_viewer.py new file mode 100644 index 0000000..e7e409c --- /dev/null +++ b/src/cyllene/widgets/widget_viewer.py @@ -0,0 +1,168 @@ +from IPython.display import display, Markdown, clear_output +import ipywidgets as widgets +import markdown +from ..MathProblems.problem_handler import ProblemHandler +from ..MathProblems.problem_basic import MultipleChoice +from ..MathProblems.problem_parameter import ParameterProblem +from ..MathProblems.problem_parametermultchoice import MultipleChoiceParameterProblem +from .widgets_aux import update_output_widget +""" +Defines a class for basic problem handling: statement, type, answer +and checking +""" + +COLORS = ['primary', 'success', 'info', 'warning', 'danger'] + + +class WidgetViewer: + """ + Pass a ProblemHandler instance and create a viewer using corresponding widgets + """ + + def __init__(self, problem: ProblemHandler): + + self.problem = problem + self.problem.select_current_problem() + + # initialize output widgets for various areas + self.title = widgets.Output() + self.statement = widgets.Output() + self.user_answer_area = widgets.Output() + self.button_area = widgets.Output() + self.feedback = widgets.Output(layout=widgets.Layout(height='50px')) + self.solution_text = widgets.Output() + self.solution_area = widgets.Accordion(children=[self.solution_text]) + + self.new_button = widgets.Button(description='New Version', + disabled=False) + self.new_button.on_click(self.new_button_clicked) + + def new_button_clicked(self, bt): + + self.problem.select_current_problem() + + # update display + self.build_problem_widget() + + def build_title(self): + update_output_widget(self.title, '### ' + + self.problem.current_problem.title) + + def build_statement(self): + update_output_widget( + self.statement, self.problem.current_problem.statement) + + def build_solution(self): + with self.solution_text: + clear_output() + display(widgets.HTMLMath( + markdown.markdown(self.problem.current_problem.solution))) + + self.solution_area.selected_index = None + self.solution_area.set_title(0, 'Show Solution') + + def build_user_answer(self): + + if isinstance(self.problem.current_problem, MultipleChoice): + self.problem.current_problem.shuffle_answers() + with self.user_answer_area: + clear_output() + + for i in range(self.problem.current_problem.num_choices): + display(widgets.HTMLMath( + value='( ' + str(i+1) + ' )    ' + self.problem.current_problem.choices[self.problem.current_problem.indices[i]] + "       ")) + + def build_choice_buttons(self): + + # create buttons + self.choice_buttons = [ + widgets.Button( + description='( '+str(i+1)+' )', + disabled=False, + button_style=COLORS[i], + tooltip='Answer choice '+str(i+1), + layout=widgets.Layout(flex='1 1 auto', width='auto')) + for i in range(self.problem.current_problem.num_choices)] + + # Activate handler for every button + for button in self.choice_buttons: + # link to a click event function + button.on_click(self.on_choice_button_clicked) + + # display buttons in button area widget + with self.button_area: + clear_output() + display(widgets.HBox(self.choice_buttons)) + + def on_choice_button_clicked(self, bt): + + self.check_answer(bt.description[2:-2]) + + def check_answer(self, answer): + + # reset current answer check + self.check = [] + + # Pre-process answer string to remove parentheses (if present) + answer = answer[0] + if len(answer) > 0 and answer[0] == '(': + answer = answer[1:] + if len(answer) > 0 and answer[-1] == ')': + answer = answer[:-1] + + try: + if self.problem.current_problem.correct == int(answer)-1: + self.check.append(True) + else: + self.check.append(False) + except ValueError: + self.check.append('Error') + + if self.check[0] == True: + result_string = 'You selected: ('+answer+') -- ' + \ + '✅   **Correct!**' + self.status = 'correct' + elif self.check[0] == False: + result_string = 'You selected: ('+answer+') -- ' + \ + '❌   **Incorrect**' + self.status = 'incorrect' + else: + result_string = 'Please enter an integer value.' + self.status = 'undecided' + + # show feedback + with self.feedback: + clear_output() + display(Markdown(result_string)) + + def build_problem_widget(self): + + # build widgets and add to widget list depending on current problem type + self.build_title() + self.widget_list = [self.title] + + if self.problem.regenerates: + self.widget_list.append(self.new_button) + + self.build_statement() + self.widget_list.append(self.statement) + + self.build_user_answer() + self.widget_list.append(self.user_answer_area) + + if isinstance(self.problem.current_problem, MultipleChoice): + self.build_choice_buttons() + self.widget_list.append(self.button_area) + + with self.feedback: + clear_output() + display(Markdown('Please select your answer.')) + self.widget_list.append(self.feedback) + + if self.problem.current_problem.has_solution: + self.build_solution() + self.widget_list.append(self.solution_area) + + def show(self): + self.build_problem_widget() + display(widgets.VBox(self.widget_list)) diff --git a/src/cyllene/widgets/widgets_problem_basic.py b/src/cyllene/widgets/widgets_problem_basic.py index cfb06c8..33b9c59 100644 --- a/src/cyllene/widgets/widgets_problem_basic.py +++ b/src/cyllene/widgets/widgets_problem_basic.py @@ -14,7 +14,7 @@ class ProblemWidget: """ - Show a basic widget displaying title and statement + Show a basic widget displaying title, statement, and solution """ def __init__(self, problem: Problem): @@ -43,7 +43,8 @@ def show(self): self.statement, self.problem.statement) display(widgets.VBox([self.title, self.statement])) - display(self.solution) + if self.has_solution: + display(self.solution) class MultipleChoiceWidget(ProblemWidget): diff --git a/src/cyllene/widgets/widgets_problem_param.py b/src/cyllene/widgets/widgets_problem_param.py index 8476253..5fca79a 100644 --- a/src/cyllene/widgets/widgets_problem_param.py +++ b/src/cyllene/widgets/widgets_problem_param.py @@ -72,7 +72,7 @@ def update_problem_area(self): for i in range(self.problem.num_choices): display(widgets.HTMLMath( - value='( ' + str(i+1) + ')    ' + self.problem.choices[self.indices[i]] + "       ")) + value='( ' + str(i+1) + ' )    ' + self.problem.choices[self.indices[i]] + "       ")) def new_button_clicked(self, bt):