Skip to content

Commit

Permalink
Merge pull request #178 from DigiKlausur/refactor_exam_submit
Browse files Browse the repository at this point in the history
Refactor exam submit
  • Loading branch information
tmetzl authored Mar 15, 2024
2 parents 31ccffb + 431af1b commit a212863
Show file tree
Hide file tree
Showing 29 changed files with 1,422 additions and 287 deletions.
108 changes: 108 additions & 0 deletions docs/source/user_docs/exchange/base_exchange.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
.. _custom_exchange:

=====================
Personalized Exchange
=====================

`e2xgrader` comes with an optional custom exchange. The exchange takes care of distributing assignments and feedback to students and collecting submissions.
Our exchange is based on the original nbgrader exchange and extends it with the following features:

- Custom :ref:`submit <custom_submit_exchange>` behavior based on e2xgrader mode, with advanced features like hashing and HTML conversion for exams in student_exam mode.
- Personalized inbound and feedback directory for each student, to prevent students from reading each other's submissions and feedback. (Activated by default)
- Personalized outbound directory for each student, to provide personalized versions of an assignment. (Deactivated by default)

Activating the Exchange
-----------------------

To activate the exchange head over to the section about :ref:`configuring e2xgrader <configure_e2xgrader>`.

Personalized Inbound
--------------------

The inbound directory is the directory where students submit their assignments.
The custom exchange can be configured to use a personalized inbound directory. This is activated by default.
When this is active, students submit to :code:`<exchange_directory>/<course_id>/personalized-inbound/<student_id>/`.
Students will only have access to their own submissions.

In the original nbgrader exchange, the inbound directory is the same for all students (:code:`<exchange_directory>/<course_id>/inbound/`).
This can be a security issue, as students can read each other's submissions if they know the timestamp or random string of the submission.

To configure the personalized inbound, add the following to your `nbgrader_config.py`:

.. code-block:: python
# nbgrader_config.py
from e2xgrader.config import configure_base, configure_exchange
c = get_config()
# Register custom preprocessors for autograding and generating assignments
configure_base(c)
# Register custom exchange
configure_exchange(c)
# Activate the personalized inbound
c.Exchange.personalized_inbound = True
Personalized Outbound
---------------------

The outbound directory is the directory where students fetch their assignments from.
If the personalized outbound is activated, students will fetch from a personalized directory.
This is useful if you want to create personalized versions of an assignment for each student.

Students will fetch from
:code:`<exchange_directory>/<course_id>/outbound/<assignment_id>/<student_id>/`.

To create personalized versions of an assignment, you will need to create a directory for each student under the release version of an assignment.

Instead of having your notebooks under
:code:`release/<assignment_id>/MyNotebook.ipynb` you will need to create a
directory for each student as
:code:`release/<assignment_id>/<student_id>/MyNotebook.ipynb`. These notebooks can be personalized for each student.

.. code-block:: python
# nbgrader_config.py
from e2xgrader.config import configure_base, configure_exchange
c = get_config()
# Register custom preprocessors for autograding and generating assignments
configure_base(c)
# Register custom exchange
configure_exchange(c)
# Activate the personalized outbound
c.Exchange.personalized_outbound = True
Personalized Feedback
---------------------

The feedback directory is the directory where students fetch their feedback from. This is activated by default.
Similar to the personalized outbound, there is an option for personalized feedback.
This makes sure students can only read their feedback directory.
To configure the personalized feedback, add the following to your `nbgrader_config.py`:

.. code-block:: python
# nbgrader_config.py
from e2xgrader.config import configure_base, configure_exchange
c = get_config()
# Register custom preprocessors for autograding and generating assignments
configure_base(c)
# Register custom exchange
configure_exchange(c)
# Activate the personalized feedback
c.Exchange.personalized_feedback = True
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
105 changes: 4 additions & 101 deletions docs/source/user_docs/exchange/index.rst
Original file line number Diff line number Diff line change
@@ -1,106 +1,9 @@
.. _custom_exchange:

===============
Custom Exchange
===============

`e2xgrader` comes with an optional custom exchange. The exchange provides personalized directories for each student.

Activating the Exchange
-----------------------

To activate the exchange head over to the section about :ref:`configuring e2xgrader <configure_e2xgrader>`.

Personalized Inbound
--------------------

The custom exchange can be configured to use a personalized inbound.
If this is activated, each student will have a personalized inbound directory.

Assume you have the course *MyCourse* and the assignment
*MyAssignment*. In the original nbgrader exchange the student
will submit to :code:`<exchange_directory>/MyCourse/inbound/`.
This will be the same for each student and causes a potential security
issue. If a student knows the name of the submission of another student,
they can read their submission.

If the personalized inbound is used, the student will submit to
:code:`<exchange_directory>/MyCourse/personalized-inbound/<student_id>/`.
This directory is only readable by the student.

.. code-block:: python
# nbgrader_config.py
from e2xgrader.config import configure_base, configure_exchange
c = get_config()
# Register custom preprocessors for autograding and generating assignments
configure_base(c)
# Register custom exchange
configure_exchange(c)
# Activate the personalized inbound
c.Exchange.personalized_inbound = True
Personalized Outbound
---------------------

If activated, the custom exchange uses a personalized outbound
directory for each student.

This allows for creating custom versions of an assignment per student.
Students will fetch from
:code:`<exchange_directory>/MyCourse/outbound/MyAssignment/<student_id>/`.

For this to work you will need a release version for each student.
In your formgrader you will need to create a folder for each student
under the release version of an assignment.

Instead of having your notebooks under
:code:`release/MyAssignment/MyNotebook.ipynb` you will need to create a
directory for each student as
:code:`release/MyAssignment/<student_id>/MyNotebook.ipynb`. These notebooks
can then be personalized.

.. code-block:: python
# nbgrader_config.py
from e2xgrader.config import configure_base, configure_exchange
c = get_config()
# Register custom preprocessors for autograding and generating assignments
configure_base(c)
# Register custom exchange
configure_exchange(c)
# Activate the personalized outbound
c.Exchange.personalized_outbound = True
Personalized Feedback
---------------------

Similar to the personalized outbound, there is an option for personalized feedback.
This makes sure students can only read their feedback directory.

.. code-block:: python
# nbgrader_config.py
from e2xgrader.config import configure_base, configure_exchange
c = get_config()
# Register custom preprocessors for autograding and generating assignments
configure_base(c)
# Register custom exchange
configure_exchange(c)
.. toctree::
:maxdepth: 2

# Activate the personalized feedback
c.Exchange.personalized_feedback = True
base_exchange
submit
95 changes: 95 additions & 0 deletions docs/source/user_docs/exchange/submit.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
.. _custom_submit_exchange:

======================
Custom Submit Exchange
======================

The custom exchange also comes with a custom submit. This exchange behaves differently
based on the active `e2xgrader` mode.

Assignment Mode
---------------

In `student` mode, the submit exchange will only copy over all files to the exchange directory.


Exam Mode
---------

In `student_exam` mode, the submit exchange will hash all files in the assignment directory and store the hashes in a file.
The default hashing method is sha1. The file containing the hashes is called `SHA1SUM.txt`.
The contents of the file can be checked in the terminal with the command:

.. code-block:: bash
sha1sum -c SHA1SUM.txt
Additionally, the exchange will convert all notebooks to html files for students to check their submission.
The name of the html file is the same as the notebook file with the extension `_hashcode.html`.

A cell is added to the top of the html file with the hashcode and timestamp of the notebook.
The hashcode displayed is truncated to 12 characters.

.. figure:: img/submitted_html.png
:align: center

Example of the html file generated by the exam exchange.

The message displayed in the cell is customizable. As an example, here is the configuration for a custom message:

.. code-block:: python
# nbgrader_config.py
from e2xgrader.config import configure_base, configure_exchange
c = get_config()
# Register custom preprocessors for autograding and generating assignments
configure_base(c)
# Register custom exchange
configure_exchange(c)
c.SubmissionExporter.exam_submitted_message = "Ihre Klausur wurde erfolgreich abgegeben."
c.SubmissionExporter.your_hashcode_message = "Ihr Hashcode ist:"
c.SubmissionExporter.verify_exam_message = "Bitte überprüfen Sie Ihre Klausur unten und loggen Sie sich aus."
The resulting html file will look like this:

.. figure:: img/submitted_html_de.png
:align: center

Example of the html file generated by the exam exchange with a custom message.

Advanced Exam Configuration
~~~~~~~~~~~~~~~~~~~~~~~~~~~

The submit exchange can also be configured to use different `HTMLExporter`.
It is advised to use an exporter that inherits from `e2xgrader.exporters.E2xExporter`, since it makes sure all custom cells are rendered correctly.

This is done via:

.. code-block:: python
# nbgrader_config.py
from e2xgrader.config import configure_base, configure_exchange
c = get_config()
# Register custom preprocessors for autograding and generating assignments
configure_base(c)
# Register custom exchange
configure_exchange(c)
# Use a custom HTMLExporter
c.ExchangeSubmit.submission_exporter_class = "e2xgrader.exporters.SubmissionExporter"
The exporter will be initialized with the `nbgrader_config`` and called with the `hashcode` and `timestamp` as resources:

.. code-block:: python
exporter = self.submission_exporter_class(config=self.config)
exporter.from_notebook_node(nb, resources=dict(hashcode=hashcode, timestamp=timestamp))
Binary file modified docs/source/user_docs/img/submit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 18 additions & 4 deletions docs/source/user_docs/modes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,27 @@ The exam toolbar is displayed in student exam mode. It contains all the resource

The exam toolbar contains a submit button, where students can submit from within the notebook.
It autosaves the notebook and submits the assignment. If used with the :ref:`custom exchange <custom_exchange>`, the timestamp of the submission is displayed.
If the assignment only contains a single notebook with the same name as the assignment, a hashcode will be generated.
The point of the hashcode is to create a checksum over an exam s.t. students can always check the integrity of their exam.
Additionally, all files in the notebook directory are hashed and the hashes are stored in a file called `SHA1SUM.txt` in the same directory as the notebook.
The point of the hashcode is to create a checksum over an exam such that students can always check the integrity of their exam.
Then all notebooks are converted to html files for students to check their submission.
The name of each html file is the same as the notebook file with the extension `_hashcode.html`.

.. figure:: img/submit.png
:alt: Submit confirmation

Dialog displayed after submit is clicked.

There is also a link where students can verify their submission. When they click on it, they will see an HTML version of their submitted exam. This way students can make sure all their answers are in the submitted version.
The HTML version also contains the hashcode and timestamp.
Students have the option to continue working on the notebook after submitting or exiting the exam.
When the exam is exited, the student is redirected to the html version of the current notebook.

Here they can check the integrity of their submission.

A cell is added to the top of the html file with the hashcode and timestamp of the notebook.
The hashcode displayed is truncated to 12 characters.

More information about the custom submit exchange can be found in the :ref:`custom submit <custom_submit_exchange>` section.

.. figure:: exchange/img/submitted_html.png
:align: center

Example of the html file generated by the exam exchange.
21 changes: 14 additions & 7 deletions e2xgrader/exchange/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,22 @@ class E2xExchangeCollect(E2xExchange, ExchangeCollect):
).tag(config=True)

def init_submissions(self):
student_id = self.coursedir.student_id if self.coursedir.student_id else "*"
if self.personalized_inbound:
self.log.info("Collecting from restricted submit dirs")
submit_dirs = [
username
for username in os.listdir(self.inbound_path)
if "+" not in username
and os.path.isdir(os.path.join(self.inbound_path, username))
]
if student_id == "*":
submit_dirs = [
username
for username in os.listdir(self.inbound_path)
if "+" not in username
and os.path.isdir(os.path.join(self.inbound_path, username))
]
else:
submit_dirs = [
username
for username in os.listdir(self.inbound_path)
if username == student_id
]
self.log.info(f"Submission dirs: {submit_dirs}")

usergroups = defaultdict(list)
Expand All @@ -59,7 +67,6 @@ def init_submissions(self):
records.append(user_records)

else:
student_id = self.coursedir.student_id if self.coursedir.student_id else "*"
pattern = os.path.join(
self.inbound_path,
"{}+{}+*".format(student_id, self.coursedir.assignment_id),
Expand Down
Loading

0 comments on commit a212863

Please sign in to comment.