diff --git a/docs/source/_static/assets/images/instructor_guide/assignment_settings.png b/docs/source/_static/assets/images/instructor_guide/assignment_settings.png index 1fdb8c1e9..4c70eef7f 100644 Binary files a/docs/source/_static/assets/images/instructor_guide/assignment_settings.png and b/docs/source/_static/assets/images/instructor_guide/assignment_settings.png differ diff --git a/docs/source/instructor_guide/working_with_assignments.md b/docs/source/instructor_guide/working_with_assignments.md index f530b468c..6a6c46993 100644 --- a/docs/source/instructor_guide/working_with_assignments.md +++ b/docs/source/instructor_guide/working_with_assignments.md @@ -107,10 +107,37 @@ In the overview window of the assignment, you will find many ways to monitor, gr ![Assignment Overview](../_static/assets/images/instructor_guide/assignment_overview.png) +## Assignment Lifecycle + +![Assingment Status](../_static/assets/images/instructor_guide/assignment_lifecycle.png) + +An assignment can have 3 states that can be switched between and represent the lifecycle of the assignment. + +- Edit + : When first created, the assignment is in "Edit mode", where the assignment files can be added and edited. + In this stage, the assignment is not visible to students. However, when an instructor opens the student view ("Assignments" card in launcher), it will be displayed to them. +- Released + : The assignment is released to students and the students can work on it. + The released files are identical to the files in the release directory at the time of the release. + It is possible to undo the release and publish a new release. However, some students may have already pulled the old release. + In this case the students might have to reset their files and might loose progress, which has to be communicated. + In general, a re-release should be avoided. + + :::{warning} + Revoking a released assignment may lead to diverging states of student files and submissions that fail auto-grading. + ::: +- Completed + : The assignment is over and cannot be worked on anymore and new submissions will be rejected, but it is still visible. + This state can be revoked without any consideration and will return to a released state. + ## Assignment Settings ![Assignment Settings](../_static/assets/images/instructor_guide/assignment_settings.png) +### Groups + +You can assign assignments to a group you specify. This allows you to cluster assignments based on different criteria, e.g. the chapters of your lecture. Please note that each assignment can be assigned to only one group. + ### Deadlines As an instructor, you can set deadlines for assignments. @@ -128,6 +155,14 @@ You can set a limit on the number of times students can submit an assignment. **Submission Limits**: You can define a maximum number of submissions for each assignment (e.g., students can submit up to 3 times). Once a student reaches this limit, they will no longer be able to make additional submissions unless you increase the limit for all students. +### Cell timeout + +This field allows you to dynamically set the timeout of a cell per assignment in seconds. This means that the running time of each cell in the notebook(s) of the assignment will be limited by the value you specify. Please note that setting timeout for each cell separately is not possible. + +### Whitelist File Patterns + +By using `glob patterns` you can define which additional files can be submitted by the students. This allows you to have a better control over students' submissions and avoid getting unnecessary files from their side. + ### Late Submissions You can allow students to submit assignments after the deadline, with applied penalties on the total score. @@ -156,28 +191,6 @@ It specifies the action taken when a user submits an assignment. : The assignment is auto-graded and feedback is generated as soon as the student submits their assignment. This requires that no manually graded cells are part of the assignment. -## Assignment Lifecycle - -![Assingment Status](../_static/assets/images/instructor_guide/assignment_lifecycle.png) - -An assignment can have 3 states that can be switched between and represent the lifecycle of the assignment. - -- Edit - : When first created, the assignment is in "Edit mode", where the assignment files can be added and edited. - In this stage, the assignment is not visible to students. However, when an instructor opens the student view ("Assignments" card in launcher), it will be displayed to them. -- Released - : The assignment is released to students and the students can work on it. - The released files are identical to the files in the release directory at the time of the release. - It is possible to undo the release and publish a new release. However, some students may have already pulled the old release. - In this case the students might have to reset their files and might loose progress, which has to be communicated. - In general, a re-release should be avoided. - - :::{warning} - Revoking a released assignment may lead to diverging states of student files and submissions that fail auto-grading. - ::: -- Completed - : The assignment is over and cannot be worked on anymore and new submissions will be rejected, but it is still visible. - This state can be revoked without any consideration and will return to a released state. ## Grading Assignments @@ -195,7 +208,6 @@ If manual grading is not needed or not wanted, it can be skipped. The last step is feedback generation, at which point students will see their results. - ## Manual Submissions You have the ability to manually add submissions for students, even after the deadline has passed. @@ -211,7 +223,9 @@ Following video illustrates the procedure: ![Manual Submission](../_static/assets/gifs/instructor_guide/manual_submission.gif) ### How To Grade Manual Answer Cells? - - Once a student submits their work, it will appear in the "Submissions" list. To manually grade a submission, it must first be automatically graded (this sets the necessary metadata for successful grading). If you selected "Automatic Grading" when creating the assignment, this will be done automatically and you may immediately proceed with manual grading. If you chose "No Automatic Grading," you must first select the submission and click "AUTOGRADE." Afterward, you will be able to manually grade the submission. + - Once a student submits their work, it will appear in the "Submissions" list. + If you selected "Automatic Grading" when creating the assignment, the submission has to be automatically graded and afterwards you may + immediately proceed with manual grading. If you chose "No Automatic Grading", you can just pull the submission right away and manually grade it. - Click on submission in the Submission List and pull it. This will reveal files the student has submitted. - Click on notebook that you want to manually grade and when the notebook opens up enable "Grading Mode". - You can now assign points, leave comments or even give extra credits for the solution. diff --git a/grader_service/autograding/local_grader.py b/grader_service/autograding/local_grader.py index 974e0c1a6..c16edd082 100644 --- a/grader_service/autograding/local_grader.py +++ b/grader_service/autograding/local_grader.py @@ -7,7 +7,6 @@ import fnmatch import json import os -import shlex import shutil import subprocess from datetime import datetime @@ -258,10 +257,7 @@ def _get_whitelisted_files(self) -> List[str]: if any(fnmatch.fnmatch(file_path, pattern) for pattern in file_patterns): files_to_commit.append(file_path) - # escape filenames to handle special characters and whitespaces - escaped_files = [shlex.quote(f) for f in files_to_commit] - - return escaped_files + return files_to_commit def _set_properties(self) -> None: """ diff --git a/grader_service/tests/autograding/test_local_grader.py b/grader_service/tests/autograding/test_local_grader.py index de0b4acf9..eb08bbbfc 100644 --- a/grader_service/tests/autograding/test_local_grader.py +++ b/grader_service/tests/autograding/test_local_grader.py @@ -148,7 +148,18 @@ def test_file_matching_with_patterns(mock_git, tmp_path, submission_123): # Create test files in output directory os.makedirs(executor.output_path, exist_ok=True) test_dirs_and_files = [ - (executor.output_path, ["Ex1.ipynb", "output.txt", "weird name.py", "config"]), + ( + executor.output_path, + [ + "Ex1.ipynb", + "Ex1.2.ipynb", + "output.txt", + "weird name.py", + "fi_le.ipynb", + "do:t.ipynb", + "config", + ], + ), (executor.output_path + "/.git", ["config", "description", "HEAD"]), (executor.output_path + "/bar", ["Ex2.ipynb", "data.gz", "config"]), ] @@ -160,7 +171,15 @@ def test_file_matching_with_patterns(mock_git, tmp_path, submission_123): # Get files to commit matching the whitelist patterns files_to_commit = executor._get_whitelisted_files() - expected_files = {"Ex1.ipynb", "'weird name.py'", "bar/Ex2.ipynb", "bar/config"} + expected_files = { + "Ex1.ipynb", + "weird name.py", + "bar/Ex2.ipynb", + "bar/config", + "fi_le.ipynb", + "do:t.ipynb", + "Ex1.2.ipynb", + } assert set(files_to_commit) == expected_files diff --git a/grader_service/tests/convert/converters/__init__.py b/grader_service/tests/convert/converters/__init__.py index 0a9f65566..c099a4431 100644 --- a/grader_service/tests/convert/converters/__init__.py +++ b/grader_service/tests/convert/converters/__init__.py @@ -20,7 +20,7 @@ def _create_input_output_dirs(p: Path, input_notebooks=None): if input_notebooks: for n in input_notebooks: shutil.copyfile(tests_dir / f"preprocessors/files/{n}", input_dir / n) - + assert n in [f.name for f in input_dir.iterdir()] return input_dir, output_dir diff --git a/grader_service/tests/convert/converters/test_autograde.py b/grader_service/tests/convert/converters/test_autograde.py index 293a3f50c..0215b7849 100644 --- a/grader_service/tests/convert/converters/test_autograde.py +++ b/grader_service/tests/convert/converters/test_autograde.py @@ -2,6 +2,7 @@ import shutil from unittest.mock import patch +import pytest from nbclient.client import NotebookClient from grader_service.api.models.assignment_settings import AssignmentSettings @@ -12,12 +13,13 @@ ) -def test_autograde(tmp_path): - input_dir, output_dir = _create_input_output_dirs(tmp_path, ["simple.ipynb"]) +@pytest.mark.parametrize("notebook_name", ["simple.ipynb", "with space.ipynb"]) +def test_autograde(tmp_path, notebook_name): + input_dir, output_dir = _create_input_output_dirs(tmp_path, [notebook_name]) _generate_test_submission(input_dir, output_dir) - assert (output_dir / "simple.ipynb").exists() + assert (output_dir / notebook_name).exists() assert (output_dir / "gradebook.json").exists() output_dir2 = tmp_path / "output_dir2" @@ -37,7 +39,7 @@ def test_autograde(tmp_path): config=None, ).start() - assert (output_dir2 / "simple.ipynb").exists() + assert (output_dir2 / notebook_name).exists() assert (output_dir2 / "gradebook.json").exists() diff --git a/grader_service/tests/convert/preprocessors/files/with space.ipynb b/grader_service/tests/convert/preprocessors/files/with space.ipynb new file mode 100644 index 000000000..1bb8f2d22 --- /dev/null +++ b/grader_service/tests/convert/preprocessors/files/with space.ipynb @@ -0,0 +1,211 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "cell-26053a7da067ded3", + "locked": true, + "schema_version": 3, + "solution": false, + "task": false + }, + "tags": [] + }, + "source": [ + "### Aufgabe 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import datetime\n", + "\n", + "print(datetime.datetime.now())" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "cell-28df1799f8f8b769", + "locked": false, + "schema_version": 3, + "solution": true, + "task": false + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def reverse(s):\n", + " ###BEGIN SOLUTION\n", + " return s[::-1]\n", + " ###END SOLUTION" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "cell-c06c761f0b7b0f59", + "locked": true, + "schema_version": 3, + "solution": false, + "task": false + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'tseT'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "reverse(\"Test\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "cell-da8c82e850a1922b", + "locked": true, + "points": 1, + "schema_version": 3, + "solution": false, + "task": false + }, + "tags": [] + }, + "outputs": [], + "source": [ + "assert reverse(\"Test\") == \"tseT\"" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "cell-81540a070d18c412", + "locked": true, + "points": 1, + "schema_version": 3, + "solution": false, + "task": false + }, + "scrolled": true, + "tags": [] + }, + "outputs": [], + "source": [ + "assert reverse(\"lol\") == \"lol\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "cell-58d7f9f371feee54", + "locked": true, + "points": 2, + "schema_version": 3, + "solution": false, + "task": true + }, + "tags": [] + }, + "source": [ + "## Aufgabe 2\n", + "What are \"fake\"-threads?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Answer:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "cell-1b9d18df2b17e57f", + "locked": true, + "schema_version": 3, + "solution": false, + "task": false + }, + "tags": [] + }, + "source": [ + "## Aufgabe 3\n", + "Does Java use \"fake\"-threads? Explain why or why not?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "cell-9ea0264ada6c25bd", + "locked": false, + "points": 2, + "schema_version": 3, + "solution": true, + "task": false + }, + "tags": [] + }, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.9.2 64-bit ('homebrew')", + "language": "python", + "name": "python392jvsc74a57bd0b0fa6594d8f4cbf19f97940f81e996739fb7646882a419484c72d19e05852a7e" + }, + "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.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file