Repository: git-keeper/git-keeper (language: python)

Issue:
Bump version to 1.0.1



Discussion / hints:

Fix patch:
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 00000000..f4d0092c
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,13 @@
+version: 2
+
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.10"
+
+mkdocs:
+  configuration: mkdocs.yml
+
+python:
+  install:
+    - requirements: docs/requirements.txt
diff --git a/VERSION b/VERSION
index 3eefcb9d..7dea76ed 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-1.0.0
+1.0.1
diff --git a/docs/admin-users.md b/docs/admin-users.md
new file mode 100644
index 00000000..a25ba6c7
--- /dev/null
+++ b/docs/admin-users.md
@@ -0,0 +1,217 @@
+# Admin Users
+
+This page contains a guide for setting up a server to run git-keeper. For more
+details about the different server configuration options see
+[Server Configuration](reference.md#server-configuration).
+
+## Server Setup
+
+This guide assumes you are starting with an Ubuntu Server minimal
+installation. git-keeper should work with any Linux distribution, but the setup
+details might be slightly different.
+
+### Quick Start
+
+Here is an overview of the steps necessary to get a git-keeper server up and
+running. Follow these if you have done it before and just need a
+refresher. Skip to the next section for more detailed instructions.
+
+These instructions assume you have a dedicated server with an Ubuntu Server
+installed and a working SSH server.
+
+* Create the `keeper` user, which must be in the `keeper` group
+* Add a line to `/etc/ssh/sshd_config` to disallow the user `keeper` to log in via SSH: `DenyUsers keeper`
+* Install `sudo`, `python3`, `git`, `firejail` (optional), and `docker`
+  (optional)
+* Install `git-keeper-server` with `pip`
+* Configure `sudo` so that `keeper` can run any command without a password
+* Create `server.cfg`
+* Create a user systemd service to automatically start gkeepd
+* Add faculty users
+
+### Requirements
+
+git-keeper requires a server running a Linux operating system which is
+dedicated to running git-keeper. The server must allow incoming SSH traffic 
+and it must have access to an SMTP server. 
+
+The guide assumes you have installed the Ubuntu Server minimal installation and
+that you have enabled the SSH server and selected `docker` to install during
+installation.
+
+Other Linux distributions will work as well but some of the following steps may
+be a little different.
+
+### Create the Keeper User
+
+The `keeper` user is the user that will run the git-keeper server
+process. **This user will have root privileges.** 
+
+Create the user. Be sure to choose a strong password:
+ 
+```no-highlight
+adduser keeper
+```
+
+Ensure that `keeper` is also in the group `keeper`. This command shows the
+groups that `keeper` is in:
+
+```no-highlight
+groups keeper
+```
+
+### Configure SSH
+
+The `keeper` user has elevated privileges, so nobody should be able to SSH to
+the server as that user. Add the line below to `/etc/ssh/sshd_config` to
+prevent this. If you are only able to access your server via SSH, be sure you
+have another less privileged user you can still use to SSH in.
+ 
+```no-highlight
+DenyUsers keeper
+```
+
+Now restart the SSH server:
+
+```no-highlight
+systemctl restart ssh.service
+```
+
+### Install Dependencies
+
+The required dependencies are `sudo`, `git`, and `python3` >= 3.8 with
+`pip`. Installing `firejail` is highly recommended for test
+sandboxing. Installing `docker` allows for even more flexible sandboxing.
+
+The Ubuntu Server minimal install comes with `sudo` and `python3`, and
+installing `docker` can be done during the setup process. That leaves the
+following to install:
+
+```no-highlight
+sudo apt install git python3-pip firejail
+```
+
+### Configure `sudo`
+
+The `keeper` user needs to be able to run a number of commands as root and
+the tester user. To allow this, create a `sudo` configuration for `keeper`:
+
+```no-highlight
+sudo visudo -f /etc/sudoers.d/keeper
+```
+
+Add the following line to the configuration:
+
+```no-highlight
+keeper ALL = (ALL) NOPASSWD: ALL
+```
+
+### Install the Server Package
+
+The git-keeper server can be installed using `pip` like so:
+
+```no-highlight
+sudo python3 -m pip install git-keeper-server
+```
+
+### Create `server.cfg`
+
+There must be a file named `server.cfg` in the `keeper` user's home
+directory. This is the configuration file for the server. See below for a
+template `server.cfg`, or see the
+[Server Configuration](reference.md#server-configuration) reference for more
+detailed descriptions of each section and field.
+
+#### Template `server.cfg`
+
+Here is a template `server.cfg`. Required parameters must be defined, optional
+parameters are commented out with their default values, if they exist.
+
+```
+[server]
+hostname = 
+
+[email]
+from_name = 
+from_address = 
+smtp_server = 
+smtp_port = 
+#use_tls = true
+#email_username = 
+#email_password = 
+#email_interval = 2
+#use_html = true
+
+[admin]
+admin_email = 
+admin_first_name = 
+admin_last_name = 
+
+#[gkeepd]
+#test_thread_count = 1
+#tests_timeout = 300
+#tests_memory_limit = 1024
+#default_test_env = firejail
+```
+
+### Using a `systemd` service
+
+You can run `gkeepd` in a `screen` or `tmux` session but it is recommended that
+you run `gkeepd` as a `systemd` service so that it automatically starts on
+boot.
+
+#### Creating the Service
+
+Create the file `/etc/systemd/system/gkeepd.service` with the
+following contents:
+
+```
+[Unit]
+Description=git-keeper server
+
+[Service]
+Type=simple
+User=keeper
+Group=keeper
+ExecStart=/usr/local/bin/gkeepd
+
+[Install]
+WantedBy=default.target
+```
+
+This assumes that `gkeepd` was installed at `/usr/local/bin/gkeepd`. Type
+`which gkeepd` to see where the executable is on your system and adjust the
+path if necessary.
+
+
+#### Enabling and Starting the Service
+
+Enable:
+
+```no-highlight
+sudo systemctl enable gkeepd
+```
+
+Start:
+
+```no-highlight
+sudo systemctl start gkeepd
+```
+
+Check that it is running:
+
+```no-highlight
+sudo systemctl status gkeepd
+```
+
+You can also look at `~keeper/gkeepd.log` to check on the status of the daemon.
+
+### Adding Faculty Members
+
+Once the server is running, the admin user can use the
+[client](faculty-users.md#client-setup) to add additional faculty members with
+[`gkeep add_faculty`](reference.md#add_faculty) like so:
+
+```no-highlight
+gkeep add_faculty <last name> <first name> <email address>
+```
diff --git a/docs/admin/faculty_csv.md b/docs/admin/faculty_csv.md
deleted file mode 100644
index 8fe0e6d8..00000000
--- a/docs/admin/faculty_csv.md
+++ /dev/null
@@ -1,5 +0,0 @@
-## gkeepd's faculty.csv
-
-`<last name>,<first name>,<email address>`
-
-
diff --git a/docs/admin/server-setup.md b/docs/admin/server-setup.md
deleted file mode 100644
index 0cb230b8..00000000
--- a/docs/admin/server-setup.md
+++ /dev/null
@@ -1,255 +0,0 @@
-# Server Setup
-
-This guide details how to set up a server to run git-keeper. The guide assumes
-you are starting with a Debian Jessie minimal netinstall
-installation. git-keeper should work with any Linux distribution, but the setup
-details might be slightly different.
-
-## Quick Start
-
-Here is an overview of the steps necessary to get a git-keeper server up and
-running. Follow these if you have done it before and just need a
-refresher. Skip to the next section for more detailed instructions.
-
-These instructions assume you have a dedicated server with a Debian
-installation and a working SSH server.
-
-* Create the following users:
-    * `keeper`. Must be in the `keeper` group.
-* Add a line to `/etc/ssh/sshd_config` to disallow the user `keeper` to log in via SSH: `DenyUsers keeper`
-* Install `sudo`, `python3`, and `git`
-* Clone the git-keeper repository and install both `git-keeper-core` and
-`git-keeper-server`
-* Configure `sudo` so that `keeper` can run any command without a password
-* Create `server.cfg`
-* Create a user systemd service to automatically start gkeepd
-* Add faculty members
-
-### Requirements
-
-git-keeper requires a server running a Linux operating system which is
-dedicated to running git-keeper. The server must allow incoming SSH traffic 
-and it must have access to an SMTP server. 
-
-The guide assumes you have installed Debian from the minimal network
-installation media and that you have enabled the SSH server.
-
-Other Linux distributions will work as well but some of the following steps may
-be a little different.
-
-### Create the Keeper User
-
-The `keeper` user is the user that will run the git-keeper server
-process. **This user will have root privileges.**
-
-Create the user. Be sure to choose a strong password:
- 
-```
-adduser keeper
-```
-
-Ensure that `keeper` is also in the group `keeper`. This command shows the
-groups that `keeper` is in:
-
-```
-groups keeper
-```
-
-### Configure SSH
-
-The `keeper` user has elevated privileges, so nobody should be able to SSH to
-the server as that user. Add this line to `/etc/ssh/sshd_config` to prevent
-this:
- 
-```
-DenyUsers keeper
-```
-
-Now restart the SSH server:
-
-```
-systemctl restart ssh.service
-```
-
-### Install Dependencies
-
-Install the following packages:
-
-```
-apt-get install python3 sudo git
-```
-
-### Configure `sudo`
-
-The `keeper` user needs to be able to run a number of commands as root and
-the tester user. To allow this, create a `sudo` configuration for `keeper`:
-
-```
-visudo -f /etc/sudoers.d/keeper
-```
-
-Add the following line to the configuration:
-
-```
-keeper ALL = (ALL) NOPASSWD: ALL
-```
-
-### Install the Server Package
-
-The git-keeper server can be installed using `pip` like so:
-
-```
-sudo python3 -m pip install git-keeper-server
-```
-
-### Create `server.cfg`
-
-There must be a file named `server.cfg` in the `keeper` user's home
-directory. This is the configuration file for the server.
-
-`server.cfg` has four sections: `[server]`, `[email]`, `[admin]`, and `[gkeepd]`.
-
-#### `[server]`
-
-The `[server]` section has a single required parameter `hostname`, which is the
-hostname of the server. This will be used to build Git URLs.
-
-#### `[email]`
-
-The `[email]` section is also required. The following parameters are required:
-
-```
-from_name: <name that the emails will be from>
-from_address: <email address that the emails come from>
-smtp_server: <hostname of the SMTP server>
-smtp_port: <SMTP server port>
-```
-
-The following parameters are optional:
-
-```
-use_tls: <true or false, defaults to true>
-email_username: <username for the SMTP server>
-email_password: <password for the SMTP server>
-email_interval: <seconds to wait between sending emails>
-use_html: <true or false, defaults to false>
-```
-
-If `use_html` is true, submission test results will be sent as an HTML email,
-placing the contents within pre tags so that they appear in a fixed width font.
-
-#### `[admin]`
-
-The `[admin]` section is also required. The section defines an admin user. The admin user will automatically be added as a faculty user when the server first starts, and then the admin user will be able to add additional faculty members.
-
-The following parameters are required:
-
-```
-admin_email: <email address of the admin user>
-admin_first_name: <first name of the admin user>
-admin_last_name: <last name of the admin user>
-```
-
-#### `[gkeepd]`
-
-The `[gkeepd]` section is optional. Below are the allowed parameters and their
-defaults:
-
-```
-tests_timeout: 300
-tests_memory_limit: 1024
-```
-
-The `tests_timeout` parameter specifies a global timeout for tests in case an assignment's tests fail to properly account for infinite loops. The default is 300 seconds. If this timeout occurs the student and faculty member will be notified that there was an error in the tests, so one should not depend on this timeout to cleanly catch infinite loops.
-
-Similar to `tests_timeout`, the `tests_memory_limit` sets a global memory limit for tests. The default limit is 1024 MB.
-
-#### Template
-
-Here is a template `server.cfg`. Required parameters must be defined, optional
-parameters are commented out with their default values, if they exist.
-
-```
-[server]
-hostname: 
-
-[email]
-from_name: 
-from_address: 
-smtp_server: 
-smtp_port: 
-#use_tls: true
-#email_username: 
-#email_password: 
-#email_interval: 2
-
-[admin]
-admin_email: 
-admin_first_name: 
-admin_last_name: 
-
-#[gkeepd]
-#tests_timeout: 300
-#tests_memory_limit: 1024
-```
-
-### Using a `systemd` service
-
-You can run `gkeepd` in a `screen` or `tmux` session but it is recommended that
-you run `gkeepd` as a `systemd` service so that it automatically starts on
-boot.
-
-#### Creating the Service
-
-Create the file `/etc/systemd/system/gkeepd.service` with the
-following contents:
-
-```
-[Unit]
-Description=git-keeper server
-
-[Service]
-Type=simple
-User=keeper
-Group=keeper
-ExecStart=/usr/local/bin/gkeepd
-
-[Install]
-WantedBy=default.target
-```
-
-This assumes that `gkeepd` was installed at `/usr/local/bin/gkeepd`. Type
-`which gkeepd` to see where the executable is on your system and adjust the
-path if necessary.
-
-
-#### Enabling and Starting the Service
-
-Enable:
-
-```
-systemctl enable gkeepd
-```
-
-Start:
-
-```
-systemctl start gkeepd
-```
-
-Check that it is running:
-
-```
-systemctl status gkeepd
-```
-
-
-You can also look at `~keeper/gkeepd.log` to check on the status of the daemon.
-
-### Adding Faculty Members
-
-Once the server is running, the admin user can use the [client](Client-setup) to add additional faculty members like so:
-
-```
-gkeep add_faculty <last name> <first name> <email address>
-```
\ No newline at end of file
diff --git a/docs/develop/actionrunner-interface.md b/docs/develop/actionrunner-interface.md
deleted file mode 100644
index 72443224..00000000
--- a/docs/develop/actionrunner-interface.md
+++ /dev/null
@@ -1,53 +0,0 @@
-This is a working draft of an interface for a class `ActionRunner` which can run a sequence of actions to test a student submission.
-
-## Constructor
-
-The `ActionRunner` constructor has no required parameters. It has the following optional parameters:
-
-* `timeout` (default `10`): Testing will run for at most `timeout` number of seconds, at which time it will kill the tests and print an error message.
-* `timeout_message` (default `'Testing your submission took too long. Maybe you have an infinite loop?'`): The error message to print when there is a timeout.
-* `memlimit` (default `1024`): Testing will use at most `memlimit` megabytes of memory, at which time it will kill the tests and print an error message.
-* `memlimit_message` (default `'Testing your submission used too much memory.`'): The error message to print when testing uses too much memory.
-* `working_dir` (defaults to the current working directory): The path to the directory in which to run the tests.
-* `submission_dir` (defaults to `sys.argv[1]`): The path to the directory containing the student's submission.
-
-## Action Methods
-
-These methods add actions to the `ActionRunner`'s internal list of actions. The actions will not be run immediately, but they will be run in the order in which they are added.
-
-* `files_exist(required_files, optional_files=None, error_message='Error: {0} does not exist.', warning_message='Warning: {0} does not exist.')`: Check that files exist in `submission_dir`
-    * `required_files`: An iterable containing filenames relative to `submission_dir` which must exist. If any do not exist, an error message is printed and all action halts.
-    * `optional_files`: An optional iterable containing filenames relative to `submission_dir` for which a warning will be printed if any of the files do not exist. Action will not halt.
-    * `error_message`: The error message to print if a required file does not exist. Must contain `{}` or `{0}` so that the filename can be inserted with `error_message.format(filename)`.
-    * `warning_message`: The warning message to print if an optional file does not exist. Must contain `{}` or `{0}` so that the filename can be inserted with `error_message.format(filename)`.
-* `copy_files(filenames, dest=None)`: Copy files from `submission_dir`
-    * `filenames`: An iterable containing relative paths to the filenames to copy from `submission_dir`
-    * `dest`: The directory to copy into. This will be `working_dir` if not provided. If provided it will be treated as a relative path to `working_dir` which will be created if it does not already exist.
-* `copy_dirs(directories, dest=None)`: Copy directories from `submission_dir`
-    * `directories`: An iterable containing relative paths to the directories to copy from `submission_dir`
-    * `dest`: The directory to copy into. This will be `working_dir` if not provided. If provided it will be treated as a relative path to `working_dir` which will be created if it does not already exist.
-* `run_command(command, output=True, error_is_fatal=True, error_message='Error running tests:\n\n{0}')`: Run a shell command
-    * `command`: The command to run as a list or a string
-    * `output`: If True, print the output of the command. If False, do nothing with the output. If it is a string, treat it as a filename relative to `working_dir` and write the output to that file
-    * `bad_output_pattern`: A regular expression suitable for Python's `re` module. If the pattern matches, treat it the same as if the command exited with a non-zero status
-    * `error_is_fatal`: If True and the command returns a non-zero exit code, halt all actions
-    * `error_message`: The error message to print if the command returns a non-zero exit code. If the command contains a formatting specifier (`{}` or `{0}`) then the output of the command will be inserted into the message.
-* `diff(filename1, filename2, output=False, different_is_fatal=True, different_message='', same_message='', directory=None)`: Determine if the contents of 2 files are different or the same
-    * `filename1`: Relative path of the first file
-    * `filename2`: Relative path of the second file
-    * `output`: Throws the output away if false, prints it if True, writes it to a file if it is a string
-    * `different_is_fatal`: If True, all actions will halt if the files are different
-    * `different_message`: Message to print if the files' contents are different
-    * `same_message`: Message to print if the files' contents are the same
-* `search(pattern, filename, dir=None)`: Try to match a regular expression against the contents of a file
-    * `pattern`: A regular expression suitable for use with the Python's `re` module
-    * `filename`: The relative path of the file in which to search
-    * `dir`: The path of the directory where the file resides. Uses `working_dir` if not provided.
-    * TODO: optional parameters to define behavior on a match or mismatch
-
-## Run Method
-
-After calling one or more action methods, call `run()` to actually run all the actions.
-
-* `run(success_message='All tests passed!')`
-    * `success_message`: The message to print if all actions have been run with no errors
\ No newline at end of file
diff --git a/docs/develop/developers.md b/docs/develop/developers.md
deleted file mode 100644
index c9d7f402..00000000
--- a/docs/develop/developers.md
+++ /dev/null
@@ -1,104 +0,0 @@
-# Developers
-
-## Project Organization
-
-There are 3 distributions in this project:
-
-* `git-keeper-client`
-* `git-keeper-core`
-* `git-keeper-server`
-
-Both `git-keeper-client` and `git-keeper-server` depend on `git-keeper-core`.
-
-## Developer Setup
-
-The preferred development environment is PyCharm on OSX or Linux.  The following steps will ensure that your development environment is isolated from the system Python and other Python projects.
-
-### Prerequisites
-
-* [Python 3.4 or greater](https://www.python.org/downloads/)
-
-### Setup Process
-
-
-* Fork the `git-keeper/git-keeper` repo to your personal GitHub account.
-* Clone the repo from your GitHub account
-* `cd git-keeper`
-* Create a virtual environment named `.env`.  The `.gitignore` file ensures that the `.env` folder is ignored.
-
- ```
- python3 -m venv .env
- ```
- 
-* Activate the virtual environment.
-
- ```
- source .env/bin/activate
- ```
- 
-* Install the required dependencies.
-
- ```
- pip install -r requirements.txt
- ```
- 
-* Install the project modules `git-keeper-core`, `git-keeper-client`, and `git-keeper-server` as editable.  This will allow the IDE to recognize the `git-keeper` modules.
-
- ```
- pip install -e git-keeper-core
- pip install -e git-keeper-client
- pip install -e git-keeper-server
- pip install -e git-keeper-robot
- ```
-
-* Open the project in PyCharm, and wait for it to index the packages.
-
-If you clone the repo from within PyCharm, you will have to change the project interpreter after you create the virtual environment.  Also, if you have the project open in PyCharm when you install the `git-keeper` modules, you will have to restart PyCharm before it will recognize the names (this is a known bug in PyCharm).
-
-
-## Style
-
-We strive for consistent style within this project. Contributions may not be
-accepted if they do not follow these guidelines.
-
-* Project filenames
-    * Most filenames should be all lowercase with words separated by
-    underscores
-    * Use all uppercase for files like `README.md` and `COPYING`
-* Documentation
-    * Place documentation in the `docs` directory. If you add a new file, add
-    it to the table of contents in `mkdocs.yml`.
-    * Use markdown
-    * Limit lines of text to 80 characters
-* Python
-    * Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) whenever
-    possible
-    * Variable names
-        * Avoid abbreviations unless the meaning remains clear or length gets
-        way out of hand
-            * `cls` is not ok for class
-            * `config` is ok for configuration
-        * Quantities
-            * Use `item_count` vs `num_items`
-        * Dictionaries
-            * Use `<value>s_by_<key>` (i.e. `students_by_class`)
-        * Indexes
-            * For an index into a single list, use `i`
-            * For nested loops and other situations with multiple indexes, use a
-            variable name ending in `_i` (i.e. `student_i` and `class_i`)
-        * Filenames, directory names, and full paths
-            * File and directory basenames (no path)
-                * End filename variable names with `_filename`
-                * End directory name variable names with `_dir`
-            * Full paths
-                * End the variable name with `_path`. If it is ambiguous
-                whether it is a file or directory, end it with `_file_path` or
-                `_dir_path`
-    * Type hinting
-        * Use type hints for all parameters that are object references
-    * Importing
-        * Either import an entire module (`import os`) or import specific
-        items (`from queue import Queue, Empty`) but do not import all items
-        from a module (do not say `from os import *`)
-        * Import system-level items first followed by imports that are
-        internal to the project
diff --git a/docs/develop/info-json-structure.md b/docs/develop/info-json-structure.md
deleted file mode 100644
index b040a336..00000000
--- a/docs/develop/info-json-structure.md
+++ /dev/null
@@ -1,38 +0,0 @@
-Many client operations fetch the contents of the latest info file from the server. An info file contains information about a faculty member's classes and assignments. Here is the structure of an example info file:
-
-```json
-{
-   "class_name":{
-      "assignments":{
-         "assignment_name":{
-            "name":"assignment_name",
-            "published":true,
-            "reports_repo":{
-               "hash":"202585432b8ff21ff4f93a886fff9b09c46eb18e",
-               "path":"/home/faculty/classes/class_name/assignment_name/reports.git"
-            },
-            "students_repos":{
-               "alovelace":{
-                  "first":"Ada",
-                  "hash":"76f608acf4d08ccd1bf07955e2600bdce3f80774",
-                  "last":"Lovelace",
-                  "path":"/home/alovelace/faculty/class_name/assignment_name.git",
-                  "submission_count":0,
-                  "time":1476986981
-               }
-            }
-         }
-      },
-      "students":{
-         "alovelace":{
-            "email_address":"alovelace@example.edu",
-            "first":"Ada",
-            "home_dir":"/home/alovelace",
-            "last":"Lovelace",
-            "last_first_username":"lovelace_ada_alovelace",
-            "username":"alovelace"
-         }
-      }
-   }
-}
-```
\ No newline at end of file
diff --git a/docs/develop/log-events.md b/docs/develop/log-events.md
deleted file mode 100644
index 1fa001dd..00000000
--- a/docs/develop/log-events.md
+++ /dev/null
@@ -1,29 +0,0 @@
-Events are written to event logs. Each line of the log is an event, and is structured like this:
-
-```
-<timestamp> <event type> <payload>
-```
-
-For example:
-
-```
-1463892789 SUBMISSION /home/student/faculty/class/assignment.git
-```
-
-Event types are used to determine what event handler should handle the event.
-
-## Student log events
-
-* `SUBMISSION <student assignment repo path>` - Student pushes to an assignment repository. Triggers test running.
-
-## Faculty log events
-
-* `UPLOAD <assignment directory>` - Faculty uploads a new assignment.
-* `UPDATE <assignment directory> <item>` - Faculty updates an assignment item.
-    * Items:
-        * `base_code`
-        * `tests`
-* `PUBLISH <assignment directory>` - Faculty requests that the assignment be published.
-* `DELETE <assignment directory>` - Faculty requests that the assignment be deleted.
-* `CLASS_ADD` <class directory>` - Faculty adds a class roster.
-* `CLASS_MODIFY` <class directory>` - Faculty modifies a class roster.
\ No newline at end of file
diff --git a/docs/develop/release_checklist.md b/docs/develop/release_checklist.md
deleted file mode 100644
index ccfcbc91..00000000
--- a/docs/develop/release_checklist.md
+++ /dev/null
@@ -1,19 +0,0 @@
-To release version x.y.z of git-keeper, follow the steps below.
-
-### GitHub
-
-* Update the `VERSION` file with the new version number.
-* Run `bump_version.py` to propagate the new version number to all
-  packages
-* Merge the newly versioned code with the `develop` branch via pull
-  request
-* Merge the `develop` branch with the `master` branch via pull request
-* Create an annotated tag on the `master` branch like so:
-  `git tag -a x.y.z -m "Version x.y.z"`
-* Push the tag directly to the master branch with `git push --tags`
-* Create a new release on GitHub from the new tag.
-
-### PyPI
-
-Follow the [official packaging documentation](https://packaging.python.org/en/latest/tutorials/packaging-projects/)
-to build and upload all 3 packages to PyPI.
diff --git a/docs/develop/server_filesystem_structure.md b/docs/develop/server_filesystem_structure.md
deleted file mode 100644
index 39a23b40..00000000
--- a/docs/develop/server_filesystem_structure.md
+++ /dev/null
@@ -1,38 +0,0 @@
-```
-/
-└── home
-    ├── faculty
-    │   ├── faculty
-    │   │   └── example_class
-    │   │       └── hw01.git
-    │   └── .gitkeeper
-    │       ├── classes
-    │       │   └── example_class
-    │       │       └── hw01
-    │       │           ├── base_code.git
-    │       │           ├── email.txt
-    │       │           ├── reports.git
-    │       │           └── tests
-    │       ├── faculty.log
-    │       ├── gkeepd.log
-    │       ├── info
-    │       │   └── 1576784041.3908958.json
-    │       └── uploads
-    │           └── 1576784036.5428793
-    │               └── students.csv
-    ├── keeper
-    │   ├── .gitconfig
-    │   ├── gkeepd.lock
-    │   ├── gkeepd.log
-    │   ├── gkeepd_db.sqlite
-    │   └── server.cfg
-    ├── student
-    │   ├── faculty
-    │   │   └── example_class
-    │   │       └── hw01.git
-    │   ├── .gitkeeper
-    │   │   └── student.log
-    │   └── git-shell-commands
-    │       └── passwd
-    └── tester
-```
\ No newline at end of file
diff --git a/docs/developers.md b/docs/developers.md
new file mode 100644
index 00000000..fea1a0a5
--- /dev/null
+++ b/docs/developers.md
@@ -0,0 +1,414 @@
+# Developers
+
+This page has information for those who would like to contribute to the
+development of git-keeper.
+
+## Project Organization
+
+There are 3 distributions in this project:
+
+* `git-keeper-client`
+* `git-keeper-core`
+* `git-keeper-server`
+
+Both `git-keeper-client` and `git-keeper-server` depend on `git-keeper-core`.
+
+## Developer Setup
+
+The preferred development environment is PyCharm on OSX or Linux.  The following steps will ensure that your development environment is isolated from the system Python and other Python projects.
+
+### Prerequisites
+
+* [Python 3.8 or greater](https://www.python.org/downloads/)
+
+### Setup Process
+
+
+* Fork the `git-keeper/git-keeper` repo to your personal GitHub account.
+* Clone the repo from your GitHub account
+* `cd git-keeper`
+* Create a virtual environment named `.env`.  The `.gitignore` file ensures that the `.env` folder is ignored.
+
+```no-highlight
+python3 -m venv .env
+```
+ 
+* Activate the virtual environment.
+
+```no-highlight
+source .env/bin/activate
+```
+ 
+* Install the `mkdocs` dependencies:
+
+```no-highlight
+pip install -r requirements.txt
+```
+ 
+* Install the project modules `git-keeper-core`, `git-keeper-client`,
+  `git-keeper-server`, and `git-keeper-robot` as editable.  This will allow the
+  IDE to recognize the `git-keeper` modules.
+
+```no-highlight
+pip install -e git-keeper-core
+pip install -e git-keeper-client
+pip install -e git-keeper-server
+pip install -e git-keeper-robot
+```
+
+* Open the project in PyCharm, and wait for it to index the packages.
+
+If you clone the repo from within PyCharm, you will have to change the project interpreter after you create the virtual environment.  Also, if you have the project open in PyCharm when you install the `git-keeper` modules, you will have to restart PyCharm before it will recognize the names (this is a known bug in PyCharm).
+
+
+## Style
+
+We strive for consistent style within this project. Contributions may not be
+accepted if they do not follow these guidelines.
+
+* Project filenames
+    * Most filenames should be all lowercase with words separated by
+    underscores
+    * Use all uppercase for files like `README.md` and `COPYING`
+* Documentation
+    * Place documentation in the `docs` directory. If you add a new file, add
+    it to the table of contents in `mkdocs.yml`.
+    * Use markdown
+    * Limit lines of text to 80 characters
+* Python
+    * Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) whenever
+    possible
+    * Variable names
+        * Avoid abbreviations unless the meaning remains clear or length gets
+        way out of hand
+            * `cls` is not ok for class
+            * `config` is ok for configuration
+        * Quantities
+            * Use `item_count` vs `num_items`
+        * Dictionaries
+            * Use `<value>s_by_<key>` (i.e. `students_by_class`)
+        * Indexes
+            * For an index into a single list, use `i`
+            * For nested loops and other situations with multiple indexes, use a
+            variable name ending in `_i` (i.e. `student_i` and `class_i`)
+        * Filenames, directory names, and full paths
+            * File and directory basenames (no path)
+                * End filename variable names with `_filename`
+                * End directory name variable names with `_dir`
+            * Full paths
+                * End the variable name with `_path`. If it is ambiguous
+                whether it is a file or directory, end it with `_file_path` or
+                `_dir_path`
+    * Type hinting
+        * Use type hints for all parameters that are object references
+    * Importing
+        * Either import an entire module (`import os`) or import specific
+        items (`from queue import Queue, Empty`) but do not import all items
+        from a module (do not say `from os import *`)
+        * Import system-level items first followed by imports that are
+        internal to the project
+
+## Acceptance Testing
+
+Most of git-keeper's features are tested using our acceptance testing suite
+which resides in `tests/acceptance`. New tests should be added when adding new
+features, and the tests should pass before new changes are merged.
+
+### Overview
+
+Our acceptance tests use two virtual machines:
+
+* `gkserver` - a server to run `gkeepd`. 
+* `gkclient` - a machine where faculty and student actions can be executed.
+
+We use Vangrant(https://www.vagrantup.com/) (backed by VirtualBox) to launch the machines and [Robot Framework](https://robotframework.org/) to write the tests.
+
+The `git-keeper-robot` package contains a library of functions to control the VMs and to implement "verbs" for Robot Framework.
+
+The `gkserver` machine has all the dependencies installed, and.  When you run `vagrant up`, Vagrant will mount `git-keeper-core` and `git-keeper-server` on within `gkserver` and then `pip install -e` both packages.  Note that this means that any changes you make to the code will be used within the VM even after this step completes.
+
+Similarly, `vagrant up` will install `git-keeper-core` and `git-keeper-client` on the `gkclient` VM, making local changes usable within the VM.
+
+The `gkserver` VM contains a mock email server that saves files to `/email` instead of actually sending email messages.
+
+### Setup
+
+Before you can run the test, you must build the VirtualBox images:
+
+* Goto `tests/acceptance/gkserver_base` and run `make_box.sh`.  This will create an image with the basic setup steps complete.  See the `Vagrantfile` in this directory for the steps.
+* Run `vagrant box add --name gkserver gkserver.box`.  This will allow Vagrant to launch the `gkserver` image.  See `tests/acceptance/Vagrantfile`.
+* Goto `tests/acceptance/gkclient_base` and run `make_box.sh`.  This will install the necessary software to run `gkeep`, and it will create a use named `keeper` (password `keeper`) that has `sudo` rights.
+* Run `vagrant box add --name gkclient gkclient.box`.  This will allow Vagrant to launch the `gkclient` image.  See `tests/acceptance/Vagrantfile`.
+
+### Running the VMs
+
+
+* Launch both VMs by running `vangrant up` in `/tests/acceptance`.  This will use `tests/acceptance/Vagrantfile` to launch both VMs and install the `git-keeper` code.
+* Destroy both VMs by running `vagrant destroy -f`
+
+
+### Running Robot Framework Tests
+
+Once you have built `gkserver` and `gkclient`, you can run the tests.  Run `robot .` to tell Robot Framework to run all tests in the current directory.
+
+When you run tests, Robot Framework has two behaviors:
+
+* If `gkserver` and `gkclient` are already up, it will use these machines for testing - and then leave them running when done.  This saves time when you have to run tests multiple times.
+* If `gkserver` and `gkclient` are not running, it will lauch both machines for testing - and then destroy them when done.
+
+In order to avoid side effects, Robot Framework will reset `gkserver` and `gkclient` before each test.  See `vmscripts/reset_server.py` and `vmscripts/reset_client.py` to see what is reset.
+
+#### Run Subset of Tests
+
+We use a `__init__.robot` file to setup the VMs, and so you have to be careful when you want to run a subset of tests.  In the following commands, note the period at the end of the commands.  This tells Robot Framework to  look in the current directory when it runs tests, and this causes it to find the `__init__.robot` file.
+
+* To run one test, use `robot -t "<test_name>" .` - For example, `robot -t "Valid Class" .`. 
+* To run one suite (file) of tests, use `robot -s <suite_name> .` - For example, `robot -s gkeepd_launch .` to run all the tests in the `gkeepd_launch.robot` file.
+
+### Manual Execution
+
+You can also use `gkserver` and `gkclient` to do manually testing.  
+
+If you want a "clean" system where you can configure and run `gkeepd`:
+
+* Run `vagrant up` in `tests/acceptance` to launch both VMs.
+* Run `vagrant ssh gkserver` to connect to the server
+* On the server, run `su - keeper` to become the keeper account.  The password is "keeper"
+* You can also connect to `gkclient`, where there is also a `keeper` account (password is "keeper").
+
+For convienence, the script `manual_configure.py` will:
+
+* Create a valid `server.cfg`
+* Create an `admin_prof` account on `gkclient` and make that account the admin of `git-keeper`
+* Create a `prof1` account on `gkclient` and make it a non-admin faculty member of `git-keeper`
+* Create a course named `cs1` for `prof1` that contains students `student1` and `student2`. 
+* Create accounts for `student1` and `student2`
+* Have `prof1` upload and publish a simple assignment
+* Have `student1` clone and submit the assignment
+* Have `prof1` fetch the submissions.
+
+To reset the system, run `manual_reset.py`.  This will:
+
+* [Server]: Stop `gkeepd`
+* [Server]: Remove all `git-keeper` files
+* [Server]: Delete all email files in `/email`
+* [Server]: Remove all users except `keeper` and `vagrant`
+* [Client]: Remove all users exept `keeper`
+
+
+### Notes
+
+* This testing system is finicky!  If you suddently get errors, try `vagrant destroy -f` and `vagrant up` first.
+* Once you get `gkserver.box` and `gkclient.box` built and added to Vagrant, you should not need to do this step again.
+* If you get an error message, "The IP address configured for the host-only network is not within the allowed ranges. Please update the address used to be within the allowed ranges and run the command again" - add the line `* 20.0.0.0/8 192.168.0.0/16` (the asterisk matters!) to `/etc/vbox/networks.conf`.  Make sure this file is world readable.  See [this StackOverFlow Question](https://stackoverflow.com/questions/70704093/the-ip-address-configured-for-the-host-only-network-is-not-within-the-allowed-ra)
+* The `tests/acceptance/vm_scripts` folder must be world readable for the tests to execute.  One one system the `umask` was set to 0077, and so all files had no group permissions allowed.  This caused the `reset_server.py` script to fail during `manual_configure.py`.  Run `umask 0022`, re-clone the repo, recreate the virtual enviroment, and then run the tests again.
+* Robot Framework masks many error messages.  If tests are failing in weird ways, run `manual_configure.py`. If this succeeds, go into `gkserver` and/or `gkclient` and run your sequence of steps manually.
+
+## Unit Testing
+
+Unit tests for classes and functions reside in `tests/unit`. These tests can be
+run by running `pytest` in the unit test directory.
+
+To add tests for a new unit, create a new file in `tests/unit` that begins with
+`test_`. Within the test file, each function that begins with `test_` is
+considered a test. See the [pytest documentation](https://docs.pytest.org/) for
+more information about writing tests for `pytest`.
+
+## Documentation
+
+Documentation is contained in the `docs` folder of the git-keeper project, is
+built using [MkDocs](https://www.mkdocs.org/), and is hosted on
+[Read the Docs](https://readthedocs.org/) at
+[https://git-keeper.readthedocs.io/](https://git-keeper.readthedocs.io/).
+
+MkDocs settings are specified in `mkdocs.yml`. Settings for Read the Docs are
+in `.readthedocs.yaml`. The versions of tools and libraries from PyPI used by
+Read the Docs are specified in `docs/requirements.txt`.
+
+Read the Docs will automatically build new docs when the `develop` and `master`
+branches are updated on GitHub. By default the `master` branch's docs are
+shown, and the `develop` branch's docs are at
+[https://git-keeper.readthedocs.io/en/develop/](https://git-keeper.readthedocs.io/en/develop/).
+
+### Viewing Docs Locally
+
+To view the docs locally before deploying, run `mkdocs serve` in the root
+directory of the project. This will print a local URL that you can visit to
+view the current docs. Editing any of the doc files will automatically reload
+the updated docs.
+
+### MkDocs Markdown
+
+The docs are written in
+[Markdown](https://daringfireball.net/projects/markdown/). MkDocs has some
+extra Markdown features, and the ones used in our docs are detailed below.
+
+#### Fenced Code Blocks
+
+Fenced code blocks begin and end with 3 backticks and create a block of text
+using a monospaced font, which may also be syntax highlighted. The language for
+syntax highlighting is automatically detected, or it can be specified with a
+string directly after the first set of backticks. To disable highlighting for a
+block, use `no-highlight` as the language. It is a good idea to disable
+highlighting for blocks that do not need it.
+
+For examples of fenced code blocks look through the raw Markdown files for the
+docs, they are used extensively.
+
+#### Admonitions
+
+A block like this:
+
+```
+!!! note
+
+    This is a note
+```
+
+Renders like this:
+
+!!! note
+
+    This is a note
+
+Other types besides `note` are `tip`, `warning`, `caution`, `error`, and
+`danger`. See here for more:
+[https://python-markdown.github.io/extensions/admonition/](https://python-markdown.github.io/extensions/admonition/)
+
+
+## Release Checklist
+
+To release version x.y.z of git-keeper, follow the steps below.
+
+### GitHub
+
+* Update the `VERSION` file with the new version number.
+* Run `bump_version.py` to propagate the new version number to all
+  packages
+* Merge the newly versioned code with the `develop` branch via pull
+  request
+* Merge the `develop` branch with the `master` branch via pull request
+* Create an annotated tag on the `master` branch like so:
+  `git tag -a x.y.z -m "Version x.y.z"`
+* Push the tag directly to the master branch with `git push --tags`
+* Create a new release on GitHub from the new tag.
+
+### PyPI
+
+Follow the [official packaging documentation](https://packaging.python.org/en/latest/tutorials/packaging-projects/)
+to build and upload all 3 packages to PyPI.
+
+## Server Filesystem Structure
+
+Below is an example filesystem layout for a git-keeper server.
+
+```no-highlight
+/
+└── home
+    ├── faculty
+    │   ├── faculty
+    │   │   └── example_class
+    │   │       └── hw01.git
+    │   └── .gitkeeper
+    │       ├── classes
+    │       │   └── example_class
+    │       │       └── hw01
+    │       │           ├── base_code.git
+    │       │           ├── email.txt
+    │       │           ├── reports.git
+    │       │           └── tests
+    │       ├── faculty.log
+    │       ├── gkeepd.log
+    │       ├── info
+    │       │   └── 1576784041.3908958.json
+    │       └── uploads
+    │           └── 1576784036.5428793
+    │               └── students.csv
+    ├── keeper
+    │   ├── .gitconfig
+    │   ├── gkeepd.lock
+    │   ├── gkeepd.log
+    │   ├── gkeepd_db.sqlite
+    │   └── server.cfg
+    ├── student
+    │   ├── faculty
+    │   │   └── example_class
+    │   │       └── hw01.git
+    │   ├── .gitkeeper
+    │   │   └── student.log
+    │   └── git-shell-commands
+    │       └── passwd
+    └── tester
+```
+
+## Log Events
+
+Each faculty and student user has a file named `<username>.log` in the
+`.gitkeeper` folder within their home directory on the server.  Requests to
+`gkeepd` are made by appending events to these log files. Each line of the log
+is an event, and is structured like this:
+
+```no-highlight
+<timestamp> <event type> <payload>
+```
+
+For example, if a student with the username `student` pushes to an assignment
+repository, there is a git hook which appends an event of the following form to
+`~student/.gitkeeper/student.log`:
+
+```no-highlight
+1463892789 SUBMISSION /home/student/faculty/class/assignment.git
+```
+
+Event types are used to determine what event handler should handle the
+event. See `gkserver/event_handlers`. The `handler_registry.py` file in that
+directory maps event types to handlers.
+
+### Log Event Responses
+
+Confirmations and error messages from `gkeepd` are also placed in log
+files. Each faculty user has a file `.gitkeeper/gkeepd.log` for this purpose.
+
+## Info JSON Structure
+
+Many client operations fetch the contents of the latest info file from the
+server in the `.gitkeeper/info` directory in the faculty's home directory. An
+info file contains information about a faculty member's classes and
+assignments. Here is the structure of an example info file:
+
+```json
+{
+   "class_name":{
+      "assignments":{
+         "assignment_name":{
+            "name":"assignment_name",
+            "published":true,
+            "reports_repo":{
+               "hash":"202585432b8ff21ff4f93a886fff9b09c46eb18e",
+               "path":"/home/faculty/classes/class_name/assignment_name/reports.git"
+            },
+            "students_repos":{
+               "alovelace":{
+                  "first":"Ada",
+                  "hash":"76f608acf4d08ccd1bf07955e2600bdce3f80774",
+                  "last":"Lovelace",
+                  "path":"/home/alovelace/faculty/class_name/assignment_name.git",
+                  "submission_count":0,
+                  "time":1476986981
+               }
+            }
+         }
+      },
+      "students":{
+         "alovelace":{
+            "email_address":"alovelace@example.edu",
+            "first":"Ada",
+            "home_dir":"/home/alovelace",
+            "last":"Lovelace",
+            "last_first_username":"lovelace_ada_alovelace",
+            "username":"alovelace"
+         }
+      }
+   }
+}
+```
diff --git a/docs/faculty-users.md b/docs/faculty-users.md
new file mode 100644
index 00000000..f8decfa6
--- /dev/null
+++ b/docs/faculty-users.md
@@ -0,0 +1,1575 @@
+# Faculty Users
+
+This page has numerous guides for faculty users. See the nagivation bar to
+explore the sections on this page.
+
+## Client Setup
+
+This guide details how to set up a faculty machine to use the git-keeper
+client. The client has been used successfully in OS X and Linux. It may work in
+Windows using the Windows Subsystem for Linux or Cygwin but that has not been
+fully tested.
+
+### Requirements
+
+The client requires [Git](https://git-scm.com/downloads) and
+[Python 3.8 or greater](https://www.python.org/downloads/).
+
+In addition, you need the username and password sent to you by the 
+`git-keeper` server.  This account must be created by the `git-keeper`
+server.  For the faculty admin, your account is created the first time
+you run `gkeepd`.  For other faculty, the faculty admin must use
+`gkeep add_faculty` to create the account.
+
+### Installing the Client
+
+To install the client system-wide, run the following command:
+
+```no-highlight
+sudo python3 -m pip install git-keeper-client
+```
+
+Alternatively, you may want to install the client in a
+[Python virtual environment](https://docs.python.org/3/tutorial/venv.html) if
+you do not want to clutter your system's Python packages.
+
+### Configuration
+
+There must be a configuration file at `~/.config/git-keeper/client.cfg`. The
+easiest way to create this file is to run `gkeep config`, which will prompt you
+for various values and then create the file.
+
+If you create `client.cfg` manually, you must include a section called
+`[server]` which defines the hostname of the server, your faculty username on
+the server, and optionally the SSH server's port number which defaults to 22.
+
+You may also include an optional section `[local]` in which you can define a
+directory into which `gkeep` will fetch submissions and the directory where
+assignment templates are stored.  If present, these *must* be an absolute path.
+
+Here is an example `client.cfg`:
+
+```
+[server]
+host = gitkeeper.myhostname.com
+username = myfacultyusername
+# optional
+ssh_port = 22
+
+# optional section
+[local]
+submissions_path = ~/submissions
+template_path = ~/gkeep_templates
+```
+
+### SSH Key
+
+The client communicates with the server over SSH, and it requires SSH keys so
+that you can make SSH connections without a password.
+
+If you have never generated a public key, do so now:
+
+```no-highlight
+ssh-keygen
+```
+
+Use the default filename, and do **NOT** set a password for the key.
+
+Next, copy your public key to your git-keeper server:
+
+```no-highlight
+ssh-copy-id <username>@<hostname>
+```
+
+### Check Your Configuration
+
+To verify that everything is configured correctly, run
+
+```no-highlight
+gkeep check
+```
+
+The output should something look like:
+
+```no-highlight
+/home/turing/.config/git-keeper/client.cfg parsed without errors
+
+Successfully communicated with gkserver
+
+Server information:
+  Version: 0.3.1
+  gkeepd uptime: 29m21s
+  Firejail installed: True
+  Docker installed: True
+```
+
+### Getting Help
+
+Run `gkeep` with no arguments to see a usage message.  Run `gkeep` with a
+subcommands and no additional arguments to see a usage message for that
+subcommand.
+
+## Creating Classes
+
+To create a class you first need to create a CSV file with the names and email
+addresses of the students in the class.  Class names are unique for each
+faculty member and cannot be changed.  We recommend you use a naming scheme
+that includes information that identifies the course and the term.  For
+example, `cs100f22`.
+
+To create a class named `cs100f22`, create a file named `cs100f22.csv`
+containing something like the following:
+
+```no-highlight
+Hamilton,Margaret,mhamilton@example.edu
+Hopper,Grace,ghopper@example.edu
+Lovelace,Ada,alovelace@example.edu
+```
+
+Now you can add the class on the server using `gkeep`:
+
+```no-highlight
+gkeep add cs100f22 cs100f22.csv
+```
+
+An Account will be created for any student in the class that does not already
+have an account on the server. The account name will be the username portion of
+the student's email address. In the case of a duplicate, a number will be added
+after the username to create a unique account.  Passwords are randomly
+generated.  An email containing the student's username and password will be
+sent to the student.
+
+!!! note
+    Faculty should save the CSV file in case they need to make changes to
+    the enrollment of the class on the `git-keeper` server.
+
+## Modifying Classes
+
+Faculty can use the `gkeep modify` command to:
+
+* Add new students to a class
+* Remove students from a class
+* Change a student's name (e.g. fix a typo)
+* Change a student's email address
+
+The system uses the email address to identify the student, so it will 
+consider a change of email address as removing the old account and
+adding a new account.
+
+Faculty can make multiple changes to the CSV file and then run `gkeep modify`
+to make all the changes at once.  The system will show you the changes and
+prompt you for confirmation.
+
+### Examples
+
+In the following examples, assume you have a class named `cs1f22` based on the 
+file `cs1f22.csv`:
+
+```no-highlight
+Hamilton,Margaret,mhamilton@example.edu
+Hopper,Grace,ghopper@example.edu
+Lovelace,Ada,alovelace@example.edu
+```
+
+#### Example: Add a New Student to a Class
+
+Edit the class CSV file to add the student information:
+
+```no-highlight
+Hamilton,Margaret,mhamilton@example.edu
+Hopper,Grace,ghopper@example.edu
+Lovelace,Ada,alovelace@example.edu
+Turing,Alan,turninga@example.edu
+```
+
+Modify the class enrollment and confirm the changes
+
+```no-highlight
+gkeep modify cs1f22 cs1f22.csv
+```
+
+The output will be:
+
+```no-highlight
+Modifying class cs1f22
+
+The following students will be added to the class:
+Turing, Alan, turninga@example.edu
+
+Proceed? (Y/n) y
+Students added successfully
+```
+
+#### Example: Remove a Student from a Class
+
+Edit the class CSV file to remove the student information (removing Grace
+Hopper in this example):
+
+```no-highlight
+Hamilton,Margaret,mhamilton@example.edu
+Lovelace,Ada,alovelace@example.edu
+```
+
+Modify the class enrollment and confirm the changes
+
+```no-highlight
+gkeep modify cs1f22 cs1f22.csv
+```
+
+The output will be:
+
+```no-highlight
+Modifying class cs1f22
+
+The following students will be removed from the class:
+Hopper, Grace, ghopper@example.edu
+
+Proceed? (Y/n) y
+Students removed successfully
+```
+
+#### Example: Change the Name of a Student
+
+Edit the class CSV file to update the student name (change "Margaret" to
+"Maggie" in this example):
+
+```no-highlight
+Modifying class cs1f22
+
+The following student names will be updated:
+Hamilton, Margaret -> Hamilton, Maggie
+
+Proceed? (Y/n) y
+Students modified successfully
+```
+
+#### Example: Change the Email Address of a Student
+
+Edit the class CSV file to update the student email address (change Ada's email
+to "alovelace@babbage.edu" in this example):
+
+Modify the class enrollment and confirm the changes:
+
+```no-highlight
+gkeep modify cs1f22 cs1f22.csv
+```
+
+The output will be:
+
+```no-highlight
+Modifying class cs1f22
+
+The following students will be added to the class:
+Lovelace, Ada, alovelace@babbage.edu
+
+The following students will be removed from the class:
+Lovelace, Ada, alovelace@example.edu
+
+Proceed? (Y/n) y
+Students added successfully
+Students removed successfully
+```
+
+A new account was created because `git-keeper` uses the email address to
+identify the student.
+
+!!! note
+    In this example, the new account on the `git-keeper` server will be
+    `alovelace1` because the username `alovelace` is associated with the
+    `alovelace@example.edu` email address.
+
+## Class Status
+
+Each class in `git-keeper` has a status, "open" or "closed."  When created a
+class it is open, and it remains open until the faculty explicitly closes it.
+To change the status of a class run the command
+
+```no-highlight
+gkeep status <class_name> <status>
+```
+
+where `<status>` is `open` or `closed`.  The system will respond with
+
+```no-highlight
+Status updated successfully
+```
+
+When a class is open, the faculty member can publish assignments and the
+students can receive results.
+
+When a class is closed, the faculty cannot upload or publish any new
+assignments.  If they run `gkeep upload` or `gkeep publish` on a closed course,
+they will receive a message, "Class <classname> is closed."
+
+If a student pushes to a repo associated with a closed course they will receive
+an email indicating that the course is closed:
+
+```no-highlight
+Subject: [cs1] class is closed
+
+You have pushed a submission for a class that is is closed.
+```
+
+## Password Reset
+
+If a student forgets their password, the faculty member can reset their
+password using
+
+```no-highlight
+gkeep reset <username> 
+```
+
+### Sample Password Reset Email
+
+```no-highlight
+Hello Sharon,
+
+Your git-keeper password has been reset to QzmPflFP
+
+If you have any questions, please contact your instructor rather than responding to this email directly.
+```
+
+## Assignments
+
+What follows is a guide to creating, uploading, and publishing assignments and
+then fetching the students' submissions.
+
+### Assignment Structure
+
+To create an assignment, first create a directory to contain the
+assignment. The name of the directory will be the name of the assignment.
+
+#### `assignment.cfg` (optional)
+
+The optional `assignment.cfg` file may be used to configure the
+testing environment, time and memory limits, and
+whether or not the results emails will use HTML. See
+[Testing Environments](#testing-environments) and
+[Assignment Configuration](#assignment-configuration) for more details.
+
+#### `base_code`
+
+Within the assignment directory there must be a directory named
+`base_code`. The contents of this directory will be the initial contents of the
+student's repository for the assignment. Put skeleton code, data files,
+instructions, etc. in this directory.
+
+#### `email.txt`
+
+There must also be a file named `email.txt`. The email that students receive
+when a new assignment is published will always contain a clone URL, and the
+contents of `email.txt` will be appended to the email after the
+URL. `email.txt` may be empty.
+
+#### `tests`
+
+A directory named `tests` is also required, which must contain either a shell
+script named `action.sh` or a Python script named `action.py`. The `tests`
+directory also contains any other code and data files that you will use to test
+student code.
+
+When a student submits an assignment to the server it creates a temporary clone
+of the student's repository and a temporary copy of the `tests` directory. With
+the temporary `tests` directory as its working directory, the testing
+environment then runs `action.sh` using `bash` or `action.py` using `python3`,
+passing the action script the path to the temporary clone of the student's
+directory.
+
+This means that before you upload the assignment you can test your tests
+locally by entering the `tests` directory in the terminal and running
+`action.sh` or `action.py`, giving it the path to a solution as a command line
+argument. It is convenient to also store a `solution` directory in the
+assignment directory for testing. This way you can run the following from
+inside the `tests` directory to test your tests against your solution:
+
+```no-highlight
+bash action.sh ../solution
+```
+
+or
+
+```no-highlight
+python3 action.py ../solution
+```
+
+The output of the action script (both stdout and stderr) are placed in the
+email that the student receives as feedback.
+
+
+### Creating Action Scripts
+
+For some assignments you can write all your tests in bash or Python and so the
+only file you need in the `tests` directory is `action.sh` or `action.py`.
+
+For other assignments you may wish to write your tests in another language or
+to use a testing framework such as JUnit to test the student code. In that case
+you can simply call your tests from `action.sh`.
+
+
+### Testing Environments
+
+The testing environment for an assignment can be specified in the optional file
+`assignment.cfg` within the assignment directory. If this file does not exist
+for a given assignment, the assignment will use the default testing
+environment. This will be `firejail`, unless specified otherwise in the
+[Server Configuration]().
+
+#### Host Environment
+
+The `host` testing environment is straightforward, but less secure than the
+other environment types, and requires that any dependencies for the tests be
+installed on the server itself.
+
+The git-keeper server creates a user named `tester`, and tests are run within a
+temporary directory in `/home/tester`. The `tester` user cannot access any
+other user home directories, but student code could potentially access other
+files within the `tester` home directory, including files for other currently
+running tests (if multiple threads are used for testing).
+
+This environment may be specified by placing the following in `assignment.cfg`:
+
+```
+[tests]
+env = host
+```
+
+#### Firejail Environment
+
+This testing environment uses [Firejail](https://firejail.wordpress.com/) for
+sandboxing. As with `host` environments, tests are run by the `tester` user
+within a temporary directory in `/home/tester`, but `firejail` is used to limit
+the process so that it can only access files within the temporary testing
+directory. Dependencies for the tests must still be installed directly on the
+server.
+
+The `firejail` package may need to be explicitly installed on the server
+before this environment can be used. It is available in the official
+repositories for many Linux distributions, including Ubuntu.
+
+By default, `firejail` is run with the following options:
+
+```no-highlight
+--noprofile --quiet --private=/home/tester/<temporary testing directory>
+```
+
+This makes `firejail` ignore any default profiles that may be installed on the
+server, supresses `firejail` output so that it is not included in the results
+email, and sandboxes the tests within the testing directory.
+
+To use the default Firejail options, place the following in `assignment.cfg`:
+
+```
+[tests]
+env = firejail
+```
+
+Additional options may be specified using the `append_args` field. For example,
+if you wish to prohibit any network activity and prevent the process from
+writing any files larger than 100 KB, you could use the following:
+
+```
+[tests]
+env = firejail
+append_args = --net=none --rlimit-fsize=102400
+```
+
+These options will be used in addition to the default options. For a full list
+of options available for `firejail`, see the
+[Firejail homepage](https://firejail.wordpress.com/) or read through the man
+page.
+
+#### Docker Environment
+
+Tests may be run in a Docker container, which can provide both sandboxing and
+encapsulation of dependencies.
+
+TODO: setting up docker on the server
+
+To use this environment, a Docker container image must be specified in
+`assignment.cfg`. For example, to use our Python 3.10 container the following
+may be used:
+
+```
+[tests]
+env = docker
+image = gitkeeper/git-keeper-tester:python3.10
+```
+
+The first time tests are run using a particular image, the server must download
+the image. This can take some time, so it is important to submit a test
+submission before publishing the assignment so that the image is downloaded to
+the server before students start submitting.
+
+TODO: Describe and link to our Docker Hub images
+
+When tests are run, the temporary testing directory is mounted as a volume
+within the container at `/git-keeper-testing`, and the `tests` directory is at
+`/git-keeper-testing/tests`. To create your own container images, you will need
+to use the `tests` directory as the working directory, and set the command to
+run either `action.sh` or `action.py` depending on your preference.
+
+For example, here is a `Dockerfile` to build an image with Python 3.10 that can
+be used with git-keeper:
+
+```dockerfile
+FROM python:3.10-slim
+
+WORKDIR /git-keeper-tester/tests
+
+CMD ["bash", "action.sh"]
+```
+
+If you want to install additional Python packages within the container, use
+`RUN` commands. For example, this installs the `pytest` and `pytest-timeout`
+packages using `pip`:
+
+```dockerfile
+FROM python:3.10-slim
+
+RUN pip install pytest pytest-timeout
+
+WORKDIR /git-keeper-tester/tests
+
+CMD ["bash", "action.sh"]
+```
+
+TODO: Describe how to test the image locally
+
+You can upload your images to Docker Hub, where they can then be accessed by
+the server. See the
+[Docker Hub documentation](https://docs.docker.com/docker-hub/) for more
+information.
+
+### Example
+
+Here is an example where the tests are written entirely in `action.sh`.
+
+Let's say you want to create an assignment for which students will write a
+Python program to print `Hello, world!`. You want them to write this program
+in an initially empty file named `hello_world.py` and for them to receive
+instructions by email.
+
+You might create an assignment structure like this:
+
+```no-highlight
+hw01-hello_world
+├── assignment.cfg
+├── base_code
+│   └── hello_world.py
+├── email.txt
+├── solution
+│   └── hello_world.py
+└── tests
+    ├── action.sh
+    └── expected_output.txt
+```
+
+`base_code/hello_world.py` can be an empty file which the students will receive
+as their starting point, and into which they will place their code. `email.txt`
+contains instructions for the assignment, `solution/hello_world.py` contains
+your solution, and `tests/expected_output.txt` contains `Hello, world!`.
+
+In `assignment.cfg` let's use `firejail` as the testing environment and set a 5
+second timeout in case the student writes an infinite loop in their code:
+
+```
+[tests]
+env = firejail
+timeout = 5
+```
+
+What's left is to write `action.sh`. You want to run the student's
+`hello_world.py` and compare the output to `expected_output.txt`. Here is a
+simple way to go about it:
+
+```bash
+# The path to the student's submission directory is in $1
+SUBMISSION_DIR=$1
+
+# Run the student's code and put the output (stdout and stderr) in output.txt
+python $SUBMISSION_DIR/hello_world.py &> output.txt
+
+# Compare the output with the expected output. Throw away the diff output
+# because we only care about diff's exit code
+diff output.txt expected_output.txt &> /dev/null
+
+# diff returns a non-zero exit code if the files were different
+if [ $? -ne 0 ]
+then
+    echo "Your program did not produce the expected output. Your output:"
+    echo
+    cat output.txt
+    echo
+    echo "Expected output:"
+    echo
+    cat expected_output.txt
+else
+    echo "Tests passed, good job!"
+fi
+
+# Always exit 0. If action.sh exits with a non-zero exit code the server sees
+# this as an error in your tests.
+exit 0
+```
+
+### Uploading an Assignment
+
+Let's assume you have created an assignment named `hw01-hello_world`, as in the
+example above, which is for the class `CS100`. You can upload the assignment to
+the server like so:
+
+```no-highlight
+gkeep upload CS100 hw01-hello_world
+```
+
+Uploading an assignment does not immediately send it to your students. If the
+assignment uploads successfully you will receive an email that looks just like
+the email that the students get when they receive the assignment. You can make
+sure the assignment works as expected by cloning it and pushing some solutions.
+
+If you discover errors in your assignment you can update it before sending it
+to the students. You can modify the any one of the three components of the
+assignment individually, or update everything. To update the entire assignment:
+
+```no-highlight
+gkeep update CS100 hw01-hello_world all
+```
+
+Run `gkeep update` without additional arguments for more info.
+
+### Publishing an Assignment
+
+Once you are satisfied with your uploaded assignment you can publish it, which
+will send it out to students:
+
+```no-highlight
+gkeep publish CS100 hw01-hello_world
+```
+
+Once an assignment is published you cannot update the base code or the email,
+but you can update the tests.
+
+### Fetching Submissions
+
+When you want to grade your students' submissions, you can fetch them with
+`gkeep fetch`. If you defined `submissions_path` in the `[local]` section of
+`client.cfg` then `gkeep` can fetch all your assignments into a common
+directory. Otherwrise you need to specify a directory to fetch to.
+
+This will fetch into `submissions_path`:
+
+```no-highlight
+gkeep fetch CS100 hw01-hello_world
+```
+
+Fetch into the current directory:
+
+```no-highlight
+gkeep fetch CS100 hw01-hello_world
+```
+
+The fetched directory for the assigment contains 2 subdirectories: `reports`
+and `submissions`. `reports` contains the text of the emails that your students
+received when they submitted. The `submissions` directory contains the code
+that your students submitted.
+
+### Assignment Templates
+
+You can use the command `gkeep new` to create the files and directories required for
+an assignment.  For example,
+
+```no-highlight
+gkeep new homework1
+```
+
+will create a folder `homework1` in the current directory with the following structure:
+
+```no-highlight
+homework1
+├── assignment.cfg
+├── base_code
+├── email.txt
+└── tests
+    └── action.sh
+```
+
+where `homework1`, `base_code`, and `tests` are folders and `assignment.cfg`, 
+`email.txt`, and `action.sh` are empty files.
+
+You can create your own template assignment in `~/.config/git-keeper/templates`
+and then add a template name to `gkeep new` to create an assignment from that
+template.
+
+For example, 
+
+```no-highlight
+gkeep new function_examples inclass 
+```
+
+Would create an assignment folder named `function_examples` using the template `inclass`.
+
+This feature is useful when you have a standard `assignment.cfg`, `email.txt`, `action.sh`, or
+other files.
+
+
+You can change the location of the templates in `client.cfg`.  In the `[local]` section, add
+`templates_path`, which must be an absolute path.  For example,
+
+```
+# ... other lines omitted
+[local]
+templates_path = ~/templates
+```
+
+## Tutorials
+
+The tutorials below will walk you through various scenarios.
+
+
+### Create your First Assignment
+
+In this tutorial you will create, test, and publish a simple assignment for an existing class.
+
+#### Prerequisites
+
+* A working `git-keeper` server
+* A faculty account on the server
+* The `gkeep` client setup on your computer
+* An active class in `git-keeper`
+
+Throughout the tutorial, we will use the class name `cs100f22`.  You should use the name of 
+your existing class.
+
+#### Create a Base Assignment from the Default Template
+
+The easiest way to create an assignment is to use the `gkeep new` command, which will create 
+a directory containing empty copies of all the required files for an assignment.  Use the following
+command to create an assignment named `hwk_even`:
+
+```no-highlight
+gkeep new hwk_even
+```
+
+An assignment in `git-keeper` is a folder where the name of the folder is the name of the assignment.  
+The folder contains:
+
+* `base_code` - a folder containing one or more files that will in the `git` repository of every student
+* `email.txt` - a file containing any information you want to include with the clone url when the
+    assignment is published
+* `assignment.cfg` (optional) - a configuration file specifying system-level details of how each 
+    student submission will be tested
+* `tests` - a folder containing any code and data used to test a student submission
+* `tests/action.sh` OR `tests/action.py` - a script that executes your tests
+
+#### Prepare the Assignment
+
+In this tutorial, we will create a simple CS1-level assignment in Python where students
+must implement a function that determines if a number is even.  The tests will use the built-in
+`unittest` framework to execute tests on the assignment.
+
+##### Base Code
+
+Create a new file `hwk_even/base_code/even.py`:
+
+```
+def is_even(number):
+    return False
+
+
+for number in range(10):
+    if is_even(number):
+        print('{} is even'.format(number))
+    else:
+        print('{} is odd'.format(number))
+```
+
+##### Tests
+
+To test a student submission we will compare the output of the student submission 
+and of a correct solution.
+
+First, create a new file `hwk_even/tests/expected.py` containing the correct solution:
+
+```
+def is_even(number):
+    return number % 2 == 0
+
+
+for number in range(10):
+    if is_even(number):
+        print('{} is even'.format(number))
+    else:
+        print('{} is odd'.format(number))
+```
+
+The `git-keeper` system runs `action.sh` to start the test process.  Edit the file 
+`hwk_even/tests/action.sh`:
+
+```
+# Redirect stderr and stdout to a file
+python3 $1/even.py > student_output.txt 2>&1
+
+# If python has a non-zero exit code, something went wrong running the student code
+if [ $? -ne 0 ]; then
+        echo "There was an error when running your code:"
+        echo "-------------------------------------------------"
+        cat student_output.txt
+        echo "-------------------------------------------------"
+        echo "Please try again"
+        exit 0
+fi
+
+# Redirect output of a correct solution
+python3 expected.py > expected_output.txt
+
+# Compare the student output with the correcdt output
+# Send output to /dev/null because we don't need it
+diff -w student_output.txt expected_output.txt >> /dev/null
+
+# diff returns non-zero when the files are not the same
+if [ $? -ne 0 ]; then
+        echo "Your output is not correct.  Expected:"
+        cat expected_output.txt
+        echo "Your output:"
+        cat student_output.txt
+        exit 0
+fi
+
+echo "Your output is correct.  Nice work!"
+cat student_output.txt
+
+# Any non-zero exit code indicates a problem with the test process
+# Make sure to have the following line at the end of all action.sh files
+exit 0
+```
+
+
+##### Email Announcement
+
+The contents of `hwk_even/email.txt` will be used in the assignment announcement email
+sent to all students.  Edit this file to contain:
+
+```no-highlight
+hwk_even: Implement a function to determine if a number is even.
+
+HINT: Use the modulus operator!
+```
+
+##### Assignment Configuration
+
+An empty file `assignment.cfg` was created by `gkeep new`, but we do not need to edit it
+for this assignment.  All possible values in this file are optional, and by leaving it
+empty the server will use default values.
+
+#### Upload the Assignment
+
+Now that we have all the files prepared for the assignment, we can upload it to the server.
+In the **parent** directory of `hwk_even`, run:
+
+```no-highlight
+gkeep upload cs100f22 hwk_even
+```
+
+The output should be
+
+```no-highlight
+uploading hwk_even in cs100f22
+Assignment uploaded successfully
+```
+
+In addition, you should have an email from `git-keeper` containing:
+
+```no-highlight
+Clone URL:
+prof1@gkserver:/home/prof1/prof1/cs100f22/hwk_even.git
+
+
+hwk_even: Implement a function to determine if a number is even.
+
+HINT: Use the modulus operator!
+```
+
+NOTE: You will see your username in place of `prof1` in the clone URL.
+
+
+#### Test the Assignment
+
+At this point the assignment is on the server, but it is **NOT** distributed to the students.
+Before we publish the assignment, let's test it to make sure the test process works as
+expected.
+
+We will use the `gkeep test` command to simulate the process of a student pushing their code to
+their repo.
+
+First we will submit a copy of the correct solution.  Create a folder named `solution` in 
+`hwk_even` and then edit the file `hwk_even/solution/even.py`:
+
+```
+def is_even(number):
+    return number % 2 == 0
+
+
+for number in range(10):
+    if is_even(number):
+        print('{} is even'.format(number))
+    else:
+        print('{} is odd'.format(number))
+```
+
+Now run the tests on this solution:
+
+```no-highlight
+gkeep test cs100f22 hwk_even hwk_even/solution
+```
+
+The output should be:
+
+```no-highlight
+Testing solution hwk_even/solution for assignment hwk_even in class cs100f22
+Pushing your solution using git
+Solution pushed successfully, you should receive a report via email
+```
+
+You should receive an email containing:
+
+```no-highlight
+Your output is correct.  Nice work!
+0 is even
+1 is odd
+2 is even
+3 is odd
+4 is even
+5 is odd
+6 is even
+7 is odd
+8 is even
+9 is odd
+```
+
+Next we will submit an incorrect solution:
+
+```no-highlight
+gkeep test cs100f22 hwk_even hwk_even/base_code
+```
+
+The output should be:
+
+```no-highlight
+Testing solution hwk_even/base_code for assignment hwk_even in class cs100f22
+Pushing your solution using git
+Solution pushed successfully, you should receive a report via email
+```
+
+The email from `git-keeper` should be:
+
+```no-highlight
+Your output is not correct.  Expected:
+0 is even
+1 is odd
+2 is even
+3 is odd
+4 is even
+5 is odd
+6 is even
+7 is odd
+8 is even
+9 is odd
+Your output:
+0 is odd
+1 is odd
+2 is odd
+3 is odd
+4 is odd
+5 is odd
+6 is odd
+7 is odd
+8 is odd
+9 is odd
+```
+
+Finally, let's submit code that throws an exception.  Create a folder `hwk_even/bad_code`
+and then edit the file `hwk_even/bad_code/even.py`:
+
+```
+def is_even(number):
+    return 0/0
+
+
+for number in range(10):
+    if is_even(number):
+        print('{} is even'.format(number))
+    else:
+        print('{} is odd'.format(number))
+```
+
+The output should be:
+
+```no-highlight
+Testing solution hwk_even/bad_code for assignment hwk_even in class cs100f22
+Pushing your solution using git
+Solution pushed successfully, you should receive a report via email
+```
+
+The email from `git-keeper` should be:
+
+```no-highlight
+There was an error when running your code:
+-------------------------------------------------
+Traceback (most recent call last):
+  File "/home/tester/hwk_even/even.py", line 7, in <module>
+    if is_even(number):
+  File "/home/tester/hwk_even/even.py", line 2, in is_even
+    return 0/0
+ZeroDivisionError: division by zero
+-------------------------------------------------
+Please try again
+```
+
+#### Publish the Assignment
+
+At this point we have demonstrated that our submission testing handles correct solutions
+as well as solutions that produce incorrect output or produce an error.  We are ready to 
+publish the assignment:
+
+```no-highlight
+gkeep publish cs100f22 hwk_even
+```
+
+The output should be:
+
+```no-highlight
+Publishing assignment hwk_even in class cs100f22
+Assignment successfully published
+```
+
+Each student will receive an announcement email:
+
+```no-highlight
+Clone URL:
+alovelace@gkserver:/home/alovelace/prof1/cs100f22/hwk_even.git
+
+
+hwk_even: Implement a function to determine if a number is even.
+
+HINT: Use the modulus operator!
+```
+
+### A Simple Assignment From the Default Template
+
+In this tutorial you will create a simple assignment and upload it to
+the `git-keeper` server.
+
+#### Prerequisites
+
+* A working `git-keeper` server
+* A faculty account on the server
+* The `gkeep` client setup on your computer
+* A class named `cs100f22` added to the server
+
+#### Overview
+
+The `gkeep new` is a fast way to create the required files and directory 
+structure for an assignment.
+
+Use the `gkeep new` command to create an assignment named `project1`.
+
+```no-highlight
+gkeep new project1
+```
+
+Edit `project1/base_code/proj1.py`:
+
+```no-highlight
+def is_valid(username):
+    return False
+```
+
+Edit `project1/email.txt`:
+
+```no-highlight
+Your first project.  See Canvas for details.
+```
+
+Edit `project1/tests/action.sh`:
+
+```no-highlight
+echo "Your submission was received"
+exit 0
+```
+
+Upload the assignment:
+
+```no-highlight
+gkeep upload cs100f22 project1
+```
+
+The output should be:
+
+```no-highlight
+uploading project1 in cs100f22
+Assignment uploaded successfully
+```
+
+And you should have an email containing:
+
+```no-highlight
+Clone URL:
+prof1@gkserver:/home/prof1/prof1/cs100f22/project1.git
+
+Your first project.  See Canvas for details.
+```
+
+(where `prof1` is your username on the `git-keeper` server).
+
+
+### Use an Assignment Template
+
+In this tutorial you will create a template and then use it to create a new assignment.
+
+#### Prerequisites
+
+* A working `git-keeper` server
+* A faculty account on the server
+* The `gkeep` client setup on your computer
+
+#### Overview
+
+The `gkeep new` command is used to create a new assignment.  By default, the command creates
+the required directories and files for a valid assignment, and then the faculty member modifies
+things to create the assignment.  But `gkeep new` can also create an assignment from a template.
+This is useful, for example, when you have a common test mechanism for a course.
+
+A template has the same structure as an assignment, but all files are optional:
+
+```no-highlight
+<template_name>
+├── assignment.cfg
+├── base_code
+├── email.txt
+└── tests
+    └── action.sh
+```
+
+By default, `gkeep new` will look in `~/.config/git-keeper/templates` for templates unless
+the `~/.config/git-keeper/client.cfg` file contains
+
+```
+[local]
+template_path = <absolute path>
+```
+
+in which case `gkeep new` will look in `<absolute path>`.
+
+#### Create a Template
+
+Faculty often use `git-keeper` to distribute code for in-class examples.  In this case, the 
+email announcement is typically minimal, and we do not expect students to submit a solution.
+We can create an assignment template containing an `email.txt` with the default message
+and an `action.sh` that simply reminds students they do not need to submit.
+
+Create the following folders and files
+
+1. Create the folder `~/.config/git-keeper/templates/inclass/base_code`
+2. Create the file `~/.config/git-keeper/templates/inclass/email.txt`:
+    ```no-highlight
+    This repo contains an in-class example.
+    ```
+3. Create the file `~/.config/git-keeper/templates/tests/action.sh`:
+    ```
+    echo 'No need to push in-class examples.
+    exit 0
+    ```
+   
+We do not need an `assignment.cfg` because this file is optional and all the default 
+values are appropriate.
+
+#### Use the template
+
+Go to a directory where you want to create an assignment and then run:
+
+```no-highlight
+gkeep new example1 inclass
+```
+
+The system will create a folder named `example1` that contains the files in 
+`~/.config/git-keeper/templates/inclass`.  You can add your code to `base_code`
+and then upload and publish the assignment.
+
+
+### Update an Assignment
+
+In this tutorial, you will update an existing assignment. 
+
+#### Prerequisites
+
+* A working `git-keeper` server
+* A faculty account on the server
+* The `gkeep` client setup on your computer
+* A class named `cs100f22` added to the server
+* The [`project1` example assignment](new_assignment.md) created and uploaded (but not published)
+
+#### Overview
+
+The `gkeep update` command is used to modify an existing assignment.  *Before* an assignment is 
+published, `gkeep update` can be used to modify any aspect of an assignment (base code, email, 
+tests, configuration).  Once an assignment is published, each student has a copy of the base code
+repo and has received the announcement email, and therefore only the tests and configuration
+may be changed with `gkeep update`.
+
+
+#### Example: Update the Email Before Publish
+
+Edit `project1/email.txt`:
+
+```no-highlight
+Your first project is to implement a function to validate
+a username.  See Canvas for details.
+```
+
+Update the assignment:
+
+```no-highlight
+gkeep update cs100f22 project1 email
+```
+
+The output should be:
+
+```no-highlight
+updating project1 in cs100f22
+Assignment updated successfully
+```
+
+And you should have an email containing:
+
+```no-highlight
+Clone URL:
+prof1@gkserver:/home/prof1/prof1/cs100f22/project1.git
+
+Your first project is to implement a function to validate
+a username.  See Canvas for details.
+```
+
+#### Example: Update Base Code Before Publish
+
+Edit `project1/base_code/proj1.py`:
+
+```
+def is_valid(username):
+    """
+    A username is valid if it contains only lowercase
+    letters, numbers, and the underscore character.
+    In addition, it must start with a letter.
+    """
+    return False
+```
+
+Update the assignment:
+
+```no-highlight
+gkeep update cs100f22 project1 base_code
+```
+
+The output should be:
+
+```no-highlight
+updating project1 in cs100f22
+Assignment updated successfully
+```
+
+And you should have an email containing:
+
+```no-highlight
+Clone URL:
+prof1@gkserver:/home/prof1/prof1/cs100f22/project1.git
+
+Your first project is to implement a function to validate
+a username.  See Canvas for details.
+```
+
+In addition, if you clone the repo you will find a *single* commit that 
+contains the base code.  This ensures that students only see the final
+version of the assignment when they clone the repo.
+
+#### Other Updates
+
+Before you publish an assignment, you can update the `tests` or `config`
+in a similar way to the previous examples.
+
+If you need to update multiple aspects of an assignment, modify all the files
+and then run
+
+```no-highlight
+gkeep update cs100f22 project1 all
+```
+
+#### Example: Base Code Cannot be Updated After Publish
+
+First, publish the assignment
+
+```no-highlight
+gkeep publish cs100f22 project1
+```
+
+The output should be: 
+
+```no-highlight
+Publishing assignment project1 in class cs100f22
+Assignment successfully published
+```
+
+Now edit `project1/base_code`:
+
+```
+# Name:
+def is_valid(username):
+    """
+    A username is valid if it contains only lowercase
+    letters, numbers, and the underscore character.
+    In addition, it must start with a letter.
+    """
+    return False
+```
+
+And attempt to update:
+
+```no-highlight
+gkeep update cs100f22 project1 base_code
+```
+
+The output should be:
+
+```no-highlight
+Assignment is already published, only tests or config may be updated.
+```
+
+You cannot update the base code of an assignment after it is published.  The publish
+action creates a copy of the repo in each student's account, and they may have cloned
+their repo.  Making a change to the student repo now could lead to `git` merge 
+conflicts.
+
+If you publish an assignment and later have to change the code, you will have to 
+distribute these changes **outside** of `git-keeper` (for example, post an announcement
+on your LMS).
+
+#### Example: Update Tests After Publish
+
+Edit `project1/tests/test_proj1.py`:
+
+```
+from proj1 import is_valid
+
+def verify_result(case, expected):
+    result = is_valid(case)
+    if result == expected:
+        print('GOOD: is_valid({}) returns {}'.format(case, expected))
+    else:
+        print('ERROR: is_valid({}) should return {}'.format(case, expected))
+
+print('Here are the results from a few test cases:')
+
+verify_result('username', True)
+verify_result('Username', False)
+verify_result('user_name', True)
+verify_result('_username', False)
+
+print('"GOOD" for all cases does not guarantee a correct solution.')
+print('When grading I will test additional cases!')
+```
+
+Edit `project1/tests/action.sh`:
+
+```
+export PYTHONPATH=$1
+python3 test_proj1.py
+exit 0
+```
+
+Update the assignment:
+
+```no-highlight
+gkeep update cs100f22 project2 tests
+```
+
+The output should be:
+
+```no-highlight
+updating project2 in cs100f22
+Assignment updated successfully
+```
+
+Keep in mind that students may have already submitted, and `git-keeper` **WILL NOT** 
+automatically rerun tests.  You can use the `gkeep trigger` command to manually run
+tests.
+
+
+### Trigger Tests for One or More Students
+
+In this tutorial you will trigger tests for one or more students.
+
+#### Prerequisites
+
+* A working `git-keeper` server
+* A faculty account on the server
+* The `gkeep` client setup on your computer
+* A class named `cs100f22` added to the server
+* The [`project1` example assignment](new_assignment.md) created, uploaded, and published
+
+
+#### Overview
+
+The `gkeep trigger` command causes the `action.sh` or `action.py` script of an assignment
+to be executed for the current repos of one or more students.  This is useful if a
+student deletes the `git-keeper` response email or if you change the tests for an
+assignment after it is published.
+
+#### Trigger Tests For the Entire Class
+
+To trigger tests for all students in the class:
+
+```no-highlight
+gkeep trigger cs100f22 project1
+```
+
+The output will ask for confirmation:
+
+```no-highlight
+Triggering tests for project1 in class cs100f22 for the following students:
+mhamilton
+ghopper
+alovelace
+Proceed? (Y/n) y
+Tests triggered successfully
+```
+
+The `action.sh` will run with each student's repo regardless of whether they have pushed
+changes.
+
+#### Trigger Tests for a Subset of Students
+
+To trigger the tests for a subset of students
+
+```no-highlight
+gkeep trigger cs100f22 project1 ghopper alovelace
+```
+
+The output will ask for confirmation:
+
+```no-highlight
+Triggering tests for project1 in class cs100f22 for the following students:
+ghopper
+alovelace
+Proceed? (Y/n) y
+Tests triggered successfully
+```
+
+### Delete or Disable an Assignment
+
+In this tutorial, you will delete an unpublished assignment and disable a
+published assignment.
+
+#### Prerequisites
+
+* A working `git-keeper` server
+* A faculty account on the server
+* The `gkeep` client setup on your computer
+* A class named `cs100f22` added to the server
+* The [`project1` example assignment](new_assignment.md) created and uploaded (but not published)
+
+#### Overview
+
+Before you publish an assignment, it is safe to delete it from the server because it only exists 
+in your account.   After you publish an assignment, you can no longer delete it because each
+student in the class has a copy of the repo (and they may have already cloned that repo).  In 
+this case your only option is to disable the assignment.
+
+#### Example: Delete an Unpublished Assignment
+
+At this point, the `project1` assignment is uploaded but not published.  The repo exists
+in your faculty account, but it has not been copied to the students' accounts.  Therefore,
+it is safe to delete the assignment:
+
+```no-highlight
+gkeep delete cs100f22 project1
+```
+
+The output asks for confirmation:
+
+```no-highlight
+Deleting assignment project3 in class cs100f22
+Proceed? (Y/n) y
+Assignment deleted successfully
+```
+
+#### Example: Disable a Published Assignment
+
+If you completed the first example, re-upload the assignment:
+
+```no-highlight
+gkeep upload cs100f22 project3
+```
+
+The output should be:
+
+```no-highlight
+uploading project3 in cs100f22
+Assignment uploaded successfully
+```
+
+Regardless of whether you completed the previous example, publish the assignment:
+
+```no-highlight
+gkeep publish cs100f22 project1
+```
+
+The output should be:
+
+```no-highlight
+Publishing assignment project3 in class cs100f22
+Assignment successfully published
+```
+
+Now try to delete the assignment:
+
+```no-highlight
+gkeep delete cs100f22 project1
+```
+
+The output should be:
+
+```no-highlight
+Assignment project1 is published and cannot be deleted.
+Use gkeep disable if you wish to disable this assignment.
+```
+
+You cannot delete a published assignment because the students already received their
+announcement emails, and they may have cloned the assignment.
+
+Instead, you must disable the assignment:
+
+```no-highlight
+gkeep disable cs100f22 project1
+```
+
+The output asks for confirmation:
+
+```no-highlight
+Disabling assignment projet1 in class cs100f22. This cannot be undone.
+Students will be notified that the assignment has been disabled.
+The action.sh/action.py script will no longer be run on submissions
+to this assignment.  Instead, students will receive an email stating
+that the assignment is disabled.
+Proceed? (Y/n) y
+Assignment disabled successfully
+```
+
+The students in the class receive an email:
+
+```no-highlight
+Assignment project1 in class cs100f22 has been disabled. No tests will be run if you push to your repository for this assignment.
+```
+
+If the student pushes to the repo, they will receive an email:
+
+```no-highlight
+Assignment project1 in class cs100f22 has been disabled.
+```
diff --git a/docs/faculty/assignment-workflow.md b/docs/faculty/assignment-workflow.md
deleted file mode 100644
index 69ba2a23..00000000
--- a/docs/faculty/assignment-workflow.md
+++ /dev/null
@@ -1,301 +0,0 @@
-What follows is a guide to creating, uploading, and publishing assignments and
-then fetching the students' submissions.
-
-## Assignment Structure
-
-To create an assignment, first create a directory to contain the
-assignment. The name of the directory will be the name of the assignment.
-
-### `assignment.cfg` (optional)
-
-Optionally, a testing environment may be specified in the file
-`test_env.yaml`. Tests may be run directly on the server (the default), within
-a [Firejail](https://firejail.wordpress.com/) sandbox, or within a
-[Docker](https://www.docker.com/) container. See
-[Testing Environments](testing-environments.md) for more details.
-
-### `base_code`
-
-Within the assignment directory there must be a directory named
-`base_code`. The contents of this directory will be the initial contents of the
-student's repository for the assignment. Put skeleton code, data files,
-instructions, etc. in this directory.
-
-### `email.txt`
-
-There must also be a file named `email.txt`. The email that students receive
-when a new assignment is published will always contain a clone URL, and the
-contents of `email.txt` will be appended to the email after the
-URL. `email.txt` may be empty.
-
-### `tests`
-
-A directory named `tests` is also required, which must contain either a shell
-script named `action.sh` or a Python script named `action.py`. The `tests`
-directory also contains any other code and data files that you will use to test
-student code.
-
-When a student submits an assignment to the server it creates a temporary clone
-of the student's repository and and a temporary copy of the tests
-directory. With the temporary `tests` directory as its working directory, the
-testing environment then runs `action.sh` using `bash` or `action.py` using
-`python3`, passing the action script the path to the temporary clone of the
-student's directory.
-
-This means that before you upload the assignment you can test your tests
-locally by entering the `tests` directory in the terminal and running
-`action.sh` or `action.py`, giving it the path to a solution as a command line
-argument. It is convenient to also store a `solution` directory in the
-assignment directory for testing. This way you can run the following from
-inside the `tests` directory to test your tests against your solution:
-
-```
-bash action.sh ../solution
-```
-
-or
-
-```
-python3 action.py ../solution
-```
-
-The output of the action script (both stdout and stderr) are placed in the
-email that the student receives as feedback.
-
-
-## Assignment Templates
-
-You can use the command `gkeep new` to create the files and directories required for
-an assignment.  For example,
-
-```
-gkeep new homework1
-```
-
-will create a folder `homework1` in the current directory with the following structure:
-
-```
-homework1
-├── assignment.cfg
-├── base_code
-├── email.txt
-└── tests
-    └── action.sh
-```
-
-where `homework1`, `base_code`, and `tests` are folders and `assignment.cfg`, 
-`email.txt`, and `action.sh` are empty files.
-
-You can create your own template assignment in `~/.config/git-keeper/templates`
-and then add a template name to `gkeep new` to create an assignment from that
-template.
-
-For example, 
-
-```
-gkeep new function_examples inclass 
-```
-
-Would create an assignment folder named `function_examples` using the template `inclass`.
-
-This feature is useful when you have a standard `assignment.cfg`, `email.txt`, `action.sh`, or
-other files.
-
-
-You can change the location of the templates in `client.cfg`.  In the `[local]` section, add
-`templates_path`, which must be an absolute path.  For example,
-
-```
-# ... other lines omitted
-[local]
-templates_path = ~/templates
-```
-
-## Creating `action.sh`
-
-
-For some assignments you can write all your tests in bash or Python and so the
-action script is the only file you need in the `tests` directory.
-
-For other assignments you may wish to write your tests in another language or
-to use a testing framework such as JUnit to test the student code. In that case
-you can simply call your tests from `action.sh`.
-
-## Example
-
-Here is an example where the tests are written entirely in `action.sh` and
-tests are run directly on the server.
-
-Let's say you want to create an assignment for which students will write a
-Python program to print `"Hello, world!"`. You want them to write this program
-in an initially empty file named `hello_world.py` and for them to receive
-instructions by email.
-
-You might create an assignment structure like this:
-
-```
-hw01-hello_world
-├── base_code
-│   └── hello_world.py
-├── email.txt
-├── solution
-│   └── hello_world.py
-└── tests
-    ├── action.sh
-    └── expected_output.txt
-```
-
-where `base_code/hello_world.py` is an empty file, `email.txt` contains
-instructions for the assignment, `solution/hello_world.py` contains your
-solution, and `tests/expected_output.txt` contains `"Hello, world!"`
-
-What's left is to write `action.sh`. You want to run the student's
-`hello_world.py` and compare the output to `expected_output.txt`. Here is a
-simple way to go about it:
-
-```bash
-# The path to the student's submission directory is in $1
-SUBMISSION_DIR=$1
-
-# Run the student's code and put the output (stdout and stderr) in output.txt
-python $SUBMISSION_DIR/hello_world.py &> output.txt
-
-# Compare the output with the expected output. Throw away the diff output
-# because we only care about diff's exit code
-diff output.txt expected_output.txt &> /dev/null
-
-# diff returns a non-zero exit code if the files were different
-if [ $? -ne 0 ]
-then
-    echo "Your program did not produce the expected output. Your output:"
-    echo
-    cat output.txt
-    echo
-    echo "Expected output:"
-    echo
-    cat expected_output.txt
-else
-    echo "Tests passed, good job!"
-fi
-
-# Always exit 0. If action.sh exits with a non-zero exit code the server sees
-# this as an error in your tests.
-exit 0
-```
-
-### Time and Memory Limits
-
-Student code containing infinite loops is troublesome. Let's add the use of
-`timeout` and `ulimit` to the example to catch infinite loops or excessive
-memory use:
-
-```bash
-# Use a 10-second timeout
-TIMEOUT=10
-# Limit memory use to 400 MB
-MEM_LIMIT_MB=400
-
-# ulimit uses KB
-MEM_LIMIT_KB=$(($MEM_LIMIT_MB * 1024))
-
-# Set the memory limit
-ulimit -v $MEM_LIMIT_KB
-
-# The path to the student's submission directory is in $1
-SUBMISSION_DIR=$1
-
-# Run the student's code and put the output (stdout and stderr) in output.txt.
-# If execution takes more than 10 seconds the process will be killed
-timeout $TIMEOUT python $SUBMISSION_DIR/hello_world.py &> output.txt
-
-# timeout returns an exit code of 124 on a timeout, otherwise it returns the
-# exit code of the process it ran
-if [ $? -eq 124 ]
-then
-    echo "Your program ran for too long. Perhaps you have an infinite loop?"
-    exit 0
-fi
-
-# Compare the output with the expected output. Throw away the diff output
-# because we only care about diff's exit code
-diff output.txt expected_output.txt &> /dev/null
-
-# diff returns a non-zero exit code if the files were different
-if [ $? -ne 0 ]
-then
-    echo "Your program did not produce the expected output. Your output:"
-    echo
-    cat output.txt
-    echo
-    echo "Expected output:"
-    echo
-    cat expected_output.txt
-else
-    echo "Tests passed, good job!"
-fi
-
-# Always exit 0. If action.sh exits with a non-zero exit code the server sees
-# this as an error in your tests.
-exit 0
-```
-
-## Uploading an Assignment
-
-Let's assume you have created an assignment named `hw01-hello_world`, as in the
-example above, which is for the class `CS100`. You can upload the assignment to
-the server like so:
-
-```
-gkeep upload CS100 hw01-hello_world
-```
-
-Uploading an assignment does not immediately send it to your students. If the
-assignment uploads successfully you will receive an email that looks just like
-the email that the students get when they receive the assignment. You can make
-sure the assignment works as expected by cloning it and pushing some solutions.
-
-If you discover errors in your assignment you can update it before sending it
-to the students. You can modify the any one of the three components of the
-assignment individually, or update everything. To update the entire assignment:
-
-```
-gkeep update CS100 hw01-hello_world all
-```
-
-Run `gkeep update` without additional arguments for more info.
-
-## Publishing an Assignment
-
-Once you are satisfied with your uploaded assignment you can publish it, which
-will send it out to students:
-
-```
-gkeep publish CS100 hw01-hello_world
-```
-
-Once an assignment is published you cannot update the base code or the email,
-but you can update the tests.
-
-## Fetching Submissions
-
-When you want to grade your students' submissions, you can fetch them with
-`gkeep fetch`. If you defined `submissions_path` in the `[local]` section of
-`client.cfg` then `gkeep` can fetch all your assignments into a common
-directory. Otherwrise you need to specify a directory to fetch to.
-
-This will fetch into `submissions_path`:
-
-```
-gkeep fetch CS100 hw01-hello_world
-```
-
-Fetch into the current directory:
-
-```
-gkeep fetch CS100 hw01-hello_world
-```
-
-The fetched directory for the assigment contains 2 subdirectories: `reports`
-and `submissions`. `reports` contains the text of the emails that your students
-received when they submitted. The `submissions` directory contains the code
-that your students submitted.
diff --git a/docs/faculty/class-status.md b/docs/faculty/class-status.md
deleted file mode 100644
index a4c29b4e..00000000
--- a/docs/faculty/class-status.md
+++ /dev/null
@@ -1,31 +0,0 @@
-
-Each class in `git-keeper` has a status, "open" or "closed."  When created a class it is open,
-and it remains open until the faculty explicitly closes it.  To change the status of a class run
-the command
-
-```
-gkeep status <class_name> <status>
-```
-
-where `<status>` is `open` or `closed`.  The system will respond with
-
-```
-Status updated successfully
-```
-
-When a class is open, the faculty member can publish assignments and the students can
-receive results.
-
-When a class is closed, the faculty cannot upload or publish any new assignments.  If
-they run `gkeep upload` or `gkeep publish` on a closed course, they will receive a message,
-"Class <classname> is closed."
-
-If a student pushes to a repo associated with a closed course they will receive an email
-indicating that the course is closed:
-
-```
-Subject: [cs1] class is closed
-
-You have pushed a submission for a class that is is closed.
-```
-
diff --git a/docs/faculty/client-setup.md b/docs/faculty/client-setup.md
deleted file mode 100644
index 851c8c8c..00000000
--- a/docs/faculty/client-setup.md
+++ /dev/null
@@ -1,104 +0,0 @@
-# Client Setup
-
-This guide details how to set up a faculty machine to use the git-keeper
-client. The client has been used successfully in OS X and Linux. It may work in
-Windows using Cygwin or the Windows Subsystem for Linux but that has not been
-fully tested.
-
-## Requirements
-
-The client requires [Git](https://git-scm.com/downloads) and [
-Python 3.8 or greater](https://www.python.org/downloads/).
-
-In addition, you need the username and password sent to you by the 
-`git-keeper` server.  This account must be created by the `git-keeper`
-server.  For the faculty admin, your account is created the first time
-you run `gkeepd`.  For other faculty, the faculty admin must use
-`gkeep add_faculty` to create the account.
-
-## Installing the Client
-
-To install the client system-wide, run the following command:
-
-```
-sudo python3 -m pip install git-keeper-client
-```
-
-Alternatively, you may want to install the client in a
-[Python virtual environment](https://docs.python.org/3/tutorial/venv.html) if
-you do not want to clutter your system's Python packages.
-
-## Configuration
-
-There must be a configuration file at `~/.config/git-keeper/client.cfg`. The easiest
-way to create this file is to run `gkeep config`, which will prompt you for various
-values and then create the file.
-
-If you create `client.cfg` manually, you must include a section called `[server]` which 
-defines the hostname of the server, your faculty username on the server, and optionally the SSH
-server's port number which defaults to 22.
-
-You may also include an optional section `[local]` in which you can define a directory
-into which `gkeep` will fetch submissions and the directory where assignment templates
-are stored.  If present, these *must* be an absolute path.
-
-Here is an example `client.cfg`:
-
-```
-[server]
-host = gitkeeper.myhostname.com
-username = myfacultyusername
-# optional
-ssh_port = 22
-
-# optional section
-[local]
-submissions_path = ~/submissions
-template_path = ~/gkeep_templates
-```
-
-## SSH Key
-
-The client communicates with the server over SSH, and it requires SSH keys so that you 
-can make SSH connections without a password.
-
-If you have never generated a public key, do so now:
-
-```
-ssh-keygen
-```
-
-Use the default filename, and do **NOT** set a password for the key.
-
-Next, copy your public key to your git-keeper server:
-
-```
-ssh-copy-id <username>@<hostname>
-```
-
-## Check Your Configuration
-
-To verify that everything is configured correctly, run
-
-```
-gkeep check
-```
-
-The output should something look like:
-
-```
-/home/turing/.config/git-keeper/client.cfg parsed without errors
-
-Successfully communicated with gkserver
-
-Server information:
-  Version: 0.3.1
-  gkeepd uptime: 29m21s
-  Firejail installed: True
-  Docker installed: True
-```
-
-## Getting Help
-
-Run `gkeep` with no arguments to see a usage message.  Run `gkeep` with a subcommands
-and no additional arguments to see a usage message for that subcommand.
diff --git a/docs/faculty/create-class.md b/docs/faculty/create-class.md
deleted file mode 100644
index d00473ca..00000000
--- a/docs/faculty/create-class.md
+++ /dev/null
@@ -1,28 +0,0 @@
-To create a class you first need to create a CSV file with the names and email
-addresses of the students in the class.  Class names are unique for each faculty
-member and cannot be changed.  We recommend you use a naming scheme that includes
-information that identifies the course and the term.  For example, `cs100f22`.
-
-To create a class named `cs100f22`, create a file named `cs100f22.csv` containing
-something like the following:
-
-```
-Hamilton,Margaret,mhamilton@example.edu
-Hopper,Grace,ghopper@example.edu
-Lovelace,Ada,alovelace@example.edu
-```
-
-Now you can add the class on the server using `gkeep`:
-
-```
-gkeep add cs100f22 cs100f22.csv
-```
-
-An Account will be created for any student in the class that does not already
-have an account on the server. The account name will be the username portion of
-the student's email address. In the case of a duplicate, a number will be added 
-after the username to create a unique account.  Passwords are randomly generated. 
-An email containing the student's username and password will be sent to the student.
-
-NOTE: Faculty should save the CSV file in case they need to make changes to
-the enrollment of the class on the `git-keeper` server.
diff --git a/docs/faculty/modify_class.md b/docs/faculty/modify_class.md
deleted file mode 100644
index 6b4eb989..00000000
--- a/docs/faculty/modify_class.md
+++ /dev/null
@@ -1,129 +0,0 @@
-
-Faculty can use the `gkeep modify` command to:
-
-* Add new students to a class
-* Remove students from a class
-* Change a student's name (e.g. fix a typo)
-* Change a student's email address
-
-The system uses the email address to identify the student, so it will 
-consider a change of email address as removing the old account and
-adding a new account.
-
-Faculty can make multiple changes to the CSV file and then run `gkeep modify`
-to make all the changes at once.  The system will show you the changes and prompt 
-you for confirmation.
-
-## Examples
-
-In the following examples, assume you have a class named `cs1f22` based on the 
-file `cs1f22.csv`:
-
-```
-Hamilton,Margaret,mhamilton@example.edu
-Hopper,Grace,ghopper@example.edu
-Lovelace,Ada,alovelace@example.edu
-```
-
-### Example: Add a New Student to a Class
-
-Edit the class CSV file to add the student information:
-
-```
-Hamilton,Margaret,mhamilton@example.edu
-Hopper,Grace,ghopper@example.edu
-Lovelace,Ada,alovelace@example.edu
-Turing,Alan,turninga@example.edu
-```
-
-Modify the class enrollment and confirm the changes
-
-```
-gkeep modify cs1f22 cs1f22.csv
-```
-
-The output will be:
-
-```
-Modifying class cs1f22
-
-The following students will be added to the class:
-Turing, Alan, turninga@example.edu
-
-Proceed? (Y/n) y
-Students added successfully
-```
-
-### Example: Remove a Student from a Class
-
-Edit the class CSV file to remove the student information (removing Grace Hopper in this example):
-
-```
-Hamilton,Margaret,mhamilton@example.edu
-Lovelace,Ada,alovelace@example.edu
-```
-
-Modify the class enrollment and confirm the changes
-
-```
-gkeep modify cs1f22 cs1f22.csv
-```
-
-The output will be:
-
-```
-Modifying class cs1f22
-
-The following students will be removed from the class:
-Hopper, Grace, ghopper@example.edu
-
-Proceed? (Y/n) y
-Students removed successfully
-```
-
-### Example: Change the Name of a Student
-
-Edit the class CSV file to update the student name (change "Margaret" to "Maggie" in this example):
-
-```
-Modifying class cs1f22
-
-The following student names will be updated:
-Hamilton, Margaret -> Hamilton, Maggie
-
-Proceed? (Y/n) y
-Students modified successfully
-```
-
-### Example: Change the Email Address of a Student
-
-Edit the class CSV file to update the student email address (change Ada's email 
-to "alovelace@babbage.edu" in this example):
-
-Modify the class enrollment and confirm the changes:
-
-```
-gkeep modify cs1f22 cs1f22.csv
-```
-
-The output will be:
-
-```
-Modifying class cs1f22
-
-The following students will be added to the class:
-Lovelace, Ada, alovelace@babbage.edu
-
-The following students will be removed from the class:
-Lovelace, Ada, alovelace@example.edu
-
-Proceed? (Y/n) y
-Students added successfully
-Students removed successfully
-```
-
-A new account was created because `git-keeper` uses the email address to identify the student.
-
-NOTE: In this example, the new account on the `git-keeper` server will be `alovelace1` because
-the username `alovelace` is associated with the `alovelace@example.edu` email address.
-
diff --git a/docs/faculty/password_reset.md b/docs/faculty/password_reset.md
deleted file mode 100644
index 7e41947b..00000000
--- a/docs/faculty/password_reset.md
+++ /dev/null
@@ -1,19 +0,0 @@
-
-## Password Reset
-
-If a student forgets their password, the faculty member can reset their
-password using
-
-```
-gkeep reset <username> 
-```
-
-## Sample Password Reset Email
-
-```
-Hello Sharon,
-
-Your git-keeper password has been reset to QzmPflFP
-
-If you have any questions, please contact your instructor rather than responding to this email directly.
-```
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index 8e6c829b..d7738952 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,19 +1,30 @@
+# Welcome to the git-keeper Documentation
 
-## Admin
+Git-keeper is an automated code-testing tool where students obtain and submit
+assignments using Git.  Unlike other systems that require students to use a web
+portal or other external system, students interact with git-keeper using
+version control, and therefore are introduced to a skill that transfers to
+real-world projects.  When a student pushes their work to the server,
+git-keeper automatically tests the submission and sends the student an email
+containing the results.
 
-* [Server Setup](admin/server-setup.md)
+Instructors interact with the system using a command-line tool to upload
+assignments to the server and to fetch student submissions for grading.  When
+assessing an assignment, git-keeper can utilize any language or tool that
+executes via the command line on a Linux system, making git-keeper adaptable to
+a wide range of classes and technologies.  In addition, git-keeper requires
+minimal dependencies and is easy to configure and administer on a local server.
 
-## Faculty
+## Getting Started
 
-* [Client Setup](faculty/client-setup.md)
-* [Create a Class](faculty/create-class.md)
-* [Assignment Workflow](faculty/assignment-workflow.md)
+The navigation bar on the left provides quick access to different sections of
+the documentation.
 
-## Developers
-
-* [General Guidelines](develop/developers.md)
-* [Release Checklist](develop/release_checklist.md)
-* [`ActionRunner` Interface](develop/actionrunner-interface.md)
-* [JSON Structure](develop/info-json-structure.md)
-* [Log Structure](develop/log-events.md)
-* [Server File Structure](develop/server_filesystem_structure.md)
+* Instructors who wish to start using git-keeper with an existing server should
+  visit [Faculty Users](faculty-users.md)
+* System administrators wishing to set up a new git-keeper server should visit
+  [Admin Users](admin-users.md)
+* Developers who wish to contribute to git-keeper should visit
+  [Developers](developers.md)
+* The [Reference](reference.md) section provides additional details beyond
+  general user guides
diff --git a/docs/reference/client.md b/docs/reference.md
similarity index 54%
rename from docs/reference/client.md
rename to docs/reference.md
index ee9413f4..86ce2cbe 100644
--- a/docs/reference/client.md
+++ b/docs/reference.md
@@ -1,7 +1,251 @@
+# Reference
+
+Below is reference documentation for numerous git-keeper topics.
+
+## User Accounts
+
+The two main categories of users that are created by `gkeepd` on the server
+are *faculty users* and *student users*. Additionally there are two
+special users used by `gkeepd`: `tester` and `keeper`.
+
+### The `keeper` User
+
+The `keeper` user is the user that runs `gkeepd`. This user is created manually
+during [Server Setup](). The `keeper` user has full `sudo` privileges and
+creates all of the other accounts on the server.
+
+### The `tester` User
+
+The `tester` user is used for running tests. It has no `sudo` permissions, and
+cannot access any other users' home directories. The `tester` user is created
+automatically when running `gkeepd` for the first time.
+
+### Faculty Users
+
+A faculty user is created by an admin faculty user, and a new faculty user will
+receive an email with the username and password for their account.
+
+Passwords are randomly generated, but can be changed by using SSH to connect to
+the server and running `passwd`.
+
+ If possible, the username will match the user's email username. See
+[Account Usernames](#account-usernames) for more on usernames.
+
+All facutly users can create and modify classes and assignments using the
+[`gkeep` client](#client). *Admin* faculty users can create additional faculty
+users using [`gkeep add_faculty`](#add_faculty), promote other faculty users to
+admin using [`gkeep admin_promote`](#admin_promote), and demote other faculty
+users from admin using [`gkeep admin_demote`](#admin_demote). The first admin
+user is defined in the [Server Configuration](#server-configuration).
+
+!!! warning
+
+    Facutly users can log into their accounts on the server normally using SSH,
+    but should refrain from manually modifying any of the files in their home
+    directories that are managed by `gkeepd`.
+
+If a facutly user is added to a class owned by another faculty user, then they
+become both a faculty and student user, without the [Git shell](#git-shell)
+restrictions placed on normal student users.
+
+### Student Users
+
+When a faculty user adds students to a class with
+[`gkeep add`](reference.md#add) or [`gkeep modify`](reference.md#modify), the
+`git-keeper` server creates an account for any student who does not already
+have an account.  When account is made, the system will email the student an
+email containing their username and password.
+
+Usernames are generally the username of the email, will make modifications for
+duplicate usernames or email usernames with inappropriate characters. See
+[Account Usernames](#account-usernames) for more information.
+
+Passwords are randomly generated for each student.
+
+If a student already has an account when added to a class, no email is
+generated.
+
+#### Sample email
+
+```no-highlight
+From: git-keeper@gitkeeper.edu
+Subject: New git-keeper account
+
+Hello Sharon,
+
+An account has been created for you on the git-keeper server. Here are your credentials:
+
+Username: colemans
+Password: 1ZEZfx3x
+
+If you have any questions, please contact your instructor rather than responding to this email directly.
+
+Enjoy!
+```
+
+#### Student Server Access
+
+Student accounts on the `git-keeper` server are not intended to support normal
+interactive sessions. If a student connects to the server using SSH, their
+account is configured to use the
+[git-shell](https://git-scm.com/docs/git-shell), which will only allow them to
+use `passwd` to change their password.
+
+#### Student SSH Keys
+
+Most IDEs allow users to store login credentials for remote `git` servers.
+However, if students are using the command line Git interface they may wish to
+create an SSH key and copy it to the `git-keeper` server so that they do not
+need to enter a password when cloning and pushing. Run
+
+```no-highlight
+ssh-keygen
+```
+
+Use the default filename, and do **NOT** set a password for the key.
+
+Next, copy your public key to your git-keeper server:
+
+```no-highlight
+ssh-copy-id <username>@<hostname>
+```
+
+### Account Usernames
+
+The username of a new user is based on the user's email address. If the
+username portion of a new user's email address is not a valid Linux username,
+normalization is performed to create a username with characters omitted, or
+substituted with approximations. If a user is added with an email address whose
+username portion matches that of another user (say `jsmith@xavier.edu` is added
+when `jsmith@moravian.edu` already exists), then a number is added at the end
+of the username to make it unique.
+
+## Assignments
+
+Assignments are distributed to students via emails which contain Git clone URLs
+which the students use to obtain a Git repository in which to do their work and
+optional additional text. Faculty users create tests to go with each assignment
+which will be run when students push assignment submissions back to the
+server. Students receive tests results via email.
+
+There are numerous [`gkeep` commands]() for working with assignments.
+
+### Structure
+
+Faculty users define assignments in a directory with the following structure:
+
+```no-highlight
+assignment_name
+├── assignment.cfg (optional)
+├── base_code
+│   └── (starting code, README, etc. go here)
+├── email.txt
+└── tests
+    ├── action.sh or action.py
+    └── (additional files to support tests go here)
+```
+
+#### Assignment Names
+
+The name of the assignment is defined by the name of the directory. Assignment
+names may only contain characters `A-Z`, `a-z`, `0-9`, `-`, and `_`. The word
+`all` is reserved and may not be used as an assignment name.
+
+#### Assignment Configuration
+
+The optional `assignment.cfg` file can be used to customize the testing
+environment and the emails sent to students. It must use Python's
+[INI File Structure](https://docs.python.org/3/library/configparser.html#supported-ini-file-structure)
+and may contain the sections `[tests]` and/or `[email]`.
+
+Below is an example `assignment.cfg`:
+
+```
+[tests]
+env = firejail
+timeout = 15
+memory_limit = 512
+
+[email]
+use_html = false
+announcement_subject = [{class_name}] New assignment: {assignment_name}
+results_subject = [{class_name}] {assignment_name} submission test results
+```
+
+##### Tests Configuration
+
+The `env` field defines the type of testing environment, which must be `host`,
+`firejail`, or `docker`. See [Testing Environments](testing-environments.md)
+for more details.
+
+The `timeout` field specifies a timeout in seconds. If the timeout is exceeded
+during testing, testing halts and the student receives an email that there was
+a timeout. Using this setting overrides the timeout value in the
+[Server Configuration]().
+
+The `memory_limit` field specifies a memory limit in megabytes. If the memory
+limit is exceeded during testing, testing halts and the student receives an
+email that something went wrong during testing. Using this setting overrides
+the memory limit defined in the [Server Configuration]().
+
+##### Email Configuration
+
+The `use_html` option specifies whether or not to use HTML in test results
+emails to display the text in a monospace font. This overrides the value set in
+the [Server Configuration]().
+
+The `announcement_subject` option can be used to specify a custom subject line
+for new assignment announcement emails. The strings `{class_name}` and
+`{assignment_name}` will be replaced by the class name and assignment name.
+
+The `results_subject` option works similarly to specify a custom subject line
+for test results emails for the assignment.
+
+#### Base Code
+
+The contents of the required `base_code` directory will be used to create
+initial Git repositories for each student in the class. Since Git repositories
+cannot be empty, this directory must contain at least one file, which can be
+empty.
+
+#### Email Text
+
+The text within the required `email.txt` file will be appended to the new
+assignment announcement email after the clone URL. This file may be empty.
+
+#### Tests
+
+The `tests` directory must contain an action script (either `action.sh` or
+`action.py`) which will be run when a student pushes a submission. The tests
+directory may contain any number of additional files and directories to support
+testing. The action script is passed a path to a clone of the student's
+submission repository as its first argument, so that the action script may
+access the student files.
+
+The output of running the action script (both standard output and standard
+error) is placed in the email sent to the student, and in the reports
+repository for the assignment.
+
+## Classes
+
+A class is owned by a single faculty user, and consists of a collection of
+students and assignments.
+
+A new class is created using the [`gkeep add`]() command, and the roster can be
+modified using [`gkeep modify`]().
+
+A class name may only contain the characters `A-Z`, `a-z`, `0-9`, `-`, and `_`.
+
+The status of a class may be `open` or `closed`. If a student tries to submit
+to a closed class, tests will not run and they will receive an email that the
+class is closed.
+
+## Client
+
 Faculty users interact with the git-keeper server using the `gkeep` client
 application. For information about installing the client, see [Client Setup]().
 
-# Configuration
+### Client Configuration
 
 The default configuration file path for `gkeep` is
 `~/.config/git-keeper/client.cfg`. This file can be created using the
@@ -12,7 +256,7 @@ The configuration file is read using Python's `ConfigParser`, and as such must
 be written using the corresponding
 [INI File Structure](https://docs.python.org/3/library/configparser.html#supported-ini-file-structure).
 
-## Server Section
+#### Server Section
 
 The `[server]` section is required, and has the following fields:
 
@@ -20,7 +264,7 @@ The `[server]` section is required, and has the following fields:
 * `username` (required): the user's account username
 * `ssh_port` (optional, defaults to 22): the SSH port of the server
 
-## Local Section
+#### Local Section
 
 The `[local]` section is optional and has the following fields:
 
@@ -31,7 +275,7 @@ The `[local]` section is optional and has the following fields:
 * `templates_path` (optional, defaults to `~/.config/git-keeper/templates`): A
   path to a directory containing assignment templates to use with `gkeep new`
 
-## Class Aliases Section
+#### Class Aliases Section
 
 The `[class_aliases]` section is optional, and is used to define aliases for
 class names. For example, the following section defines the alias `100` for the
@@ -45,12 +289,12 @@ class `cs100f22`, allowing `100` to be used in place of `cs100f22` whenever
 
 Multiple aliases may be defined in this section.
 
-# gkeep Commands
+### gkeep Commands
 
 `gkeep` has numerous sub-commands to carry out various actions for faculty
 users.
 
-## check
+#### check
 
 Checks that `gkeep` is properly configured and can communicate with the
 server. Parses the configuration file, connects to the server, and retrieves
@@ -58,7 +302,7 @@ information about the server.
 
 Usage: `gkeep check`
 
-## add
+#### add
 
 Adds a new class on the server.
 
@@ -69,7 +313,7 @@ Usage: `gkeep add <class name> [<csv filename>]`
   `Last,First,email@school.edu`. If omitted, an empty class will be created and
   students can be added later.
 
-## modify
+#### modify
 
 Modifies an existing class on the server. If students are added after
 assignments have been published, the new students will receive new assignment
@@ -81,7 +325,7 @@ Usage: `gkeep modify <class name> <csv filename>`
 * `<csv filename>`: Path to the updated roster, in the same format as for
   `gkeep add`
 
-## new
+#### new
 
 Creates a directory containing base files for a new assignment. By default
 empty files and directories will be created, but the user can create
@@ -95,7 +339,7 @@ Usage: `gkeep new <path to assignment folder> [<template name>]`
   will be used. If provided, specifies the name of a template in the
   [Templates Directory]().
 
-## upload
+#### upload
 
 Uploads a new assignment to the server. The name of the directory containing
 the assignment files will be the name of the assignment. See
@@ -112,7 +356,7 @@ Usage: `gkeep upload <class name> <assignment directory>`
   files. See [Assignment Directories]() for details about the required
   structure.
 
-## update
+#### update
 
 Updates an existing assignment on the server. If the assignment is already
 published, only a subset of the assignment's items may be updated. The faculty
@@ -137,7 +381,7 @@ Usage: `gkeep update <class name> <assignment directory> <update item>`
     * `all`: Update all of the above. This may only be updated for unpublished
     assignments.
 
-## publish
+#### publish
 
 Publish an uploaded assignment. Each student in the class will receive an email
 with a clone URL for the assignment and the contents of the assignment's
@@ -149,7 +393,7 @@ Usage: `gkeep publish <class name> <assignment name>`
 * `<assignment name>`: The name of the assignment, or a path to a directory
   whose name matches the assignment name
 
-## delete
+#### delete
 
 Delete an assignment. The assignment must not be published. Published
 assignments may be disable with `gkeep disable` instead.
@@ -160,7 +404,7 @@ Usage: `gkeep delete <class name> <assignment name>`
 * `<assignment name>`: The name of the assignment, or a path to a directory
   whose name matches the assignment name
 
-## disable
+#### disable
 
 Disable a published assignment. If students attempt to submit to a disabled
 assignment, no tests will be run. Instead they will receive an email that the
@@ -172,7 +416,7 @@ Usage: `gkeep disable <class name> <assignment name>`
 * `<assignment name>`: The name of the assignment, or a path to a directory
   whose name matches the assignment name
 
-## fetch
+#### fetch
 
 Fetch student submissions and test results from the server. Submission data
 will be placed in a directory with the same name as the assignment, and will
@@ -198,7 +442,7 @@ Usage: `gkeep fetch <class name> <assignment name> [<destination>]`
   it will either be set to the current working directory or to the
   `submissions_path` specified in the [Client Configuration]()
 
-## query
+#### query
 
 Query the server for data regarding the faculty user's classes, assignments,
 and students.
@@ -287,7 +531,7 @@ Usage: `gkeep query [--json] <query type> [<number of days>]`
 * `<number of days>`: Optional number to specify the number of days to consider
   recent. Defaults to 1
 
-## trigger
+#### trigger
 
 Trigger tests to be run on the latest commits in submission repositories for
 a list of specified students or all students in the class. This can be used to
@@ -303,7 +547,7 @@ Usage: `gkeep trigger <class name> <assignment name> [<student username> ...]`
 * `<student username>`: Optional username or list of user names to trigger
   tests for. If omitted, tests will be triggered for all students in the class.
 
-## passwd
+#### passwd
 
 Reset the password of a student user. A new password will be randomly generated
 and emailed to the student.
@@ -312,7 +556,7 @@ Usage: `gkeep passwd <username>`
 
 * `<username>`: Username of the user whose password will be reset
 
-## test
+#### test
 
 Submits a solution to the server to be tested in the same manner that student
 submissions are tested, and results are sent via email. This can be used to
@@ -326,14 +570,14 @@ Usage: `gkeep test <class name> <assignment name> <solution path>`
 * `<solution path>`: The path to a directory containing a solution for the
   assignment
 
-## config
+#### config
 
 Writes a `gkeep` configuration file to `~/.config/git-keeper/config.cfg`. The
 user is prompted to enter values for the various fields.
 
 Usage: `gkeep config`
 
-## status
+#### status
 
 Changes the status of a class to `open` or `closed`. If a class is closed,
 tests will not be run if a student pushes a new submission, and the student
@@ -345,7 +589,7 @@ Usage: `gkeep status <class name> <class status>`
 * `<class name>`: Name of the class to open or close
 * `<class status>`: New status for the class, either `open` or `closed`
 
-## add_faculty
+#### add_faculty
 
 Adds a new faculty user. The user running this command must be an admin
 user. An account will be created for the user, and the username will be based
@@ -358,7 +602,7 @@ Usage: `gkeep add_faculty <last name> <first name> <email address>`
 * `<first name>`: First name of the new faculty user
 * `<email address>`: Email address of the new faculty user
 
-## admin_promote
+#### admin_promote
 
 Promotes an existing faculty user to an admin user. The user running this
 command must be an admin user. The first admin user must be specified in the
@@ -368,7 +612,7 @@ Usage: `gkeep admin_promote <email address>`
 
 * `<email address>`: Email address of the facutly user to promote
 
-## admin_demote
+#### admin_demote
 
 Remove admin privileges for a user.
 
@@ -376,7 +620,7 @@ Usage: `gkeep admin_demote <email address>`
 
 * `<email address>`: Email address of the facutly user to demote
 
-# New Assignment Templates
+### New Assignment Templates
 
 The [`gkeep new`]() command can create a directory structure with empty files
 as a skeleton for a new assignment, or it can copy a custom template directory
@@ -387,7 +631,7 @@ setting the `templates_path` option in the [Client Configuration]().
 Note that `gkeep new` does not perform any checks on the template to ensure the
 proper structure, it simply performs a copy of the template directory.
 
-# Fetched Submissions
+### Fetched Submissions
 
 The [`gkeep fetch`]() command can fetch student submission data to a specified
 location. If the location is omitted, then submission data is fetched either
@@ -409,7 +653,7 @@ student has submitted since the last fetch.
 Here is an example structure for feched data in an assignment named
 `hello_world` for a class with 2 students that have each submitted once:
 
-```
+```no-highlight
 hello_world
 ├── reports
 │   ├── hamilton_margaret_mhamilton
@@ -434,7 +678,7 @@ stored at the assignment directory level which contains the Git hashes of the
 
 The full structure including hidden files and directories is below:
 
-```
+```no-highlight
 hello_world
 ├── .hash_cache
 ├── reports
@@ -458,3 +702,111 @@ hello_world
         └── hello_world.py
 ```
 
+## Server
+
+The server side of git-keeper is implemented by the `gkeepd` application. See
+the [Server Setup]() guide for a walk-through of how to set up the server.
+
+### Dependencies
+
+`gkeepd`, must run on a Linux system. It is very highly recommended that the
+Linux system be dedicated to running `gkeepd` and nothing else, since `gkeepd`
+needs to create accounts on the machine and manage files in users' home
+directories.
+
+The system must be able to send emails, so access to an SMTP server is
+required.
+
+`gkeepd` also requires Git and Python 3.8 or higher. By default, the Firejail
+sandboxing tool is used to run tests. This setting can be changed, but it is
+highly recommended that Firejail be installed for added security. Docker may
+also be used as a testing environment.
+
+### Server Configuration
+
+`gkeepd` is configured with the `server.cfg` file in the `keeper` user's home
+directory. Details about all sections and fields are below. There is a
+[template `server.cfg`](admin-users.md#template-servercfg) in the
+[Admin Users](#admin-users) guide.
+
+ `server.cfg` has four sections: `[server]`, `[email]`, `[admin]`,
+and `[gkeepd]`.
+
+#### `[server]`
+
+The `[server]` section has a single required parameter `hostname`, which is the
+hostname of the server. This will be used to build Git URLs.
+
+#### `[email]`
+
+The `[email]` section is also required. The following parameters are required:
+
+```
+from_name = <name that the emails will be from>
+from_address = <email address that the emails come from>
+smtp_server = <hostname of the SMTP server>
+smtp_port = <SMTP server port>
+```
+
+The following parameters are optional:
+
+```
+use_tls = <true or false, defaults to true>
+email_username = <username for the SMTP server>
+email_password = <password for the SMTP server>
+email_interval = <seconds to wait between sending emails>
+use_html = <true or false, defaults to false>
+```
+
+If `use_html` is true, submission test results will be sent as an HTML email,
+placing the contents within pre tags so that they appear in a fixed width font.
+
+#### `[admin]`
+
+The `[admin]` section is also required. The section defines an admin user. The
+admin user will automatically be added as a faculty user when the server first
+starts, and then the admin user will be able to add additional faculty members.
+
+The following parameters are required:
+
+```
+admin_email = <email address of the admin user>
+admin_first_name = <first name of the admin user>
+admin_last_name = <last name of the admin user>
+```
+
+#### `[gkeepd]`
+
+The `[gkeepd]` section is optional. Below are the allowed parameters and their
+defaults:
+
+```
+test_thread_count = 1
+tests_timeout = 300
+tests_memory_limit = 1024
+default_test_env = firejail
+```
+
+The `test_thread_count` parameter specifies how many threads will be used to
+run student tests. Multiple threads will allow multiple tests to be run
+simultaneously. Be sure to set the `tests_memory_limit` appropriately based on
+the number of test threads and the memory available on the system.
+
+The `tests_timeout` parameter specifies a global timeout for tests in case an
+assignment's tests fail to properly account for infinite loops. The default is
+300 seconds. If this timeout occurs the student's test results will state that
+the tests timed out.
+
+Similar to `tests_timeout`, the `tests_memory_limit` sets a global memory limit
+for tests. The default limit is 1024 MB. If the memory limit is exceeded, tests
+will be halted and the student and faculty users will receive emails that there
+was an error running the tests.
+
+The `default_test_env` parameters specifies the test environment that will be
+used if an assignment has not defined a test environment in
+`assignment.cfg`. The default is `firejail`, but this can also be set to `host`
+if Firejail is not available on your server.
+
+!!! warning
+
+    Using `host` as the default test environment is potentially insecure.
diff --git a/docs/reference/accounts.md b/docs/reference/accounts.md
deleted file mode 100644
index eee309e4..00000000
--- a/docs/reference/accounts.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# User Accounts
-
-The two main categories of users that are created by `gkeepd` on the server
-are *faculty users* and *student users*. Additionally there are two
-special users used by `gkeepd`: `tester` and `keeper`.
-
-## The `keeper` User
-
-The `keeper` user is the user that runs `gkeepd`. This user is created manually
-during [Server Setup](). The `keeper` user has full `sudo` privileges and
-creates all of the other accounts on the server.
-
-## The `tester` User
-
-The `tester` user is used for running tests. It has no `sudo` permissions, and
-cannot access any other users' home directories. The `tester` user is created
-automatically when running `gkeepd` for the first time.
-
-## Faculty Users
-
-A faculty user is created by an admin faculty user, and a new faculty user will
-receive an email with the username and password for their account.
-
-All facutly users can create and modify classes and assignments using the
-[`gkeep` client](). *Admin* faculty users can create additional faculty users
-using [`gkeep add_faculty`](), promote other faculty users to admin using
-[`gkeep admin_promote`](), and demote other faculty users from admin using
-[`gkeep admin_demote`](). The first admin user is defined in the
-[Server Configuration]().
-
-Facutly users can log into their accounts on the server normally using SSH, but
-should refrain from manually modifying any of the files in their home
-directories that are managed by `gkeepd`.
-
-If a facutly user is added to a class owned by another faculty user, then they
-become both a faculty and student user, without the [Git shell]() restrictions
-placed on normal student users.
-
-## Student Users
-
-A student user is created by `gkeepd` when a class is added or modified to
-contain a student that does not yet have an account. A new student user will
-receive an email with the username and password for their account.
-
-### Git Shell
-
-Students can use SSH to log into their accounts on the server, but the Git
-shell limits them to only being able to run the `passwd` command.
-
-If a student user becomes a faculty user, this restriction is lifted.
-
-## Account Usernames
-
-The username of a new user is based on the user's email address. If the
-username portion of a new user's email address is not a valid Linux username,
-normalization is performed to create a username with characters omitted, or
-substituted with approximations. If a user is added with an email address whose
-username portion matches that of another user (say `jsmith@xavier.edu` is added
-when `jsmith@moravian.edu` already exists), then a number is added at the end
-of the username to make it unique.
diff --git a/docs/reference/assignments.md b/docs/reference/assignments.md
deleted file mode 100644
index 27947778..00000000
--- a/docs/reference/assignments.md
+++ /dev/null
@@ -1,105 +0,0 @@
-# Assignments
-
-Assignments are distributed to students via emails which contain Git clone URLs
-which the students use to obtain a Git repository in which to do their work and
-optional additional text. Faculty users create tests to go with each assignment
-which will be run when students push assignment submissions back to the
-server. Students receive tests results via email.
-
-There are numerous [`gkeep` commands]() for working with assignments.
-
-## Structure
-
-Faculty users define assignments in a directory with the following structure:
-
-```
-assignment_name
-├── assignment.cfg (optional)
-├── base_code
-│   └── (starting code, README, etc. go here)
-├── email.txt
-└── tests
-    ├── action.sh or action.py
-    └── (additional files to support tests go here)
-```
-
-### Assignment Names
-
-The name of the assignment is defined by the name of the directory. Assignment
-names may only contain characters `A-Z`, `a-z`, `0-9`, `-`, and `_`. The word
-`all` is reserved and may not be used as an assignment name.
-
-### Assignment Configuration
-
-The optional `assignment.cfg` file can be used to customize the testing
-environment and the emails sent to students. It must use Python's
-[INI File Structure](https://docs.python.org/3/library/configparser.html#supported-ini-file-structure)
-and may contain the sections `[tests]` and/or `[email]`.
-
-Below is an example `assignment.cfg`:
-
-```
-[tests]
-env: firejail
-timeout: 15
-memory_limit: 512
-
-[email]
-use_html: false
-announcement_subject: [{class_name}] New assignment: {assignment_name}
-results_subject: [{class_name}] {assignment_name} submission test results
-```
-
-#### Tests Configuration
-
-The `env` field defines the type of testing environment, which must be `host`,
-`firejail`, or `docker`. See [Testing Environments](testing-environments.md)
-for more details.
-
-The `timeout` field specifies a timeout in seconds. If the timeout is exceeded
-during testing, testing halts and the student receives an email that there was
-a timeout. Using this setting overrides the timeout value in the
-[Server Configuration]().
-
-The `memory_limit` field specifies a memory limit in megabytes. If the memory
-limit is exceeded during testing, testing halts and the student receives an
-email that something went wrong during testing. Using this setting overrides
-the memory limit defined in the [Server Configuration]().
-
-#### Email Configuration
-
-The `use_html` option specifies whether or not to use HTML in test results
-emails to display the text in a monospace font. This overrides the value set in
-the [Server Configuration]().
-
-The `announcement_subject` option can be used to specify a custom subject line
-for new assignment announcement emails. The strings `{class_name}` and
-`{assignment_name}` will be replaced by the class name and assignment name.
-
-The `results_subject` option works similarly to specify a custom subject line
-for test results emails for the assignment.
-
-### Base Code
-
-The contents of the required `base_code` directory will be used to create
-initial Git repositories for each student in the class. Since Git repositories
-cannot be empty, this directory must contain at least one file, which can be
-empty.
-
-### Email Text
-
-The text within the required `email.txt` file will be appended to the new
-assignment announcement email after the clone URL. This file may be empty.
-
-### Tests
-
-The `tests` directory must contain an action script (either `action.sh` or
-`action.py`) which will be run when a student pushes a submission. The tests
-directory may contain any number of additional files and directories to support
-testing. The action script is passed a path to a clone of the student's
-submission repository as its first argument, so that the action script may
-access the student files.
-
-The output of running the action script (both standard output and standard
-error) is placed in the email sent to the student, and in the reports
-repository for the assignment.
diff --git a/docs/reference/classes.md b/docs/reference/classes.md
deleted file mode 100644
index 9df8063a..00000000
--- a/docs/reference/classes.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Classes
-
-A class is owned by a single faculty user, and consists of a collection of
-students and assignments.
-
-A new class is created using the [`gkeep add`]() command, and the roster can be
-modified using [`gkeep modify`]().
-
-A class name may only contain the characters `A-Z`, `a-z`, `0-9`, `-`, and `_`.
-
-The status of a class may be `open` or `closed`. If a student tries to submit
-to a closed class, tests will not run and they will receive an email that the
-class is closed.
diff --git a/docs/reference/server.md b/docs/reference/server.md
deleted file mode 100644
index 99e0b1cf..00000000
--- a/docs/reference/server.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Server
-
-The server side of git-keeper is implemented by the `gkeepd` application. See
-the [Server Setup]() guide for a walk-through of how to set up the server.
-
-## Dependencies
-
-`gkeepd`, must run on a Linux system. It is very highly recommended that the
-Linux system be dedicated to running `gkeepd` and nothing else, since `gkeepd`
-needs to create accounts on the machine and manage files in users' home
-directories.
-
-The system must be able to send emails, so access to an SMTP server is
-required.
-
-`gkeepd` also requires Git and Python 3.8 or higher. By default, the Firejail
-sandboxing tool is used to run tests. This setting can be changed, but it is
-highly recommended that Firejail be installed for added security. Docker may
-also be used as a testing environment.
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 00000000..4ff3e741
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,16 @@
+click==8.1.3
+ghp-import==2.1.0
+importlib-metadata==4.12.0
+Jinja2==3.0.3
+Markdown==3.3.7
+MarkupSafe==2.1.1
+mergedeep==1.3.4
+mkdocs==1.3.1
+packaging==21.3
+pyparsing==3.0.9
+python-dateutil==2.8.2
+PyYAML==6.0
+pyyaml_env_tag==0.1
+six==1.16.0
+watchdog==2.1.9
+zipp==3.8.1
diff --git a/docs/student/student-setup.md b/docs/student/student-setup.md
deleted file mode 100644
index a246683f..00000000
--- a/docs/student/student-setup.md
+++ /dev/null
@@ -1,59 +0,0 @@
-
-## Account Creation 
-
-When a faculty uses `gkeep add` to create a new class, the `git-keeper`
-server creates an account for any student who does not already have an 
-account.  When account is made, the system will email the student an email
-containing their username and password.
-
-Usernames are generally the username of the email, but if a duplicate
-occurs (e.g. two students with the same username on different email 
-servers), the system will append a digit to the end of the second
-student to ensure distinct usernames.
-
-Passwords are randomly generated for each student.
-
-If a student already has an account when `gkeep add` is run, no
-email is generated.
-
-## Sample email
-
-```
-From: git-keeper@gitkeeper.edu
-Subject: New git-keeper account
-
-Hello Sharon,
-
-An account has been created for you on the git-keeper server. Here are your credentials:
-
-Username: colemans
-Password: 1ZEZfx3x
-
-If you have any questions, please contact your instructor rather than responding to this email directly.
-
-Enjoy!
-```
-
-## Student Server Access
-
-Student accounts on the `git-keeper` server are not intended to support normal interactive 
-sessions. If a student connects to the server using SSH, their account is configured to use the 
-[git-shell](https://git-scm.com/docs/git-shell), 
-which will only allow them use `passwd` to change their password.
-
-### Student SSH Keys
-
-Most IDEs allow users to store login credentials for remote `git` servers.  If students wish,
-they can create an SSH key and copy it to the `git-keeper` server.  Run
-
-```
-ssh-keygen
-```
-
-Use the default filename, and do **NOT** set a password for the key.
-
-Next, copy your public key to your git-keeper server:
-
-```
-ssh-copy-id <username>@<hostname>
-```
diff --git a/git-keeper-client/gkeepclient/version.py b/git-keeper-client/gkeepclient/version.py
index 998fc9dc..e0d29079 100644
--- a/git-keeper-client/gkeepclient/version.py
+++ b/git-keeper-client/gkeepclient/version.py
@@ -2,4 +2,4 @@
 # Instead, edit the VERSION file in the top level directory of the git-keeper
 # project and then run bump_version.py.
 
-__version__ = '1.0.0'
+__version__ = '1.0.1'
diff --git a/git-keeper-core/gkeepcore/valid_names.py b/git-keeper-core/gkeepcore/valid_names.py
index 3fd8bf4a..6a232789 100644
--- a/git-keeper-core/gkeepcore/valid_names.py
+++ b/git-keeper-core/gkeepcore/valid_names.py
@@ -32,7 +32,7 @@ def validate_assignment_name(assignment_name: str):
 
     :param assignment_name: name of the assignment
     """
-    blacklist = ['all']
+    blacklist = ['all', 'tests']
 
     if assignment_name in blacklist:
         error = ('"{0}" is not an allowed assignment name'
diff --git a/git-keeper-core/gkeepcore/version.py b/git-keeper-core/gkeepcore/version.py
index 998fc9dc..e0d29079 100644
--- a/git-keeper-core/gkeepcore/version.py
+++ b/git-keeper-core/gkeepcore/version.py
@@ -2,4 +2,4 @@
 # Instead, edit the VERSION file in the top level directory of the git-keeper
 # project and then run bump_version.py.
 
-__version__ = '1.0.0'
+__version__ = '1.0.1'
diff --git a/git-keeper-server/gkeepserver/check_system.py b/git-keeper-server/gkeepserver/check_system.py
index 87db2fad..0e8104d5 100644
--- a/git-keeper-server/gkeepserver/check_system.py
+++ b/git-keeper-server/gkeepserver/check_system.py
@@ -57,7 +57,7 @@ def check_system():
 
     check_paths_and_permissions()
     check_dummy_accounts()
-    check_faculty()
+    check_admin()
 
 
 def check_paths_and_permissions():
@@ -166,17 +166,29 @@ def write_gitconfig():
     gkeepd_logger.log_info('Created {0}'.format(config.gitconfig_file_path))
 
 
-def check_faculty():
-    """Add any new faculty members."""
+def check_admin():
+    """
+    Check the admin user in the server configuration.
+
+    If no user exists with the admin email address, add a new faculty admin
+    user. If a faculty user exists with the admin email address, promote the
+    user to admin. If a student user exists with the admin email address, make
+    the existing student user a faculty admin user.
+    """
 
     gkeepd_logger.log_debug('Checking faculty')
 
-    if not db.faculty_username_exists(config.admin_username):
+    if not db.email_exists(config.admin_email):
         add_faculty(config.admin_last_name, config.admin_first_name,
                     config.admin_email, admin=True)
-
-    elif not db.is_admin(config.admin_username):
-        db.set_admin(config.admin_username)
+    else:
+        username = db.get_username_from_email(config.admin_email)
+        if not db.faculty_username_exists(username):
+            add_faculty(config.admin_last_name, config.admin_first_name,
+                        config.admin_email, admin=True)
+        elif not db.is_admin(username):
+            gkeepd_logger.log_info('Making user {} an admin'.format(username))
+            db.set_admin(username)
 
 
 def check_dummy_accounts():
diff --git a/git-keeper-server/gkeepserver/server_configuration.py b/git-keeper-server/gkeepserver/server_configuration.py
index 6fc23469..2f2b349f 100644
--- a/git-keeper-server/gkeepserver/server_configuration.py
+++ b/git-keeper-server/gkeepserver/server_configuration.py
@@ -227,8 +227,6 @@ def _initialize_default_attributes(self):
         self.admin_first_name = None
         self.admin_last_name = None
 
-        self.admin_username = None
-
     def _parse_config_file(self):
         # Use a ConfigParser object to parse the configuration file and store
         # the state
@@ -335,7 +333,7 @@ def _set_admin_options(self):
             raise ServerConfigurationError(e.message)
 
         try:
-            self.admin_username, _ = self.admin_email.split('@')
+            _, _ = self.admin_email.split('@')
         except ValueError:
             raise ServerConfigurationError('{} is not a valid email address'
                                            .format(self.admin_email))
diff --git a/git-keeper-server/gkeepserver/submission.py b/git-keeper-server/gkeepserver/submission.py
index b2020be2..3450200e 100644
--- a/git-keeper-server/gkeepserver/submission.py
+++ b/git-keeper-server/gkeepserver/submission.py
@@ -34,13 +34,33 @@
     git_checkout
 from gkeepcore.assignment_config import AssignmentConfig, TestEnv
 from gkeepcore.system_commands import cp, sudo_chown, rm, chmod, mv
-from gkeepcore.shell_command import run_command, CommandError, CommandExitCodeError
+from gkeepcore.shell_command import run_command, CommandExitCodeError
 from gkeepserver.email_sender_thread import email_sender
 from gkeepserver.info_update_thread import info_updater
 from gkeepserver.reports import reports_clone
 from gkeepserver.server_configuration import config
 from gkeepserver.server_email import Email
-from gkeepcore.path_utils import parse_submission_repo_path, user_home_dir
+from gkeepcore.path_utils import user_home_dir
+
+
+class TempPaths:
+    """
+    Represents the various paths in a temporary testing directory.
+
+    Attributes:
+
+    temp_path - path to the temporary directory storing all items
+    submission_path - path to the clone of the student's submission
+    run_action_sh_path - path to run_action.sh that will be called ot initiate
+                         tests
+    assignment_cfg_path - path to assignment.cfg, which may or may not exist
+    """
+
+    def __init__(self, temp_path, assignment_name):
+        self.temp_path = temp_path
+        self.submission_path = os.path.join(temp_path, assignment_name)
+        self.run_action_sh_path = os.path.join(temp_path, 'run_action.sh')
+        self.assignment_cfg_path = os.path.join(temp_path, 'assignment.cfg')
 
 
 class Submission:
@@ -88,81 +108,37 @@ def run_tests(self):
         run the tests.
         """
 
-        faculty_username, class_name, assignment_name = \
-            parse_submission_repo_path(self.student_repo_path)
-
-        if not db.class_is_open(class_name, faculty_username):
-            self._send_closed_email(class_name, assignment_name)
+        if not db.class_is_open(self.class_name, self.faculty_username):
+            self._send_closed_email()
             return
 
-        if db.is_disabled(class_name, assignment_name, faculty_username):
-            self._send_disabled_email(class_name, assignment_name)
+        if db.is_disabled(self.class_name, self.assignment_name,
+                          self.faculty_username):
+            self._send_disabled_email()
             return
 
         logger.log_debug('Running tests on {0}'.format(self.student_repo_path))
 
-        temp_path = mkdtemp(dir=user_home_dir(config.tester_user),
-                            prefix='{}_'.format(int(time())))
-        chmod(temp_path, '770')
-
-        temp_assignment_path = os.path.join(temp_path, assignment_name)
+        temp_path = ''
 
-        git_clone(self.student_repo_path, temp_path)
-        git_checkout(temp_assignment_path, self.commit_hash)
-
-        # copy the tests - this creates a tests folder inside the temp dir
-        with directory_locks.get_lock(self.assignment_dir.path):
-            cp(self.tests_path, temp_path, recursive=True)
-            # copy the assignment.cfg file (if present)
-            if os.path.exists(self.config_path):
-                cp(self.config_path, temp_path)
+        try:
+            # An exception raised in this block is considered a failure to
+            # run the tests, and both the student and faculty user will be
+            # notified
 
-        temp_run_action_sh_path = os.path.join(temp_path, 'run_action.sh')
+            temp_path = mkdtemp(dir=user_home_dir(config.tester_user),
+                                prefix='{}_'.format(int(time())))
 
-        assignment_config = AssignmentConfig(os.path.join(temp_path,
-                                                          'assignment.cfg'),
-                                             config.default_test_env)
+            paths = TempPaths(temp_path, self.assignment_name)
 
-        if assignment_config.env == TestEnv.FIREJAIL:
-            # firejail makes the temp directory look like /home/tester from
-            # the tests point of view
-            write_run_action_sh(temp_run_action_sh_path, self.tests_path,
-                                assignment_config,
-                                user_home_dir(config.tester_user))
-        elif assignment_config.env == TestEnv.DOCKER:
-            write_run_action_sh(temp_run_action_sh_path, self.tests_path,
-                                assignment_config)
-        else:
-            write_run_action_sh(temp_run_action_sh_path, self.tests_path,
-                                assignment_config, temp_path)
+            with directory_locks.get_lock(self.assignment_dir.path):
+                assignment_cfg_path = self.assignment_dir.config_path
+                assignment_cfg = AssignmentConfig(assignment_cfg_path,
+                                                  config.default_test_env)
 
-        temp_assignment_path = os.path.join(temp_path, assignment_name)
+            self._setup_temp_dir(paths, assignment_cfg)
 
-        # make the tester user the owner of the temporary directory
-        sudo_chown(temp_path, config.tester_user, config.keeper_group,
-                   recursive=True)
-
-        # execute action.sh and capture the output
-        try:
-            assignment_config = AssignmentConfig(self.config_path,
-                                                 config.default_test_env)
-            if assignment_config.env == TestEnv.DOCKER:
-                # Run a docker pull on the image here so that the output
-                # is not captured in the email.  If the container image is
-                # already on the server, this will return quickly
-                pull_cmd = ['docker', 'pull', assignment_config.image]
-                run_command(pull_cmd)
-                cmd = self._make_docker_command(temp_path, assignment_name,
-                                                assignment_config.image)
-            elif assignment_config.env == TestEnv.FIREJAIL:
-                cmd = self._make_firejail_command(temp_path, assignment_name,
-                                                  assignment_config.append_args)
-            elif assignment_config.env == TestEnv.HOST:
-                cmd = self._make_host_command(temp_run_action_sh_path,
-                                              temp_assignment_path)
-            else:
-                raise GkeepException('Unknown test env type {}'
-                                     .format(assignment_config.env))
+            cmd = self._make_action_command(paths, assignment_cfg)
 
             try:
                 body = run_command(cmd)
@@ -175,19 +151,9 @@ def run_tests(self):
                 else:
                     raise e
 
-            # send output as email
-            if assignment_config.use_html is not None:
-                html_pre_body = assignment_config.use_html
-            else:
-                html_pre_body = config.use_html
+            self._email_results(body, assignment_cfg)
 
-            subject = (assignment_config.results_subject
-                       .format(class_name=class_name,
-                               assignment_name=assignment_name))
-            email_sender.enqueue(Email(self.student.email_address, subject,
-                                       body, html_pre_body=html_pre_body))
-
-            if self.student.username != faculty_username:
+            if self.student.username != self.faculty_username:
                 with directory_locks.get_lock(self.assignment_dir.path):
                     self._add_report(body)
                 info_updater.enqueue_submission_scan(self.faculty_username,
@@ -196,15 +162,46 @@ def run_tests(self):
                                                      self.student.username)
 
         except Exception as e:
-            report_failure(assignment_name, self.student,
+            report_failure(self.assignment_name, self.student,
                            self.faculty_email, str(e))
-
-        if os.path.isdir(temp_path):
-            rm(temp_path, recursive=True, sudo=True)
+        finally:
+            if os.path.isdir(temp_path):
+                rm(temp_path, recursive=True, sudo=True)
 
         logger.log_debug('Done running tests on {0}'
                          .format(self.student_repo_path))
 
+    def _setup_temp_dir(self, paths: TempPaths,
+                        assignment_cfg: AssignmentConfig):
+        # clone the student's submission, copy the tests and assignment config,
+        # write run_action.sh, and set permissions
+
+        chmod(paths.temp_path, '770')
+
+        git_clone(self.student_repo_path, paths.temp_path)
+        git_checkout(paths.submission_path, self.commit_hash)
+
+        # copy the tests - this creates a tests folder inside the temp dir
+        with directory_locks.get_lock(self.assignment_dir.path):
+            cp(self.tests_path, paths.temp_path, recursive=True)
+
+        if assignment_cfg.env == TestEnv.FIREJAIL:
+            # firejail makes the temp directory look like /home/tester from
+            # the tests point of view
+            write_run_action_sh(paths.run_action_sh_path, self.tests_path,
+                                assignment_cfg,
+                                user_home_dir(config.tester_user))
+        elif assignment_cfg.env == TestEnv.DOCKER:
+            write_run_action_sh(paths.run_action_sh_path, self.tests_path,
+                                assignment_cfg)
+        else:
+            write_run_action_sh(paths.run_action_sh_path, self.tests_path,
+                                assignment_cfg, paths.temp_path)
+
+        # make the tester user the owner of the temporary directory
+        sudo_chown(paths.temp_path, config.tester_user, config.keeper_group,
+                   recursive=True)
+
     def _add_report(self, body):
         # Add the results of running tests to the reports repository
         with reports_clone(self.assignment_dir) as temp_reports_repo_path:
@@ -236,63 +233,100 @@ def _add_report(self, body):
             git_add_all(temp_reports_repo_path)
             git_commit(temp_reports_repo_path, reports_commit_message)
 
-    def _make_docker_command(self, temp_path, assignment_name,
-                             container_image):
-        return ['docker', 'run', '-v',
-                '{}:/git-keeper-tester'.format(temp_path),
-                container_image, 'bash',
+    def _email_results(self, body, assignment_cfg: AssignmentConfig):
+        # send the student the results via email
+
+        if assignment_cfg.use_html is not None:
+            html_pre_body = assignment_cfg.use_html
+        else:
+            html_pre_body = config.use_html
+
+        subject = (assignment_cfg.results_subject
+                   .format(class_name=self.class_name,
+                           assignment_name=self.assignment_name))
+        email_sender.enqueue(Email(self.student.email_address, subject,
+                                   body, html_pre_body=html_pre_body))
+
+    def _make_action_command(self, paths: TempPaths,
+                             assignment_cfg: AssignmentConfig):
+        # build the command to run to run the tests based on the test
+        # environment type
+
+        if assignment_cfg.env == TestEnv.DOCKER:
+            # Run a docker pull on the image here so that the output
+            # is not captured in the email.  If the container image is
+            # already on the server, this will return quickly
+            pull_cmd = ['docker', 'pull', assignment_cfg.image]
+            run_command(pull_cmd)
+            cmd = self._make_docker_command(paths, assignment_cfg)
+        elif assignment_cfg.env == TestEnv.FIREJAIL:
+            cmd = self._make_firejail_command(paths, assignment_cfg)
+        elif assignment_cfg.env == TestEnv.HOST:
+            cmd = self._make_host_command(paths)
+        else:
+            raise GkeepException('Unknown test env type {}'
+                                 .format(assignment_cfg.env))
+
+        return cmd
+
+    def _make_docker_command(self, paths: TempPaths,
+                             assignment_cfg: AssignmentConfig):
+        return ['docker', 'run', '--pull', 'never', '-v',
+                '{}:/git-keeper-tester'.format(paths.temp_path),
+                assignment_cfg.image, 'bash',
                 '/git-keeper-tester/run_action.sh',
-                os.path.join('/git-keeper-tester/', assignment_name),
+                os.path.join('/git-keeper-tester/', self.assignment_name),
                 self.student.username, self.student.email_address,
                 self.student.last_name, self.student.first_name]
 
-    def _make_host_command(self, temp_run_action_sh_path,
-                           temp_assignment_path):
+    def _make_host_command(self, paths: TempPaths):
         return ['sudo', '--user', config.tester_user, '--set-home', 'bash',
-                temp_run_action_sh_path, temp_assignment_path,
+                paths.run_action_sh_path, paths.submission_path,
                 self.student.username, self.student.email_address,
                 self.student.last_name, self.student.first_name]
 
-    def _make_firejail_command(self, temp_path, assignment_name, append_args):
+    def _make_firejail_command(self, paths: TempPaths,
+                               assignment_cfg: AssignmentConfig):
         tester_home = user_home_dir(config.tester_user)
 
         cmd = ['sudo', '--user', config.tester_user, '--set-home',
                'firejail', '--noprofile', '--quiet',
-               '--deterministic-exit-code', '--private={}'.format(temp_path)]
+               '--deterministic-exit-code',
+               '--private={}'.format(paths.temp_path)]
 
-        if append_args is not None:
-            cmd.extend(append_args.split())
+        if assignment_cfg.append_args is not None:
+            cmd.extend(assignment_cfg.append_args.split())
 
         cmd.extend(['bash', os.path.join(tester_home, 'run_action.sh'),
-                    os.path.join(tester_home, assignment_name),
+                    os.path.join(tester_home, self.assignment_name),
                     self.student.username, self.student.email_address,
                     self.student.last_name, self.student.first_name])
 
         return cmd
 
-    def _send_closed_email(self, class_name, assignment_name):
+    def _send_closed_email(self):
         # Send an email to the submitter that the class has closed
-        subject = ('[{0}] class is closed'.format(class_name))
+        subject = ('[{0}] class is closed'.format(self.class_name))
         body = ('You have pushed a submission for a class that is is '
                 'closed.')
         email_sender.enqueue(Email(self.student.email_address, subject,
                                    body))
         logger.log_info('{} pushed to {} in {}, which is closed'
-                        .format(self.student.username, assignment_name,
-                                class_name))
+                        .format(self.student.username, self.assignment_name,
+                                self.class_name))
 
-    def _send_disabled_email(self, class_name, assignment_name):
+    def _send_disabled_email(self):
         # Send an email to the submitter that the class is disabled
         subject = ('[{0}] assignment {1} is disabled'
-                   .format(class_name, assignment_name))
+                   .format(self.class_name, self.assignment_name))
         body = ('You have pushed a submission for assignment {} which is '
                 'disabled. No tests were run on your submission.'
-                .format(assignment_name))
+                .format(self.assignment_name))
         email_sender.enqueue(Email(self.student.email_address, subject,
                                    body))
         logger.log_info('{} pushed to {} in {}, which is disabled'
-                        .format(self.student.username, assignment_name,
-                                class_name))
+                        .format(self.student.username, self.assignment_name,
+                                self.class_name))
 
 
 def write_run_action_sh(dest_path: str, tests_path: str,
diff --git a/git-keeper-server/gkeepserver/version.py b/git-keeper-server/gkeepserver/version.py
index 998fc9dc..e0d29079 100644
--- a/git-keeper-server/gkeepserver/version.py
+++ b/git-keeper-server/gkeepserver/version.py
@@ -2,4 +2,4 @@
 # Instead, edit the VERSION file in the top level directory of the git-keeper
 # project and then run bump_version.py.
 
-__version__ = '1.0.0'
+__version__ = '1.0.1'
diff --git a/mkdocs.yml b/mkdocs.yml
index 92e01ccb..8cd992f9 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,19 +1,12 @@
 site_name: git-keeper Documentation
 repo_url: https://github.com/git-keeper/git-keeper
 nav:
-    - Home: 'index.md'
-    - 'Faculty Users':
-        - 'Client Setup': 'faculty/client-setup.md'
-        - 'Create a Class': 'faculty/create-class.md'
-        - 'Assignment Workflow': 'faculty/assignment-workflow.md'
-    - 'Admin Users':
-        - 'Server Setup': 'admin/server-setup.md'
-    - Developers:
-        - 'General Guidelines': 'develop/developers.md'        
-        - 'Release Checklist': 'develop/release_checklist.md'
-        - 'JSON Structure': 'develop/info-json-structure.md'
-        - 'Log Structure': 'develop/log-events.md'
-        - 'Server File Structure': 'develop/server_filesystem_structure.md'
-    - Reference:
-        - 'Client': 'reference/client.md'
+    - Home: index.md
+    - 'Faculty Users': faculty-users.md
+    - 'Admin Users': admin-users.md
+    - Reference: reference.md
+    - Developers: developers.md
 theme: readthedocs
+markdown_extensions:
+    - admonition
+    - toc
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 050ae596..00000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-jinja2<3.1.0
-markdown<3.4


Test patch:
diff --git a/docs/develop/acceptance_testing.md b/docs/develop/acceptance_testing.md
deleted file mode 100644
index 0ec8ebe1..00000000
--- a/docs/develop/acceptance_testing.md
+++ /dev/null
@@ -1,91 +0,0 @@
-
-## Overview
-
-Our acceptance tests use two virtual machines:
-
-* `gkserver` - a server to run `gkeepd`. 
-* `gkclient` - a machine where faculty and student actions can be executed.
-
-We use Vangrant(https://www.vagrantup.com/) (backed by VirtualBox) to launch the machines and [Robot Framework](https://robotframework.org/) to write the tests.
-
-The `git-keeper-robot` package contains a library of functions to control the VMs and to implement "verbs" for Robot Framework.
-
-The `gkserver` machine has all the dependencies installed, and.  When you run `vagrant up`, Vagrant will mount `git-keeper-core` and `git-keeper-server` on within `gkserver` and then `pip install -e` both packages.  Note that this means that any changes you make to the code will be used within the VM even after this step completes.
-
-Similarly, `vagrant up` will install `git-keeper-core` and `git-keeper-client` on the `gkclient` VM, making local changes usable within the VM.
-
-The `gkserver` VM contains a mock email server that saves files to `/email` instead of actually sending email messages.
-
-## Setup
-
-Before you can run the test, you must build the VirtualBox images:
-
-* Goto `tests/acceptance/gkserver_base` and run `make_box.sh`.  This will create an image with the basic setup steps complete.  See the `Vagrantfile` in this directory for the steps.
-* Run `vagrant box add --name gkserver gkserver.box`.  This will allow Vagrant to launch the `gkserver` image.  See `tests/acceptance/Vagrantfile`.
-* Goto `tests/acceptance/gkclient_base` and run `make_box.sh`.  This will install the necessary software to run `gkeep`, and it will create a use named `keeper` (password `keeper`) that has `sudo` rights.
-* Run `vagrant box add --name gkclient gkclient.box`.  This will allow Vagrant to launch the `gkclient` image.  See `tests/acceptance/Vagrantfile`.
-
-## Running the VMs
-
-
-* Launch both VMs by running `vangrant up` in `/tests/acceptance`.  This will use `tests/acceptance/Vagrantfile` to launch both VMs and install the `git-keeper` code.
-* Destroy both VMs by running `vagrant destroy -f`
-
-
-## Running Robot Framework Tests
-
-Once you have built `gkserver` and `gkclient`, you can run the tests.  Run `robot .` to tell Robot Framework to run all tests in the current directory.
-
-When you run tests, Robot Framework has two behaviors:
-
-* If `gkserver` and `gkclient` are already up, it will use these machines for testing - and then leave them running when done.  This saves time when you have to run tests multiple times.
-* If `gkserver` and `gkclient` are not running, it will lauch both machines for testing - and then destroy them when done.
-
-In order to avoid side effects, Robot Framework will reset `gkserver` and `gkclient` before each test.  See `vmscripts/reset_server.py` and `vmscripts/reset_client.py` to see what is reset.
-
-### Run Subset of Tests
-
-We use a `__init__.robot` file to setup the VMs, and so you have to be careful when you want to run a subset of tests.  In the following commands, note the period at the end of the commands.  This tells Robot Framework to  look in the current directory when it runs tests, and this causes it to find the `__init__.robot` file.
-
-* To run one test, use `robot -t "<test_name>" .` - For example, `robot -t "Valid Class" .`. 
-* To run one suite (file) of tests, use `robot -s <suite_name> .` - For example, `robot -s gkeepd_launch .` to run all the tests in the `gkeepd_launch.robot` file.
-
-## Manual Execution
-
-You can also use `gkserver` and `gkclient` to do manually testing.  
-
-If you want a "clean" system where you can configure and run `gkeepd`:
-
-* Run `vagrant up` in `tests/acceptance` to launch both VMs.
-* Run `vagrant ssh gkserver` to connect to the server
-* On the server, run `su - keeper` to become the keeper account.  The password is "keeper"
-* You can also connect to `gkclient`, where there is also a `keeper` account (password is "keeper").
-
-For convienence, the script `manual_configure.py` will:
-
-* Create a valid `server.cfg`
-* Create an `admin_prof` account on `gkclient` and make that account the admin of `git-keeper`
-* Create a `prof1` account on `gkclient` and make it a non-admin faculty member of `git-keeper`
-* Create a course named `cs1` for `prof1` that contains students `student1` and `student2`. 
-* Create accounts for `student1` and `student2`
-* Have `prof1` upload and publish a simple assignment
-* Have `student1` clone and submit the assignment
-* Have `prof1` fetch the submissions.
-
-To reset the system, run `manual_reset.py`.  This will:
-
-* [Server]: Stop `gkeepd`
-* [Server]: Remove all `git-keeper` files
-* [Server]: Delete all email files in `/email`
-* [Server]: Remove all users except `keeper` and `vagrant`
-* [Client]: Remove all users exept `keeper`
-
-
-## Notes
-
-* This testing system is finicky!  If you suddently get errors, try `vagrant destroy -f` and `vagrant up` first.
-* Once you get `gkserver.box` and `gkclient.box` built and added to Vagrant, you should not need to do this step again.
-* If you get an error message, "The IP address configured for the host-only network is not within the allowed ranges. Please update the address used to be within the allowed ranges and run the command again" - add the line `* 20.0.0.0/8 192.168.0.0/16` (the asterisk matters!) to `/etc/vbox/networks.conf`.  Make sure this file is world readable.  See [this StackOverFlow Question](https://stackoverflow.com/questions/70704093/the-ip-address-configured-for-the-host-only-network-is-not-within-the-allowed-ra)
-* The `tests/acceptance/vm_scripts` folder must be world readable for the tests to execute.  One one system the `umask` was set to 0077, and so all files had no group permissions allowed.  This caused the `reset_server.py` script to fail during `manual_configure.py`.  Run `umask 0022`, re-clone the repo, recreate the virtual enviroment, and then run the tests again.
-* Robot Framework masks many error messages.  If tests are failing in weird ways, run `manual_configure.py`. If this succeeds, go into `gkserver` and/or `gkclient` and run your sequence of steps manually.
-
diff --git a/docs/faculty/testing-environments.md b/docs/faculty/testing-environments.md
deleted file mode 100644
index b6af0702..00000000
--- a/docs/faculty/testing-environments.md
+++ /dev/null
@@ -1,128 +0,0 @@
-The testing environment for an assignment can be specified in the optional file
-`test_env.yaml` within the assignment directory. If this file does not exist
-for a given assignment, the assignment will default to the `host` environment,
-described below.
-
-### Host Environment
-
-This testing environment is straightforward, but less secure than the other
-environment types, and requires that any dependencies for the tests be
-installed on the server itself.
-
-The git-keeper server creates a user named `tester`, and tests are run within a
-temporary directory in `/home/tester`. The `tester` user cannot access any
-other user home directories, but student code could potentially access other
-files within the `tester` home directory, including files for other currently
-running tests (if multiple threads are used for testing).
-
-If `test_env.yaml` is omitted, this environment will be used by
-default. Alternatively, the following may be placed in `test_env.yaml`:
-
-```
-type: host
-```
-
-### Firejail Environment
-
-This testing environment uses [Firejail](https://firejail.wordpress.com/) for
-sandboxing. As with `host` environments, tests are run by the `tester` user
-within a temporary directory in `/home/tester`, but `firejail` is used to limit
-the process so that it can only access files within the temporary testing
-directory. Dependencies for the tests must still be installed directly on the
-server.
-
-The `firejail` package may need to be explicitly installed on the server
-before this environment can be used. It is available in the official
-repositories for many Linux distributions, including Ubuntu.
-
-By default, `firejail` is run with the following options:
-
-```
---noprofile --quiet --private=/home/tester/<temporary testing directory>
-```
-
-This makes `firejail` ignore any default profiles that may be installed on the
-server, supresses `firejail` output so that it is not included in the results
-email, and sandboxes the tests within the testing directory.
-
-To use the default options, simply create a `test_env.yaml` file with the
-following contents:
-
-```
-type: firejail
-```
-
-Additional options may be specified using the `append_args` field. For example,
-if you wish to prohibit any network activity and prevent the process from
-writing any files larger than 100 KB, you could use the following:
-
-```
-type: firejail
-append_args: --net=none --rlimit-fsize=102400
-```
-
-These options will be used in addition to the default options. For a full list
-of options available for `firejail`, see the
-[Firejail homepage](https://firejail.wordpress.com/) or read through the man
-page.
-
-### Docker Environment
-
-Tests may be run in a Docker container, which can provide both sandboxing and
-encapsulation of dependencies.
-
-TODO: setting up docker on the server
-
-To use this environment, a Docker container image must be specified in
-`test_env.yaml`. For example, to use our Python 3.10 container the following
-may be used:
-
-```
-type: docker
-image: gitkeeper/git-keeper-tester:python3.10
-```
-
-The first time tests are run using a particular image, the server must download
-the image. This can take some time, so it is important to submit a test
-submission before publishing the assignment so that the image is downloaded to
-the server before students start submitting.
-
-TODO: Describe and link to our Docker Hub images
-
-When tests are run, the temporary testing directory is mounted as a volume
-within the container at `/git-keeper-testing`, and the `tests` directory is at
-`/git-keeper-testing/tests`. To create your own container images, you will need
-to use the `tests` directory as the working directory, and set the command to
-run either `action.sh` or `action.py` depending on your preference.
-
-For example, here is a `Dockerfile` to build an image with Python 3.10 that can
-be used with git-keeper:
-
-```
-FROM python:3.10-slim
-
-WORKDIR /git-keeper-tester/tests
-
-CMD ["bash", "action.sh"]
-```
-
-If you want to install additional Python packages within the container, use
-`RUN` commands. For example, this installs the `pytest` and `pytest-timeout`
-packages using `pip`:
-
-```
-FROM python:3.10-slim
-
-RUN pip install pytest pytest-timeout
-
-WORKDIR /git-keeper-tester/tests
-
-CMD ["bash", "action.sh"]
-```
-
-TODO: Describe how to test the image locally
-
-You can upload your images to Docker Hub, where they can then be accessed by
-the server. See the
-[Docker Hub documentation](https://docs.docker.com/docker-hub/) for more
-information.
diff --git a/tests/acceptance/assignments/all/base_code/code.py b/tests/acceptance/assignments/all/base_code/code.py
new file mode 100644
index 00000000..292a7af9
--- /dev/null
+++ b/tests/acceptance/assignments/all/base_code/code.py
@@ -0,0 +1,1 @@
+print('Unpushed')
diff --git a/tests/acceptance/assignments/all/correct_solution/code.py b/tests/acceptance/assignments/all/correct_solution/code.py
new file mode 100644
index 00000000..67e9dd6e
--- /dev/null
+++ b/tests/acceptance/assignments/all/correct_solution/code.py
@@ -0,0 +1,1 @@
+print('Done')
diff --git a/tests/acceptance/assignments/all/email.txt b/tests/acceptance/assignments/all/email.txt
new file mode 100644
index 00000000..91d5c638
--- /dev/null
+++ b/tests/acceptance/assignments/all/email.txt
@@ -0,0 +1,1 @@
+good_simple assignment
diff --git a/tests/acceptance/assignments/all/tests/action.sh b/tests/acceptance/assignments/all/tests/action.sh
new file mode 100644
index 00000000..9c8438ca
--- /dev/null
+++ b/tests/acceptance/assignments/all/tests/action.sh
@@ -0,0 +1,2 @@
+python3 $1/code.py
+exit 0
diff --git a/tests/acceptance/assignments/tests/base_code/code.py b/tests/acceptance/assignments/tests/base_code/code.py
new file mode 100644
index 00000000..292a7af9
--- /dev/null
+++ b/tests/acceptance/assignments/tests/base_code/code.py
@@ -0,0 +1,1 @@
+print('Unpushed')
diff --git a/tests/acceptance/assignments/tests/correct_solution/code.py b/tests/acceptance/assignments/tests/correct_solution/code.py
new file mode 100644
index 00000000..67e9dd6e
--- /dev/null
+++ b/tests/acceptance/assignments/tests/correct_solution/code.py
@@ -0,0 +1,1 @@
+print('Done')
diff --git a/tests/acceptance/assignments/tests/email.txt b/tests/acceptance/assignments/tests/email.txt
new file mode 100644
index 00000000..91d5c638
--- /dev/null
+++ b/tests/acceptance/assignments/tests/email.txt
@@ -0,0 +1,1 @@
+good_simple assignment
diff --git a/tests/acceptance/assignments/tests/tests/action.sh b/tests/acceptance/assignments/tests/tests/action.sh
new file mode 100644
index 00000000..9c8438ca
--- /dev/null
+++ b/tests/acceptance/assignments/tests/tests/action.sh
@@ -0,0 +1,2 @@
+python3 $1/code.py
+exit 0
diff --git a/tests/acceptance/gkeep_upload.robot b/tests/acceptance/gkeep_upload.robot
index a7e52ca9..f5e658a1 100644
--- a/tests/acceptance/gkeep_upload.robot
+++ b/tests/acceptance/gkeep_upload.robot
@@ -60,3 +60,13 @@ Double Assignment Upload
     Add Assignment to Client  faculty1  good_simple
     Gkeep Upload Succeeds   faculty1   cs1    good_simple
     Gkeep Upload Fails   faculty1   cs1    good_simple
+
+Assignment Named all
+    [Tags]  error
+    Add Assignment to Client  faculty1  all
+    Gkeep Upload Fails  faculty1  cs1  all
+
+Assignment Named tests
+    [Tags]  error
+    Add Assignment to Client  faculty1  tests
+    Gkeep Upload Fails  faculty1  cs1  tests
diff --git a/tests/acceptance/gkeepd_launch.robot b/tests/acceptance/gkeepd_launch.robot
index e7ad0dc6..b1ec9d5b 100644
--- a/tests/acceptance/gkeepd_launch.robot
+++ b/tests/acceptance/gkeepd_launch.robot
@@ -36,6 +36,12 @@ Admin Account Already Exists
     Add File To Server    keeper    files/valid_server.cfg    server.cfg
     Start gkeepd
     New Account Email Does Not Exist    admin_prof
+    New Account Email Exists    admin_prof1
+    User Exists On Server    admin_prof1
+    Gkeepd Is Running
+    # make sure restarting still works, this was broken in v1.0.0
+    Stop Gkeepd
+    Start Gkeepd
     Gkeepd Is Running
 
 Admin Named Faculty