From 64db0ff87bf1f830f835088e2fa2221f83db77cb Mon Sep 17 00:00:00 2001 From: Chris Pyles Date: Mon, 19 Feb 2024 11:43:07 -0800 Subject: [PATCH] rewrote demo --- demo/attendance_closed_config.json | 2 +- demo/attendance_open_config.json | 2 +- demo/demo.ipynb | 487 +++++++---------------------- demo/nbforms_config.json | 2 +- demo/nbforms_config_google.json | 2 +- docs/changelog.md | 1 + docs/notebook_usage.md | 2 +- 7 files changed, 123 insertions(+), 375 deletions(-) diff --git a/demo/attendance_closed_config.json b/demo/attendance_closed_config.json index cf15c01..ac8518c 100644 --- a/demo/attendance_closed_config.json +++ b/demo/attendance_closed_config.json @@ -1,5 +1,5 @@ { - "server_url": "https://nbforms-demo-server.herokuapp.com/", + "server_url": "http://127.0.0.1:8000", "notebook": "attendance-closed", "attendance": true, "questions": [ diff --git a/demo/attendance_open_config.json b/demo/attendance_open_config.json index 0c988f9..2ee34b2 100644 --- a/demo/attendance_open_config.json +++ b/demo/attendance_open_config.json @@ -1,5 +1,5 @@ { - "server_url": "https://nbforms-demo-server.herokuapp.com/", + "server_url": "http://127.0.0.1:8000", "notebook": "attendance-open", "attendance": true, "questions": [ diff --git a/demo/demo.ipynb b/demo/demo.ipynb index a563497..0d853aa 100644 --- a/demo/demo.ipynb +++ b/demo/demo.ipynb @@ -6,7 +6,7 @@ "source": [ "# nbforms Demo Notebook\n", "\n", - "The nbforms package allows you to have interactive questions in Jupyter Notebooks that is designed to allow immediate usage of collected data. It requires you to have deployed an [`nbforms-server`](https://github.com/chrispyles/nbforms-server); the demo server is located at https://nbforms-demo-server.herokuapp.com.\n", + "The nbforms package allows you to have interactive questions in Jupyter Notebooks that is designed to allow immediate usage of collected data.\n", "\n", "### Setup\n", "\n", @@ -15,61 +15,9 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"server_url\": \"https://nbforms-demo-server.herokuapp.com/\",\n", - " \"notebook\": 1,\n", - " \"attendance\": true,\n", - " \"questions\": [\n", - " {\n", - " \"identifier\": \"q1\",\n", - " \"type\": \"multiplechoice\",\n", - " \"question\": \"Which of the following is a noble gas?\",\n", - " \"options\": [\n", - " \"Helium\",\n", - " \"Chlorine\",\n", - " \"Oxygen\",\n", - " \"Lithium\"\n", - " ]\n", - " }, {\n", - " \"identifier\": \"q2\",\n", - " \"type\": \"checkbox\",\n", - " \"data_type\": \"str\",\n", - " \"question\": \"Which of the following is a noble gas?\",\n", - " \"options\": [\n", - " \"Helium\",\n", - " \"Chlorine\",\n", - " \"Oxygen\",\n", - " \"Lithium\",\n", - " \"Neon\",\n", - " \"Zync\"\n", - " ]\n", - " }, {\n", - " \"identifier\": \"q3\",\n", - " \"type\": \"text\",\n", - " \"question\": \"Name a noble gas.\"\n", - " }, {\n", - " \"identifier\": \"q4\",\n", - " \"type\": \"paragraph\",\n", - " \"question\": \"List the noble gases.\",\n", - " \"placeholder\": \"Type here\"\n", - " }, {\n", - " \"identifier\": \"q5\",\n", - " \"type\": \"text\",\n", - " \"question\": \"What is 75% of 5?\"\n", - " }\n", - " ]\n", - "}\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "with open(\"./nbforms_config.json\") as f:\n", " print(f.read())" @@ -79,152 +27,93 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Usage\n", - "\n", - "To use nbforms, create a `Form` instance. This will load the config file and ask the use to authenticate with the server, generating an API token for this notebook session.\n", - "\n", - "**If a user has never authenticated before, the cell below will create their user, which they can then reuse on the server. There is no need to go to another page to create a user.** The rules for logins are:\n", - "* If there is no user with the username provided, a new user is created with the provided password. Then, a new API key is generated and returned to the `Form` instance.\n", - "* If there is a user with the username provided, the password provided is compared against the user's password. If this passes, a new API key is generated and returned to the `Form` instance.\n", - "* If the passwords do not match, then the cell will throw an `AssertionError`." + "nbforms requires a server to collect responses. The cells below clone the nbforms server repo, start a local development server that you can use for this notebook, and perform some setup tasks in the server instance's database." ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import nbforms\n", - "form = nbforms.Form()" + "%%bash --bg --out sh_out --err sh_err\n", + "if ! [ -d nbforms-server ]; then git clone https://github.com/chrispyles/nbforms-server; fi\n", + "cd nbforms-server\n", + "CREATE=\"false\"\n", + "if ! [ -d .venv ]; then python -m venv .venv; CREATE=\"true\"; fi\n", + "source .venv/bin/activate\n", + "if [ $CREATE = \"true\" ]; then pip install -qr requirements.txt; fi\n", + "flask --app nbforms_server run --port 8000" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "It is also possible to use a Google account to authenticate. To do this, specify an `auth` key in your `nbforms_config.json` file with the value set to `google`:" + "# Only run this cell once. It will show you the output from the bash script in the cell above, which\n", + "# is running in a background process.\n", + "\n", + "import asyncio\n", + "from IPython.display import Pretty\n", + "\n", + "async def reader(stream, d):\n", + " text = \"\"\n", + " while not stream.at_eof():\n", + " text += (await stream.readline()).decode()\n", + " d.update(Pretty(text))\n", + "\n", + "d1, d2 = display(display_id=\"stream-1\"), display(display_id=\"stream-2\")\n", + "asyncio.create_task(reader(sh_out, d1))\n", + "asyncio.create_task(reader(sh_err, d2));" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"server_url\": \"https://nbforms-demo-server.herokuapp.com/\",\n", - " \"notebook\": 1,\n", - " \"auth\": \"google\",\n", - " \"attendance\": true,\n", - " \"questions\": [\n", - " {\n", - " \"identifier\": \"q1\",\n", - " \"type\": \"multiplechoic\n" - ] - } - ], + "outputs": [], "source": [ - "with open(\"nbforms_config_google.json\") as f:\n", - " print(f.read()[:200])" + "%%bash\n", + "cd nbforms-server\n", + "source .venv/bin/activate\n", + "python -m nbforms_server attendance open attendance-open --create" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "When users sign in with Google, they will be asked to open a link to the nbforms server where they will authenticate with Google and then to copy their API key and input it here in the notebook. You can try this process below." + "### Usage\n", + "\n", + "To use nbforms, create a `Form` instance. This will load the config file and ask the use to authenticate with the server, generating an API token for this notebook session. Since the server has no users, a user will be created with whatever credentials you enter." ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "google_auth_form = nbforms.Form(\"nbforms_config_google.json\")" + "import nbforms\n", + "form = nbforms.Form()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "To ask a user to respond to a question, call `Form.ask` with the question _identifier_ as its argument. The widget generated will have a \"Submit\" button which will send a POST request to your nbforms server that will record the user's response. `Form.ask` can accept multiple arguments and will display a widget for each identifier you pass it. If you pass no arguments, it will display all the widgets.\n", + "To ask a user to respond to a question, call `Form.ask` with the question _identifier(s)_ as its argument(s). The widget generated will have a \"Submit\" button which will send a request to your nbforms server that will record the user's response. `Form.ask` can accept multiple arguments and will display a widget for each identifier you pass it. If you pass no arguments, it will display all the questions.\n", "\n", "nbforms allows multiple choice (with one or many selections), text, and paragraph responses. An example of each is given below." ] }, { "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3520955007e544859d14b4a2e4bbdb5e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(VBox(children=(VBox(children=(Label(value='Which of the following is a noble gas?'), RadioButto…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "form.ask(\"q1\")" - ] - }, - { - "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "7ee9107dceeb4136b3fea551d13d3776", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(VBox(children=(VBox(children=(Label(value='Which of the following is a noble gas?'), SelectMult…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "form.ask(\"q2\", \"q3\")" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "f0195237f9c74c05baeff9afd6c823fd", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(VBox(children=(VBox(children=(Label(value='Which of the following is a noble gas?'), RadioButto…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "form.ask()" ] @@ -240,225 +129,53 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
q1 q2 q3 q4 q5
Oxygen ('Chlorine', 'Lithium') dfwegf lkjsg sdf
Oxygen ('Helium',) something else nan asefkl;wkldfk;lakwfdfsdd
Helium nan nan nan nan
Helium ('Helium', 'Chlorine', 'Oxygen') nan nan nan
Helium ('Helium',) Helium nan 2.8
Helium ('Helium',) nan nan nan
Helium nan nan nan nan
" - ], - "text/plain": [ - "q1 | q2 | q3 | q4 | q5\n", - "Oxygen | ('Chlorine', 'Lithium') | dfwegf | lkjsg | sdf\n", - "Oxygen | ('Helium',) | something else | nan | asefkl;wkldfk;lakwfdfsdd\n", - "Helium | nan | nan | nan | nan\n", - "Helium | ('Helium', 'Chlorine', 'Oxygen') | nan | nan | nan\n", - "Helium | ('Helium',) | Helium | nan | 2.8\n", - "Helium | ('Helium',) | nan | nan | nan\n", - "Helium | nan | nan | nan | nan" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "form.to_table(\"q1\", \"q2\", \"q3\", \"q4\", \"q5\")" + "form.to_table()" ] }, { "cell_type": "code", - "execution_count": 18, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
userq1q4q5
009c4f878f46c3ecOxygenlkjsgsdf
1166e05ab4b3e44dOxygenNaNasefkl;wkldfk;lakwfdfsdd
23d0a8e500509333HeliumNaNNaN
3dd420c24d4529d1HeliumNaNNaN
47c3680af2207899HeliumNaN2.8
50d7e1c882f10830HeliumNaNNaN
68556d704ee858d1HeliumNaNNaN
\n", - "
" - ], - "text/plain": [ - " user q1 q4 q5\n", - "0 09c4f878f46c3ec Oxygen lkjsg sdf\n", - "1 166e05ab4b3e44d Oxygen NaN asefkl;wkldfk;lakwfdfsdd\n", - "2 3d0a8e500509333 Helium NaN NaN\n", - "3 dd420c24d4529d1 Helium NaN NaN\n", - "4 7c3680af2207899 Helium NaN 2.8\n", - "5 0d7e1c882f10830 Helium NaN NaN\n", - "6 8556d704ee858d1 Helium NaN NaN" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "form.to_df(\"q1\", \"q4\", \"q5\", user_hashes=True)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Important:** The server route that these methods use to retrieve the data requires no authentication. This means that anyone with the server URL will be able to download any of the data in the responses provided by users. Be very careful about what kind of data you ask users to input to the server; it should not be used to store things like [PII](https://en.wikipedia.org/wiki/Personal_data)." + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Taking Attendance\n", "\n", - "nbforms can be used to take attendance in classes using rake tasks on the Heroku app. This is done by including an `attendance` key in the config file set to `true`. This will then allow students to run `Form.take_attendance` which will log their attendance on the server.\n", - "\n", - "Attendance can be opened by running `rake attendance:open[NOTEBOOK_ID]` on the Heroku app, where `NOTEBOOK_ID` corresponds to the `notebook` parameter in your config file. There are two notebooks on the demo server, one which is always open and another that is always closed, on which you can take your attendance below." + "nbforms can be used to take attendance in classes, allowing you to open and close a notebook for attendance tracking. This is done by including an `attendance` key in the config file set to `true`. This will then allow students to run `Form.take_attendance` which will log their attendance on the server." ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "OPEN ATTENDANCE:\n", - "{\n", - " \"server_url\": \"https://nbforms-demo-server.herokuapp.com/\",\n", - " \"notebook\": \"attendance-open\",\n", - " \"attendance\": true,\n", - " \"questions\": [\n", - " {\n", - " \"identifier\": \"q1\",\n", - " \"type\": \"multiplechoice\",\n", - "\n", - "\n", - "\n", - "CLOSED ATTENDANCE:\n", - "{\n", - " \"server_url\": \"https://nbforms-demo-server.herokuapp.com/\",\n", - " \"notebook\": \"attendance-closed\",\n", - " \"attendance\": true,\n", - " \"questions\": [\n", - " {\n", - " \"identifier\": \"q1\",\n", - " \"type\": \"multiplechoice\"\n" - ] - } - ], + "outputs": [], "source": [ "with open(\"attendance_open_config.json\") as f:\n", " print(\"OPEN ATTENDANCE:\")\n", - " print(f.read()[:200])\n", + " print(f.read()[:100])\n", " \n", "print(\"\\n\")\n", "\n", "with open(\"attendance_closed_config.json\") as f:\n", " print(\"CLOSED ATTENDANCE:\")\n", - " print(f.read()[:200])" + " print(f.read()[:100])" ] }, { @@ -470,17 +187,9 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Your attendance has been recorded.\n" - ] - } - ], + "outputs": [], "source": [ "always_open = nbforms.Form(\"attendance_open_config.json\")\n", "always_open.take_attendance()" @@ -488,17 +197,9 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Your attendance has been recorded.\n" - ] - } - ], + "outputs": [], "source": [ "always_closed = nbforms.Form(\"attendance_closed_config.json\")\n", "always_closed.take_attendance()" @@ -511,6 +212,52 @@ "Attendance is always logged, so even if an attendance form is closed, a student will not know and this will be included in the attendance report." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using the server's CLI, you can run reports for users, notebooks, responses, and attendance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cd nbforms-server\n", + "source .venv/bin/activate\n", + "echo \"Responses report for notebook 1:\" && echo\n", + "python -m nbforms_server reports responses 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cd nbforms-server\n", + "source .venv/bin/activate\n", + "echo \"Attendance report for notebook attendance-open:\" && echo\n", + "python -m nbforms_server reports attendance attendance-open" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cd nbforms-server\n", + "source .venv/bin/activate\n", + "echo \"Attendance report for notebook attendance-closed:\" && echo\n", + "python -m nbforms_server reports attendance attendance-closed" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -525,7 +272,7 @@ "lastKernelId": null }, "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -539,7 +286,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.11.7" }, "varInspector": { "cols": { @@ -572,5 +319,5 @@ } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/demo/nbforms_config.json b/demo/nbforms_config.json index ec60490..913b2e1 100644 --- a/demo/nbforms_config.json +++ b/demo/nbforms_config.json @@ -1,5 +1,5 @@ { - "server_url": "https://nbforms-demo-server.herokuapp.com/", + "server_url": "http://127.0.0.1:8000", "notebook": 1, "attendance": true, "questions": [ diff --git a/demo/nbforms_config_google.json b/demo/nbforms_config_google.json index 19071db..ab9b119 100644 --- a/demo/nbforms_config_google.json +++ b/demo/nbforms_config_google.json @@ -1,5 +1,5 @@ { - "server_url": "https://nbforms-demo-server.herokuapp.com/", + "server_url": "http://127.0.0.1:5000", "notebook": 1, "auth": "google", "attendance": true, diff --git a/docs/changelog.md b/docs/changelog.md index 4a24297..dbfdd8e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -8,6 +8,7 @@ * Rewrote Python client library * Removed support for Google OAuth to authenticate with a server instance * Added ability to seed users from a CSV file for a server instance +* Allowed users to enter server URL during authentication **v0.5.1:** diff --git a/docs/notebook_usage.md b/docs/notebook_usage.md index 39869c5..17d0d4e 100644 --- a/docs/notebook_usage.md +++ b/docs/notebook_usage.md @@ -71,7 +71,7 @@ This will output the widget and a "Submit" button that, when clicked, will send ### Retrieving Data -nbforms allows you to get your data from the server and collect it into either a datascience `Table` or a pandas `DataFrame`. To retrieve the responses from the server, use `Form.to_table` or `Form.to_df`; the optional `user_hashes` argument (default `False`) indicates whether or not to include a column with a randomly generated string as a pseudo-username. Note that these pseudonymized usernames are ephemeral and will change each time you run `Form.to_table` and `Form.to_df`. +nbforms allows you to get your data from the server and collect it into either a datascience `Table` or a pandas `DataFrame`. To retrieve the responses from the server, use `Form.to_table` or `Form.to_df`; the optional `user_hashes` argument (default `False`) indicates whether or not to include a column with a pseudonymized username. ```python # datascience Table