diff --git a/notebooks/UsingMCQquestions.ipynb b/notebooks/UsingMCQquestions.ipynb new file mode 100644 index 0000000..4218670 --- /dev/null +++ b/notebooks/UsingMCQquestions.ipynb @@ -0,0 +1,196 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f77d360f", + "metadata": {}, + "source": [ + "# Using Multiple Choice Questions in Notebooks\n", + "\n", + "(a guide for instructors)\n", + "\n", + "In interactive notebooks it can be useful to ask students to respond to a multiple choice questions. This notebook demonstrates how this can be done, either using local code or by embedding an external site. " + ] + }, + { + "cell_type": "markdown", + "id": "2627c40a", + "metadata": {}, + "source": [ + "### Local Code Solution\n", + "The helper file mcq.py shows how this can be achieved locally. For example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1083919f", + "metadata": {}, + "outputs": [], + "source": [ + "# import the questions from the helper file\n", + "from mcq import mcq_1, mcq_2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "242b1018", + "metadata": {}, + "outputs": [], + "source": [ + "# run the cell to view the mcq\n", + "mcq_1()" + ] + }, + { + "cell_type": "markdown", + "id": "bf13498c", + "metadata": {}, + "source": [ + "Setting up the mcq is just by setting up a small function which calls the mcq class. You just need to define the question as a string, the options and whether the answer is correct using a list of dictionary objects. You can also provide feedback if the answer is correct or incorrect (or both) using the feedbackCorrent and feedbackIncorrect keys. By default only ticked answers will be marked unless you pass a third False flag in the set up(to the CHECK_SUBMITTED_ONLY flag). Here is the function that creates the quiz above:" + ] + }, + { + "cell_type": "markdown", + "id": "262616b4", + "metadata": {}, + "source": [ + ">```Python\n", + ">def mcq_1():\n", + "> question = \"

Select the correct answers about the Python Language (more than one answer may be correct)

\"\n", + ">\n", + "> options_dict = [\n", + "> {\n", + "> \"text\": \"Python is the fastest coding language\",\n", + "> \"correct\": False,\n", + "> \"feedbackIncorrect\": \"Python is not the fastest — precompiled languages like C or Rust are generally faster.\",\n", + "> },\n", + "> {\n", + "> \"text\": \"Python is a great language to start with\",\n", + "> \"correct\": True,\n", + "> \"feedbackCorrect\": \"Yes! Python's simple syntax makes it a great first language.\",\n", + "> },\n", + "> {\n", + "> \"text\": \"Python was developed in the 1980s\",\n", + "> \"correct\": True,\n", + "> \"feedbackCorrect\": \"Correct — Python was conceived in the late 1980s by Guido van Rossum.\",\n", + "> },\n", + "> ]\n", + ">\n", + "> quiz = MCQ(question, options_dict)\n", + "> return quiz.quiz()" + ] + }, + { + "cell_type": "markdown", + "id": "93e67a9e", + "metadata": {}, + "source": [ + "If you want to use code as part of the question format it carefully using a \"\"\" syntax and <pre> and <code> tags\n", + ">```Python\n", + "> question = \"\"\"\n", + ">

Analyse the following code and select the two correct answers\n", + ">

\n",
+    ">\n",
+    ">for i in range(10):\n",
+    ">    if i % 2 == 1:\n",
+    ">        print(f\"{i} is odd\")\n",
+    ">    else:\n",
+    ">        print(f\"{i} is even\")\n",
+    ">

\n", + ">\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a0675f8e", + "metadata": {}, + "outputs": [], + "source": [ + "mcq_2()" + ] + }, + { + "cell_type": "markdown", + "id": "96c5f219", + "metadata": {}, + "source": [ + "It may not be obvious to students that they have all the correct answers unless you tell them at the start how many are correct." + ] + }, + { + "cell_type": "markdown", + "id": "ffa1e196", + "metadata": {}, + "source": [ + "### Alternative Embed Approach\n", + "\n", + "If you dont want to mess around with local helper functions then you can also take an embed approach. As with the Parsons problems examples \n", + "\n", + "Go to the example problem on CodePen https://codepen.io/StuartLW/pen/WbbNvXe or https://codepen.io/StuartLW/pen/yyyLOpe (you may need to create a free account) and in the bottom right click Fork. This should create a new copy of the problem for you to edit. \n", + "- In the HTML box you can edit the question\n", + "- In the JS box edit the const quizData to reflect the answer options you want.\n", + "- In the JS box edit the const CHECK_SELECTED_ONLY to false if you want to change the feedback behaviour\n", + "\n", + "Thats it - your problem should now render OK in the results section\n", + "- Save your codepen\n", + "- from the bottom right click Embed - a new dialogue opens\n", + "- in the top pane make sure only result is selected\n", + "- click iframe in the lower right tab and copy the src from the displayed code to use in your notebook\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dfc508fb", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import IFrame\n", + "\n", + "IFrame(\n", + " src=\"https://codepen.io/StuartLW/embed/WbbNvXe?default-tab=result\",\n", + " width=1500,\n", + " height=400,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c72c10c6", + "metadata": {}, + "outputs": [], + "source": [ + "IFrame(\n", + " src=\"https://codepen.io/StuartLW/embed/yyyLOpe?default-tab=result\",\n", + " width=1500,\n", + " height=600,\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "teaching", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/UsingParsonsProblems.ipynb b/notebooks/UsingParsonsProblems.ipynb new file mode 100644 index 0000000..ff5da43 --- /dev/null +++ b/notebooks/UsingParsonsProblems.ipynb @@ -0,0 +1,114 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8fffd7ef", + "metadata": {}, + "source": [ + "# Parsons Problems in Jupyter Notebooks\n", + "\n", + "(a guide for instructors)\n", + "\n", + "Parsons problems are useful problems to check if students undestand various aspects of code from pseudo code and process flow to indenting in python. Thanks go to the Edinburgh python in chemistry team for pointing these problems out at the first UK Python in Chemistry conference. Deploying these used to be fairly easy thanks to a website where you could construct your problems and then embed them in your notebook, however this site now seems to have become defunct. The original site and most deployed Parsons problems seem to be based on [The JS parsons code](https://github.com/js-parsons/js-parsons) available on github. Direct implementation in a Jupyter notebook however seems very hard due to the number of libaries that are required to make the javascript load correctly in a markdown cell. The best solution therefore is to find an alternative host for embedding the problem and then bring the code in via an IFrame. The system works on both Codepen and JSFiddle however the embedding from JSFiddle does not seem to render correctly in my hands. There may be other possibilites and others may be able to solve the JSFiddle issue" + ] + }, + { + "cell_type": "markdown", + "id": "397f7eb6", + "metadata": {}, + "source": [ + "## Embedding a Parsons problem\n", + "Once your Parsons problem is constructed (more below) you just need to embed it as follows. Executing the code renders the answer. Students can click Get feedback for it to mark the answer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59da754b", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import IFrame\n", + "\n", + "IFrame(\n", + " src=\"https://codepen.io/StuartLW/embed/yyyBZjJ?default-tab=result\",\n", + " width=1500,\n", + " height=600,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "7b3ec7e8", + "metadata": {}, + "source": [ + "## Creating your Parsons problem\n", + "Go to the example problem on CodePen https://codepen.io/StuartLW/pen/yyyBZjJ (you may need to create a free account) and in the bottom right click Fork. This should create a new copy of the problem for you to edit. \n", + "- In the HTML box you can edit the instructions\n", + "- In the JS box edit the const initial to be your target code including any indentation\n", + "- you can add a distracting entry by adding the dummy line at the end with #distractor after the command\n", + "\n", + "Thats it - your problem should now render OK in the results section\n", + "- Save your codepen\n", + "- from the bottom right click Embed - a new dialogue opens\n", + "- in the top pane make sure only result is selected\n", + "- click iframe in the lower right tab and copy the src from the displayed code to use in your notebook\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "437c26b2", + "metadata": {}, + "outputs": [], + "source": [ + "IFrame(\n", + " src=\"https://codepen.io/StuartLW/embed/gbbYZvo?default-tab=result\",\n", + " width=1500,\n", + " height=650,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a98fb8e6", + "metadata": {}, + "source": [ + "## Advanced information\n", + "\n", + "In addition to the HTML, CSS and JS that is shown on the CodePen page there are a number of imports from the github site that are used directly in making this work. These are listed in settings. There are 2 external CSS style sheets and 7 JS scripts that are loaded. The order of loading the js scripts matters so stick with what is logged here" + ] + }, + { + "cell_type": "markdown", + "id": "6e284d4a", + "metadata": {}, + "source": [ + "## TODO\n", + "Looking at the JS-Parsons github site there are possible some other interesting features within the capabilities of this code, but sadly it is very poorly documented with examples that miss key information. Others may be able to add examples to this notebook that exploit different functionality to demonstrate different coding concepts." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "teaching", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/mcq.py b/notebooks/mcq.py new file mode 100644 index 0000000..3ebecce --- /dev/null +++ b/notebooks/mcq.py @@ -0,0 +1,259 @@ +from IPython.display import HTML +import json + + +class MCQ: + """A class to hold the definitions of a multiple choice quiz.""" + + div_counter = 0 # class based counter for divs to avoid id collision on the images to be moved around if multiple quizzes + + def __init__( + self, question: str, option_dict: dict, check_selected_only=True + ) -> str: + """creates a html/ javascript multiple choice quiz + + Args: + question (str): A string based question that the multiple choice options will refer to + option_dict (dict): A list of dictionary items showing the options, if they are correct and any optional feedback + check_selected_only (bool, optional): A boolean flag as to whether only ticked items should be marked or all items. Defaults to True. + + Returns: + str: html/javascript combination for rendering the quiz + """ + self.question = json.dumps(question) + self.options = json.dumps(option_dict) + self.CSO = json.dumps(check_selected_only) + MCQ.div_counter += 1 + + def quiz(self): + quiz = f"quiz-container-{MCQ.div_counter}" + html = f""" + + +
+ + + """ + return HTML(html) + + +def mcq_1(): + """Creates a simple mcq question. The question text is defined and then the options_dict is defined as a list of dictionary + objects. For each option provide a text for the option and a boolean value to the correct field indicating if the answer is correct + There are optional feedbackCorrect and feedbackIncorrect fields that can be defined for feedback displayed on submission. + When we create the quiz we can also pass an optional third boolean argument as to whether to mark only options that are ticked as true (default) + or show feedback on all options + + Returns: + HTML:HTML rendering of the quiz + """ + + question = "

Select the correct answers about the Python Language (more than one answer may be correct)

" + + options_dict = [ + { + "text": "Python is the fastest coding language", + "correct": False, + "feedbackIncorrect": "Python is not the fastest — precompiled languages like C or Rust are generally faster.", + }, + { + "text": "Python is a great language to start with", + "correct": True, + "feedbackCorrect": "Yes! Python's simple syntax makes it a great first language.", + }, + { + "text": "Python was developed in the 1980s", + "correct": True, + "feedbackCorrect": "Correct — Python was conceived in the late 1980s by Guido van Rossum.", + }, + ] + + quiz = MCQ(question, options_dict) + return quiz.quiz() + + +def mcq_2(): + """Creates a simple mcq question. The question text is defined and then the options_dict is defined as a list of dictionary + objects. In this case we have defined some code for inspection. use the syntax below so your code displays correctly with indentation. + For each option provide a text for the option and a boolean value to the correct field indicating if the answer is correct + There are optional feedbackCorrect and feedbackIncorrect fields that can be defined for feedback displayed on submission. + When we create the quiz we can also pass an optional third boolean argument as to whether to mark only options that are ticked as true (default) + or show feedback on all options + + Returns: + HTML:HTML rendering of the quiz + """ + + question = """ +

Analyse the following code and select the two correct answers +

+
+for i in range(10):
+    if i % 2 == 1:
+        print(f"{i} is odd")
+    else:
+        print(f"{i} is even")
+

+""" + + options_dict = [ + { + "text": "in the for loop i takes on integers between 1 and 10", + "correct": False, + "feedbackIncorrect": "Remember the default start for the range object is 0 and the stop position is NOT included", + }, + { + "text": "in the for loop i takes on integers between 0 and 9", + "correct": True, + "feedbackCorrect": "Yes! The range objects default start is 0 and stop is not included", + }, + { + "text": "The % operator shows the remainder after division", + "correct": True, + "feedbackCorrect": "Correct in this code we look at the remainder after division by 2 - an odd number will always have remainder 1", + }, + ] + + quiz = MCQ(question, options_dict, True) + return quiz.quiz()