{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "import pathlib\n", "import sys\n", "\n", "import numpy as np\n", "import matplotlib\n", "import matplotlib.pyplot as plt\n", "\n", "from IPython.display import FileLink, FileLinks\n", "\n", "# we don't want out plots to show while building them\n", "matplotlib.use('Agg')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So that the library is readily imported. This is unnecessary (and probably not recommended) if you have a local installation of `py2gift`. It is meant for running *online* through a cloud service such as *mybinder*." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sys.path.append('..')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import py2gift.question\n", "import py2gift.input_file\n", "import py2gift.notebook\n", "import py2gift.core\n", "import py2gift.tex\n", "import py2gift.file" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Minimal example\n", "\n", "> A sample quiz." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# General settings" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A settings manager object (with default options)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "settings = py2gift.input_file.Settings()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "How many versions of a question are to be generated" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "n_instances = 2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Question 1 (numerical)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We need to specify\n", "* the name of the Python class that implements this question\n", "* the category to which the question will belong inside the *Moodle*'s question bank\n", "* the *base* name for the question: several versions of the same question will be created, and they will be named \"<*question base name*> <*number of version in Roman numbers*>\". For instance, if the *question base name* is \"Foo\", we will get questions \"Foo I\", \"Foo II\",..." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Caveat**: the variables below are used by `%%statement` and `%%feedback` *magics* to know what to modify (they determine the *context*). So, when moving back and forth between questions (up and down in the jupyter notebook), one should at least re-run the cell below before modifying anything in the corresponding question." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class_name = 'Question1'\n", "category_name = 'Cat 1'\n", "question_base_name='Toy question'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The category is *registered* in the settings object" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "category_name = settings.add_category(category_name=category_name)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The question is registered in the newly-created category" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "settings.add_or_update_class(\n", " category_name=category_name, class_name=class_name, question_base_name=question_base_name,\n", " n_instances=n_instances)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The statement of the question is entered through an *ipython* magic since it allows to capture freely-typed text. In principle, the text can be anything but if you want different versions of the same question, it should contain some *variables* that will be filled by Python code. These variables are prefixed by `!`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'statement recorded'" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%statement settings --cls {class_name} --category {json.dumps(category_name)}\n", "What is the product of !factors?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'feedback recorded'" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%feedback settings --cls {class_name} --category {json.dumps(category_name)}\n", "Since blah blah" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The class implementing the question is defined. It should inherit from one of the classes in module `py2gift.question`:\n", "* `py2gift.question.MultipleChoiceQuestionGenerator`: for multiple-choice questions\n", "* `py2gift.question.NumericalQuestionGenerator`: for numerical-answer questions\n", "\n", "The only mandatory method the new class must define is `setup`. Its purpose is to fill in the *blanks* in both the `statement` and `feedback` of the question by calling, respectively, `self.statement.fill` and `self.feedback.fill`. Also, it should provide:\n", "* the solution and error tolerance for `py2gift.question.NumericalQuestionGenerator`: one should set `self.solution` to some **number** and `self.error` to either a **number or a string indicating a percentage**\n", "* the right answer along with the wrong ones for `py2gift.question.MultipleChoiceQuestionGenerator`:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In order to generate several instances (versions) of the same question, random numbers (or pictures!!) must be used somewhere (otherwise all the instances of the question will be identical). For that purpose, when one inherits from a class in `py2gift.question`, a pseudo-random numbers generator, `self.prng`, is provided. The method `setup`, in the class below, will be called once for each new *instance* of the question." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Question1(py2gift.question.NumericalQuestionGenerator):\n", " \n", " def setup(self):\n", " \n", " factors = self.prng.rand(4) * 10\n", " \n", " # above `numpy` array needs to be turned into a `str` for the statement; one of the convenience functions\n", " # in `py2gift.tex`can be used\n", " str_factors = py2gift.tex.enumerate_math(factors)\n", " \n", " # the statement is \"filled\" in\n", " self.statement.fill(factors=str_factors)\n", " \n", " self.solution = np.prod(factors)\n", " self.error = '10%'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can easily preview *the first instance* of the question" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "\n", "Statement\n", "\n", "What is the product of $\\Large 3.75$, $\\Large 9.51$, $\\Large 7.32$ and $\\Large 5.99$?\n", "\n", "\n", "Feedback\n", "\n", "\n", "\n", "Since blah blah\n", "\n", "Solution\n", "\n", " 1560.3966226689554 (error: 156.03966226689553)\n" ], "text/plain": [ "" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "py2gift.util.render_latex(py2gift.core.generator_to_markdown(\n", " settings.to_dict(), category_name, getattr(settings.fake_module, class_name)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$\\LaTeX$ formulas are enlarged (`\\Large` is prepended) for better visualization inside the notebook, but they are kept as they were when written in the generated GIFT file. Also notice that `n_instances` of this question will actually be generated, though only the first one was shown here." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Question 2 (multiple-choice)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class_name = 'Question2'\n", "category_name = 'Cat 2'\n", "question_base_name='Another question'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "category_name = settings.add_category(category_name=category_name)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "settings.add_or_update_class(\n", " category_name=category_name, class_name=class_name, question_base_name=question_base_name,\n", " n_instances=n_instances)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'statement recorded'" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%statement settings --cls {class_name} --category {json.dumps(category_name)}\n", "Consider the heatmap\n", "!heatmap\n", "Now what?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'feedback recorded'" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%feedback settings --cls {class_name} --category {json.dumps(category_name)}\n", "Just ignore this stuff..." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If copy and pasting, you must remember to match the name of this class with whatever you specified above in `class_name`. Since this is a multiple-choice question, we should set\n", "* `self.right_answer` to a **string** with the right answer\n", "* `self.wrong_answers` to a **list of strings** with the wrong ones" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%mkdir -p 'images'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "\n", "Statement\n", "\n", "Consider the heatmap\n", "\n", "![](heatmap_094168d2-ba3d-11ea-bf69-65721d4e6420.svg)\n", "\n", "Now what?\n", "\n", "\n", "Feedback\n", "\n", "\n", "\n", "Just ignore this stuff...\n", "\n", "Choices\n", "\n", "* **This doesn't make any sense**\n", "* **42**\n", "* **The information action ratio**\n" ], "text/plain": [ "" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class Question2(py2gift.question.MultipleChoiceQuestionGenerator):\n", " \n", " def setup(self):\n", " \n", " # a random matrix...\n", " matrix = self.prng.rand(2,2)\n", " \n", " # ...is plotted as a heat map\n", " fig, ax = plt.subplots()\n", " im = ax.imshow(matrix)\n", " \n", " # image must be saved as an svg...\n", "# heatmap = pathlib.Path('images') / 'heatmap.svg'\n", " heatmap = 'heatmap.svg'\n", " \n", " # however, since different \"versions\" of this question (for different random matrices) are going to\n", " # be created, we must make sure to choose a different name for each one; one way of achieving this is\n", " # by using `py2gift.file.unique_name`\n", " heatmap = py2gift.file.unique_name(heatmap)\n", " \n", " fig.savefig(heatmap)\n", " \n", " self.statement.fill(heatmap=heatmap)\n", " \n", " # this must be a string...\n", " self.right_answer = \"This doesn't make any sense\"\n", " \n", " # ...and this a *list* of strings\n", " self.wrong_answers = ['42', 'The information action ratio']\n", "\n", "# for previewing the question\n", "py2gift.util.render_latex(py2gift.core.generator_to_markdown(\n", " settings.to_dict(), category_name, getattr(settings.fake_module, class_name)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Generating the GIFT file" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A minimal parameters file for *gift-wrapper*:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "parameters = {\n", " 'latex': {'auxiliary file': '__latex_check.tex'}\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "903bb724207b4a6ab83f4d20f1a697f7", "version_major": 2, "version_minor": 0 }, "text/plain": [ "HBox(children=(FloatProgress(value=0.0, description='category', max=2.0, style=ProgressStyle(description_width…" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "2badfdf7d63d4eae984e3fe682033936", "version_major": 2, "version_minor": 0 }, "text/plain": [ "HBox(children=(FloatProgress(value=0.0, description='question', max=2.0, style=ProgressStyle(description_width…" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "2e34df3e6bb348c9ba7a3dd0b790572b", "version_major": 2, "version_minor": 0 }, "text/plain": [ "HBox(children=(FloatProgress(value=0.0, description='question', max=2.0, style=ProgressStyle(description_width…" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "file \"quiz.gift.txt\" created\n" ] } ], "source": [ "# %%script false --no-raise-error\n", "local_run = True\n", "embed_images = True\n", "py2gift.core.build(\n", " settings.to_dict(), local_run=local_run, questions_module=settings.fake_module, parameters_file=parameters,\n", " no_checks=True, embed_images=embed_images)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Retrieve the created file from the link below (not present in the docs)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/html": [ "quiz.gift.txt
" ], "text/plain": [ "/home/manu/py2gift/examples/quiz.gift.txt" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from IPython.display import FileLink, FileLinks\n", "FileLink('quiz.gift.txt')" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 4 }