diff --git a/demo-notebooks/demo08-quiz.py b/demo-notebooks/demo08-quiz.py index 0e0a66be..8f463e3f 100644 --- a/demo-notebooks/demo08-quiz.py +++ b/demo-notebooks/demo08-quiz.py @@ -28,7 +28,7 @@ # --- # %% [markdown] -# # some quizzes +# # one sample quiz # %% {"scrolled": true} # mostly for using under binder or in a devel tree @@ -41,17 +41,7 @@ # %autoreload 2 # %% [markdown] -# ## a quizz is made of questions - -# %% [markdown] -# a Quiz object contains one or several questions; here is an example with a single question - -# %% {"scrolled": false} -from exercises.quizsample import single_quiz -single_quiz.widget() - -# %% [markdown] -# ## more questions +# ## a quiz is made of questions # # in this notebook the correct answers are always the one starting with 'a' @@ -65,5 +55,5 @@ # %% [markdown] # Here's the code that defines the above quizz -# %% {"scrolled": true} +# %% {"scrolled": false} # !cat ../exercises/quizsample.py diff --git a/demo-notebooks/demo09-quiz2.py b/demo-notebooks/demo09-quiz2.py new file mode 100644 index 00000000..635c3aac --- /dev/null +++ b/demo-notebooks/demo09-quiz2.py @@ -0,0 +1,60 @@ +# --- +# jupyter: +# jupytext: +# cell_metadata_filter: all +# cell_metadata_json: true +# formats: py:percent +# notebook_metadata_filter: all,-language_info,-jupytext.text_representation.jupytext_version +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# toc: +# base_numbering: 1 +# nav_menu: {} +# number_sections: true +# sideBar: true +# skip_h1_title: false +# title_cell: Table of Contents +# title_sidebar: Contents +# toc_cell: false +# toc_position: {} +# toc_section_display: true +# toc_window_display: false +# --- + +# %% [markdown] +# # another quiz + +# %% {"scrolled": true} +# mostly for using under binder or in a devel tree +import sys +sys.path.append('..') + +# %% {"scrolled": true} +# for convenience in development +# %load_ext autoreload +# %autoreload 2 + +# %% [markdown] +# ## a quizz is made of questions + +# %% [markdown] +# a Quiz object contains one or several questions; here is an example with a single question + +# %% {"scrolled": false} +from exercises.quizsample2 import quiz2 +quiz2.widget() + +# %% [markdown] +# ## under the hood + +# %% [markdown] +# Here's the code that defines the above quizz + +# %% {"scrolled": false} +# !cat ../exercises/quizsample2.py diff --git a/exercises/quizsample.py b/exercises/quizsample.py index a1b08e26..c238e958 100644 --- a/exercises/quizsample.py +++ b/exercises/quizsample.py @@ -1,22 +1,5 @@ from nbautoeval import Quiz, QuizQuestion, Option, CodeOption, MathOption -### -# most basic single-answer - -question_basic_single = QuizQuestion( - "Pick the right fruit\n(one correct option)", - options=[ - Option("banana"), - Option("pear"), - Option("apple", correct=True), - ], - horizontal=True, -) -single_quiz = Quiz( - "quizsample-single", - [question_basic_single]) - - questions = [] ### question_basic_multiple = QuizQuestion( @@ -30,7 +13,7 @@ Option("pineapple"), ], score = 1, - horizontal=True, + horizontal_layout=True, ) questions.append(question_basic_multiple) @@ -46,7 +29,7 @@ ], shuffle=False, score = 2, - horizontal=True, + horizontal_layout=True, ) questions.append(question_unshuffle) @@ -65,7 +48,7 @@ MathOption(r"multiple double dollars $$\forall x\in\mathbb{R}$$ $$\forall x\in\mathbb{R}$$ $$\forall x\in\mathbb{R}$$"), ], score = 3, - horizontal=True, + horizontal_layout=True, ) questions.append(question_math) @@ -78,7 +61,8 @@ Option("pear"), ], score = 4, - horizontal=True, + horizontal_layout=True, + horizontal_options=True, ) questions.append(question_none) @@ -93,7 +77,7 @@ CodeOption("b = sort(x for x in list if x.is_valid())"), ], score = 5, - horizontal=True, + horizontal_options=True, ) questions.append(question_code) diff --git a/exercises/quizsample2.py b/exercises/quizsample2.py new file mode 100644 index 00000000..b993e4aa --- /dev/null +++ b/exercises/quizsample2.py @@ -0,0 +1,58 @@ +from nbautoeval import Quiz, QuizQuestion, Option, CodeOption, MathOption + + +############ +quiz2 = Quiz( + + # needs a unique name for storing progress and marks + "quizsample-horizontal", + + [ + QuizQuestion(""" +horizontal_layout means to have +
the questions and the options +
in a horizontal box""", + options=[ + Option('', correct=True), + Option(''), + ], + horizontal_layout=True, + ), + + + QuizQuestion(""" +horizontal_options means the options appear side by side like here, +because horizontal_layout is False the question spans 100% of page width +""", + options=[ + Option('', correct=True), + Option(''), + ], + horizontal_options=True, + ), + + + QuizQuestion(""" +the default is to have none of these 2 horizontal flags +""", + options=[ + Option('', correct=True), + Option(''), + ], + ), + + QuizQuestion(""" +of course they can be used together as well""", + options=[ + Option('', correct=True), + Option(''), + ], + horizontal_layout=True, + horizontal_options=True, + ), + + + ], + max_attempts = 5, + ) + diff --git a/media/image1.png b/media/image1.png new file mode 100644 index 00000000..e68a9bea Binary files /dev/null and b/media/image1.png differ diff --git a/media/image2.png b/media/image2.png new file mode 100644 index 00000000..a899de4b Binary files /dev/null and b/media/image2.png differ diff --git a/nbautoeval/quiz.py b/nbautoeval/quiz.py index 465dd0bc..0c9e5539 100644 --- a/nbautoeval/quiz.py +++ b/nbautoeval/quiz.py @@ -47,7 +47,7 @@ def __iter__(self): # usually options are displayed in some random order class _DisplayedOptionsList: - def __init__(self, options: List[GenericBooleanOption], shuffle=True): + def __init__(self, options: List[GenericBooleanOption], shuffle): self.displayed = options[:] if shuffle: random.shuffle(self.displayed) @@ -126,6 +126,11 @@ def __iter__(self): } """ +def points(score): + return f"{score} {'pt' if score<=1 else 'pts'}" + + + class QuizQuestion: """ question may include html tags and/or math content between '$$' @@ -144,12 +149,15 @@ class QuizQuestion: def __init__(self, question: str, options: List, *, score = 1, - shuffle=True, horizontal=False): + shuffle=True, + horizontal_layout=False, + horizontal_options=False): self.question = question self.options_list = _OptionsList(options) - self.displayed = _DisplayedOptionsList(options) + self.displayed = _DisplayedOptionsList(options, shuffle) self.score = score - self.horizontal = horizontal + self.horizontal_layout = horizontal_layout + self.horizontal_options = horizontal_options self.feedback_area = None self._widget_instance = None @@ -159,8 +167,7 @@ def widget(self): if self._widget_instance: return self._widget_instance - points = f"{self.score} {'pt' if self.score<=1 else 'pts'}" - points_question = f'
{points}
{self.question}' + points_question = f'
{points(self.score)}
{self.question}' question = HTMLMath(points_question) question.add_class('question') @@ -168,20 +175,17 @@ def widget(self): self.checkboxes = [Checkbox(value=option.selected, disabled=False, description='', indent=False) for option in self.displayed] labels = [option.render().widget() for option in self.displayed] - answers = VBox([HBox([checkbox, label]) - for (checkbox, label) in zip(self.checkboxes, labels)]) + options_box = HBox if self.horizontal_options else VBox + answers = options_box( + [HBox([checkbox, label]) + for (checkbox, label) in zip(self.checkboxes, labels)]) answers.add_class("answers") css_widget = CssContent(CSS).widget() - if not self.horizontal: - self._widget_instance = VBox([question, - answers, - css_widget]) - else: - self._widget_instance = HBox([question, - answers, - css_widget]) + layout_box = HBox if self.horizontal_layout else VBox + self._widget_instance = layout_box( + [question, answers, css_widget]) self._widget_instance.add_class("nbae-question") self.feedback_area = self._widget_instance self.feedback(None) @@ -293,8 +297,8 @@ def update(self): self.submit_button.description = "quiz over" current_score, max_score = self.score() self.submit_summary.value = ( - f"final score {current_score} / {max_score}" - f" -- after {self.current_attempts} attempts / {self.max_attempts}" + f"final score {current_score} / {points(max_score)} " + f" -- after {self.current_attempts} / {self.max_attempts} attempts" ) if all(self.answers): self.submit_summary.add_class("ok")