Repository: ucbds-infra/otter-grader

Issue:
Add files arguments pass in at # ASSIGNMENT CONFIG to Export Cell
**Is your feature request related to a problem? Please describe.**

I found the zipping students' submission feature by `grader.export()` pretty useful. We aim to combine the written LaTeX questions along with the coding question in a single notebook. In order to support students who are not familiar with LaTeX, we want them to insert images into the markdown cell.

**Issue:**
The images are not automatically compressed into the zip file as those are not specified by the `grader.export(files=)` arguments. In addition, in the student version of the notebook, we cannot easily pass on the argument of `files` by the assignment config at the top of the notebook. Plus, the support of passing directories instead of specific file paths.

**Describe the solution you'd like**
Add argument support under `export_cell, files` and pass that to the `grader.export(files=files)`. This should include all the subfiles in the specified directory.



Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 18c49b6c..842e16de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
 * Updated default version of `ottr` to v1.4.0
 * Added a configuration to require students to acknowledge when a PDF of their notebook cannot be generated when using `Notebook.export` before exporting the zip file per [#599](https://github.com/ucbds-infra/otter-grader/issues/599)
 * Added a simple TCP socket server for receiving Otter's logs from the executed notebook to re-enable question logging during Otter Assign per [#589](https://github.com/ucbds-infra/otter-grader/issues/589)
+* Fixed recursive inclusion of files when a directory is in the list passed to the `files` argument of `Notebook.export` per [#620](https://github.com/ucbds-infra/otter-grader/issues/620)
 
 **v5.0.2:**
 
diff --git a/otter/check/notebook.py b/otter/check/notebook.py
index c67c897d..6d8a63cb 100644
--- a/otter/check/notebook.py
+++ b/otter/check/notebook.py
@@ -411,7 +411,7 @@ class Notebook(Loggable):
 
             for file in files:
                 if os.path.isdir(file):
-                    sub_files = glob(f"./{file}/**/*.*")
+                    sub_files = glob(f"{file}/**/*.*", recursive=True)
                     for sub_file in sub_files:
                         zf.write(sub_file)
                 else:


Test patch:
diff --git a/test/test_check/test_notebook.py b/test/test_check/test_notebook.py
index b5f1347b..a2ca3bf4 100644
--- a/test/test_check/test_notebook.py
+++ b/test/test_check/test_notebook.py
@@ -4,6 +4,7 @@ import datetime as dt
 import nbformat as nbf
 import os
 import pytest
+import shutil
 
 from glob import glob
 from textwrap import dedent
@@ -160,6 +161,33 @@ def test_export(mocked_export, mocked_zf, mocked_dt, write_notebook):
     # TODO: test display_link
 
 
+@mock.patch("otter.check.notebook.zipfile.ZipFile")
+def test_export_with_directory_in_files(mocked_zf, write_notebook):
+    """
+    Checks that ``Notebook.export`` correctly recurses into subdirectories to find files when a
+    directory is in the list passed to the ``files`` argument.
+    """
+    write_notebook(nbf.v4.new_notebook())
+
+    file_path = FILE_MANAGER.get_path("data/foo.csv")
+    dir_path = os.path.split(file_path)[0]
+    os.makedirs(dir_path, exist_ok=True)
+    with open(file_path, "w+") as f:
+        f.write("hi there")
+
+    try:
+        grader = Notebook(tests_dir=TESTS_DIR)
+        with mock.patch.object(grader, "_resolve_nb_path") as mocked_resolve:
+            mocked_resolve.return_value = NB_PATH
+
+            grader.export(pdf=False, files=[dir_path])
+
+            mocked_zf.return_value.write.assert_any_call(file_path)
+
+    finally:
+        shutil.rmtree(dir_path)
+
+
 @mock.patch("otter.check.utils.Button")
 @mock.patch("otter.check.utils.HTML")
 @mock.patch("otter.check.utils.Output")