{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Managing assignment files manually"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Distributing assignments to students and collecting them can be a logistical nightmare. If you are relying on distributing the release version of the assignment to the students using your institution's existing learning management system the process of downloading the students submissions from the learning management system and then getting them back into ``nbgrader`` can be simplified by relying on some of nbgrader's built-in functionality."
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    ".. contents:: Table of Contents\n",
    "   :depth: 2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Releasing and fetching assignments"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "After an assignment has been created using `nbgrader generate_assignment`, the instructor must actually release that assignment to students. This section of the documentation assumes you are using your institution's existing learning management system for distributing the release version of the assignment. It is also assumed that the students will fetch the assignments from - and submit their assignments to - the learning management system."
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "If this is not the case and you are using nbgrader in a shared server environment, you can do this with the ``nbgrader release_assignment`` command (see :doc:`managing_assignment_files`)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Collecting assignments"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    ".. seealso::\n",
    "\n",
    "    :doc:`/command_line_tools/nbgrader-zip-collect`\n",
    "        Command line options for ``nbgrader zip_collect``\n",
    "\n",
    "    :doc:`/plugins/zipcollect-plugin`\n",
    "        Plugins for ``nbgrader zip_collect``\n",
    "\n",
    "    :doc:`philosophy`\n",
    "        More details on how the nbgrader hierarchy is structured.\n",
    "\n",
    "    :doc:`/configuration/config_options`\n",
    "        Details on ``nbgrader_config.py``"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Once the students have submitted their assignments and you have downloaded these assignment files from your institution's learning management system, you can get theses files back into ``nbgrader`` by using the ``nbgrader zip_collect`` sub-command."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Directory Structure:"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "``nbgrader zip_collect`` makes a few assumptions about how the downloaded assignment files are organized. By default, ``nbgrader zip_collect`` assumes that your downloaded assignment files will be organized with the following directory structure:\n",
    "::\n",
    "\n",
    "    {downloaded}/{assignment_id}/{collect_step}/\n",
    "\n",
    "where:\n",
    "\n",
    "* The ``downloaded`` directory is the main directory where your downloaded assignment files are placed.\n",
    "  This location can also be customized (see the :doc:`configuration options </configuration/config_options>`)\n",
    "  so that you can run the nbgrader commands from anywhere on your system, but still have them\n",
    "  operate on the same directory.\n",
    "\n",
    "* ``assignment_id`` corresponds to the unique name of an assignment.\n",
    "\n",
    "* The ``collect_step`` sub-directory corresponds to a step in the ``zip_collect`` workflow."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Workflow"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "The workflow for using ``nbgrader zip_collect`` is\n",
    "\n",
    "1. You, as an instructor, place submitted files/archives in\n",
    "   ::\n",
    "\n",
    "       {downloaded}/{assignment_id}/{archive_directory}\n",
    "\n",
    "2. You run ``nbgrader zip_collect {assignment_id}``, which will:\n",
    "\n",
    "  a. Extract these archive files - or copy non-archive files - to\n",
    "     ::\n",
    "\n",
    "         {downloaded}/{assignment_id}/{extracted_directory}\n",
    "     \n",
    "  b. Copy these extracted files to\n",
    "     ::\n",
    "\n",
    "         {submitted_directory}/{student_id}/{assignment_id}/{notebook_id}.ipynb\n",
    "\n",
    "3. At this point you can use ``nbgrader autograde`` to grade the files in the submitted directory\n",
    "   (See :ref:`autograde-assignments`).\n",
    "\n",
    "There are a few subtleties about how ``nbgrader zip_collect`` determines the correct student and notebook ids, which we'll go through in the sections below."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Step 1: Download submission files or archives"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "The first step in the collect process is to extract files from any archive (zip) files - and copy any non-archive files - found in the following directory:\n",
    "::\n",
    "\n",
    "    {downloaded}/{assignment_id}/{archive_directory}/\n",
    "\n",
    "where:\n",
    "\n",
    "* The ``archive_directory`` contains the actual submission files or archives downloaded from your institution's\n",
    "  learning management system. ``nbgrader zip_collect`` assumes you have already created this directory and placed all\n",
    "  downloaded submission files or archives in this directory."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For demo purposes we have already created the directories needed by the ``nbgrader zip_collect`` sub-command and placed the downloaded assignment submission files and archive (zip) files in there. For example we have one ``.zip`` file and one ``.ipynb`` file:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "total ##\n",
      "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] notebooks.zip\n",
      "-rw------- 1 nb_user nb_group [size] [date] [time] ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "\n",
    "ls -l downloaded/ps1/archive"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But before we can run the ``nbgrader zip_collect`` sub-command we first need to specify a few config options:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Overwriting nbgrader_config.py\n"
     ]
    }
   ],
   "source": [
    "%%file nbgrader_config.py\n",
    "\n",
    "c = get_config()\n",
    "\n",
    "# Only set for demo purposes so as to not mess up the other documentation\n",
    "c.CourseDirectory.submitted_directory = 'submitted_zip'\n",
    "\n",
    "# Only collect submitted notebooks with valid names\n",
    "c.ZipCollectApp.strict = True\n",
    "\n",
    "# Apply this regular expression to the extracted file filename (absolute path)\n",
    "c.FileNameCollectorPlugin.named_regexp = (\n",
    "    '.*_(?P<student_id>\\w+)_attempt_'\n",
    "    '(?P<timestamp>[0-9\\-]+)_'\n",
    "    '(?P<file_id>.*)'\n",
    ")\n",
    "\n",
    "c.CourseDirectory.db_assignments = [dict(name=\"ps1\", duedate=\"2015-02-02 17:00:00 UTC\")]\n",
    "c.CourseDirectory.db_students = [\n",
    "    dict(id=\"bitdiddle\", first_name=\"Ben\", last_name=\"Bitdiddle\"),\n",
    "    dict(id=\"hacker\", first_name=\"Alyssa\", last_name=\"Hacker\")\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Setting the ``strict`` flag ``True`` skips any submitted notebooks with invalid names.\n",
    "\n",
    "By default the ``nbgrader zip_collect`` sub-command uses the ``FileNameCollectorPlugin`` to collect files from the ``extracted_directory``. This is done by sending each filename (**absolute path**) through to the ``FileNameCollectorPlugin``, which in turn applies a named group regular expression (``named_regexp``) to the filename.\n",
    "\n",
    "The ``FileNameCollectorPlugin`` returns ``None`` if the given file should be skipped or it returns an object that must contain the ``student_id`` and ``file_id`` data, and can optionally contain the ``timestamp``, ``first_name``, ``last_name``, and ``email`` data.\n",
    "\n",
    "Thus if using the default ``FileNameCollectorPlugin`` you must at least supply the ``student_id`` and ``file_id`` named groups. This plugin assumes all extracted files have the same filename or path structure similar to the downloaded notebook:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "total ##\n",
      "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] notebooks.zip\n",
      "-rw------- 1 nb_user nb_group [size] [date] [time] ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "\n",
    "ls -l downloaded/ps1/archive"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    ".. note::\n",
    "\n",
    "    When collecting files in assignment sub-folders the ``file_id`` data must include the relative path to\n",
    "    ``{assignment_id}`` and the filename in order to preserve the assignment directory structure.\n",
    "\n",
    "If you wish to use a custom plugin for the ``ZipCollectApp`` see :doc:`/plugins/zipcollect-plugin` for more information."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Before we extract the files, we also need to have run ``nbgrader generate_assignment``:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[GenerateAssignmentApp | WARNING] Removing existing assignment: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/release/ps1\n",
      "[GenerateAssignmentApp | INFO] Copying [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/source/./ps1/jupyter.png -> [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/release/./ps1/jupyter.png\n",
      "[GenerateAssignmentApp | INFO] Updating/creating assignment 'ps1': {'duedate': '2015-02-02 17:00:00 UTC'}\n",
      "[GenerateAssignmentApp | INFO] Converting notebook [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/source/./ps1/problem1.ipynb\n",
      "[GenerateAssignmentApp | INFO] Writing [size] bytes to [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/release/./ps1/problem1.ipynb\n",
      "[GenerateAssignmentApp | INFO] Converting notebook [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/source/./ps1/problem2.ipynb\n",
      "[GenerateAssignmentApp | INFO] Writing [size] bytes to [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/release/./ps1/problem2.ipynb\n",
      "[GenerateAssignmentApp | INFO] Setting destination file permissions to 644\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "\n",
    "nbgrader generate_assignment \"ps1\" --IncludeHeaderFooter.header=source/header.ipynb --force"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Step 2: Extract, collect, and copy files"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "With the ``nbgrader_config.py`` file created we can now run the ``nbgrader zip_collect`` sub-command. This will:\n",
    "\n",
    "a. Extract archive - or copy non-archive - files from the ``{archive_directory}`` into\n",
    "   the following directory:\n",
    "   ::\n",
    "\n",
    "      {downloaded}/{assignment_id}/{extracted_directory}/\n",
    "\n",
    "b. Then collect and copy files from the ``extracted_directory`` above to the students\n",
    "   ``submitted_directory``:\n",
    "   ::\n",
    "\n",
    "       {submitted_directory}/{student_id}/{assignment_id}/{notebook_id}.ipynb\n",
    "\n",
    "For example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[ZipCollectApp | INFO] Using file extractor: ExtractorPlugin\n",
      "[ZipCollectApp | INFO] Using file collector: FileNameCollectorPlugin\n",
      "[ZipCollectApp | WARNING] Directory not found. Creating: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted\n",
      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/archive/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
      "[ZipCollectApp | INFO] Extracting from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/archive/notebooks.zip\n",
      "[ZipCollectApp | INFO]   Extracting to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks\n",
      "[ZipCollectApp | INFO] Start collecting files...\n",
      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_jupyter.png\n",
      "[ZipCollectApp | WARNING] Skipped submission with no match information provided: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_jupyter.png\n",
      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_problem1.ipynb\n",
      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_problem2.ipynb\n",
      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_hacker_attempt_2016-01-30-16-30-10_jupyter.png\n",
      "[ZipCollectApp | WARNING] Skipped submission with no match information provided: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_hacker_attempt_2016-01-30-16-30-10_jupyter.png\n",
      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_hacker_attempt_2016-01-30-16-30-10_myproblem1.ipynb\n",
      "[ZipCollectApp | WARNING] Skipped notebook with invalid name 'myproblem1.ipynb'\n",
      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_hacker_attempt_2016-01-30-16-30-10_problem2.ipynb\n",
      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
      "[ZipCollectApp | WARNING] 4 files collected, 3 files skipped\n",
      "[ZipCollectApp | INFO] Start transfering files...\n",
      "[ZipCollectApp | WARNING] Directory not found. Creating: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/bitdiddle/ps1\n",
      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_problem1.ipynb\n",
      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/bitdiddle/ps1/problem1.ipynb\n",
      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_problem2.ipynb\n",
      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/bitdiddle/ps1/problem2.ipynb\n",
      "[ZipCollectApp | INFO] Creating timestamp: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/bitdiddle/ps1/timestamp.txt\n",
      "[ZipCollectApp | WARNING] Directory not found. Creating: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/hacker/ps1\n",
      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_hacker_attempt_2016-01-30-16-30-10_problem2.ipynb\n",
      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/hacker/ps1/problem2.ipynb\n",
      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/hacker/ps1/problem1.ipynb\n",
      "[ZipCollectApp | INFO] Creating timestamp: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/hacker/ps1/timestamp.txt\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "\n",
    "nbgrader zip_collect ps1 --force"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "After running the ``nbgrader zip_collect`` sub-command, the archive (zip) files were extracted - and the non-archive files were copied - to the ``extracted_directory``:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "total ##\n",
      "drwxr-xr-x 1 nb_user nb_group [size] [date] [time] notebooks\n",
      "-rw------- 1 nb_user nb_group [size] [date] [time] ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
      "total ##\n",
      "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] ps1_bitdiddle_attempt_2016-01-30-15-30-10_jupyter.png\n",
      "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] ps1_bitdiddle_attempt_2016-01-30-15-30-10_problem1.ipynb\n",
      "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] ps1_bitdiddle_attempt_2016-01-30-15-30-10_problem2.ipynb\n",
      "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] ps1_hacker_attempt_2016-01-30-16-30-10_jupyter.png\n",
      "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] ps1_hacker_attempt_2016-01-30-16-30-10_myproblem1.ipynb\n",
      "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] ps1_hacker_attempt_2016-01-30-16-30-10_problem2.ipynb\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "\n",
    "ls -l downloaded/ps1/extracted/\n",
    "ls -l downloaded/ps1/extracted/notebooks/"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "By default archive files will be extracted into their own sub-directory in the ``extracted_directory`` and any archive files, within archive files, will also be extracted into their own sub-directory along the path. To change this default behavior you can write your own extractor plugin for ``zip_collect`` (see :doc:`/plugins/zipcollect-plugin`).\n",
    "\n",
    "These extracted files were then collected and copied into the students ``submitted_directory``:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "total ##\n",
      "drwxr-xr-x 1 nb_user nb_group [size] [date] [time] bitdiddle\n",
      "drwxr-xr-x 1 nb_user nb_group [size] [date] [time] hacker\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "\n",
    "ls -l submitted_zip"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "total ##\n",
      "-rw------- 1 nb_user nb_group [size] [date] [time] problem1.ipynb\n",
      "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] problem2.ipynb\n",
      "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] timestamp.txt\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "\n",
    "ls -l submitted_zip/hacker/ps1/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Custom plugins"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    ".. seealso::\n",
    "\n",
    "    :doc:`/plugins/zipcollect-plugin`\n",
    "        Plugins for ``nbgrader zip_collect``\n",
    "\n",
    "Unfortunately, for the demo above, the timestamp strings from the filenames did not parse correctly:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2016-01-31 06:00:00"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "\n",
    "cat submitted_zip/hacker/ps1/timestamp.txt"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is an issue with the underlying ``dateutils`` package used by ``nbgrader``. But not to worry, we can easily create a custom collector plugin to correct the timestamp strings when the files are collected, for example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Writing plugin.py\n"
     ]
    }
   ],
   "source": [
    "%%file plugin.py\n",
    "\n",
    "from nbgrader.plugins import FileNameCollectorPlugin\n",
    "\n",
    "\n",
    "class CustomPlugin(FileNameCollectorPlugin):\n",
    "    def collect(self, submission_file):\n",
    "        info = super(CustomPlugin, self).collect(submission_file)\n",
    "        if info is not None:\n",
    "            info['timestamp'] = '{}-{}-{} {}:{}:{}'.format(\n",
    "                *tuple(info['timestamp'].split('-'))\n",
    "            )\n",
    "        return info"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[ZipCollectApp | INFO] Using file extractor: ExtractorPlugin\n",
      "[ZipCollectApp | INFO] Using file collector: CustomPlugin\n",
      "[ZipCollectApp | WARNING] Clearing existing files in [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted\n",
      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/archive/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
      "[ZipCollectApp | INFO] Extracting from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/archive/notebooks.zip\n",
      "[ZipCollectApp | INFO]   Extracting to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks\n",
      "[ZipCollectApp | INFO] Start collecting files...\n",
      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_jupyter.png\n",
      "[ZipCollectApp | WARNING] Skipped submission with no match information provided: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_jupyter.png\n",
      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_problem1.ipynb\n",
      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_problem2.ipynb\n",
      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_hacker_attempt_2016-01-30-16-30-10_jupyter.png\n",
      "[ZipCollectApp | WARNING] Skipped submission with no match information provided: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_hacker_attempt_2016-01-30-16-30-10_jupyter.png\n",
      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_hacker_attempt_2016-01-30-16-30-10_myproblem1.ipynb\n",
      "[ZipCollectApp | WARNING] Skipped notebook with invalid name 'myproblem1.ipynb'\n",
      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_hacker_attempt_2016-01-30-16-30-10_problem2.ipynb\n",
      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
      "[ZipCollectApp | WARNING] 4 files collected, 3 files skipped\n",
      "[ZipCollectApp | INFO] Start transfering files...\n",
      "[ZipCollectApp | WARNING] Clearing existing files in [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/bitdiddle/ps1\n",
      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_problem1.ipynb\n",
      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/bitdiddle/ps1/problem1.ipynb\n",
      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_problem2.ipynb\n",
      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/bitdiddle/ps1/problem2.ipynb\n",
      "[ZipCollectApp | INFO] Creating timestamp: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/bitdiddle/ps1/timestamp.txt\n",
      "[ZipCollectApp | WARNING] Clearing existing files in [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/hacker/ps1\n",
      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_hacker_attempt_2016-01-30-16-30-10_problem2.ipynb\n",
      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/hacker/ps1/problem2.ipynb\n",
      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/hacker/ps1/problem1.ipynb\n",
      "[ZipCollectApp | INFO] Creating timestamp: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/hacker/ps1/timestamp.txt\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "\n",
    "# Use force flag to overwrite existing files\n",
    "nbgrader zip_collect --force --collector=plugin.CustomPlugin ps1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The ``--force`` flag is used this time to overwrite existing extracted and submitted files. Now if we check the timestamp we see it parsed correctly:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2016-01-30 20:30:10"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "\n",
    "cat submitted_zip/hacker/ps1/timestamp.txt"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that there should only ever be *one* instructor who runs the ``nbgrader zip_collect`` command (and there should probably only be one instructor -- the same instructor -- who runs `nbgrader generate_assignment`, `nbgrader autograde` and `nbgrader formgrade` as well). However this does not mean that only one instructor can do the grading, it just means that only one instructor manages the assignment files. Other instructors can still perform grading by accessing the formgrader URL."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python",
   "language": "python",
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
