import glob
import re
import sys
import os
import six
import logging
import warnings

from traitlets.config import LoggingConfigurable, Config
from traitlets import Instance, Enum, Unicode, observe

from ..coursedir import CourseDirectory
from ..converters import GenerateAssignment, Autograde, GenerateFeedback
from ..exchange import ExchangeList, ExchangeReleaseAssignment, ExchangeReleaseFeedback, ExchangeFetchFeedback, ExchangeCollect, ExchangeError, ExchangeSubmit
from ..api import MissingEntry, Gradebook, Student, SubmittedAssignment
from ..utils import parse_utc, temp_attrs, capture_log, as_timezone, to_numeric_tz
from ..auth import Authenticator


class NbGraderAPI(LoggingConfigurable):
    """A high-level API for using nbgrader."""

    coursedir = Instance(CourseDirectory, allow_none=True)
    authenticator = Instance(Authenticator, allow_none=True)

    # The log level for the application
    log_level = Enum(
        (0, 10, 20, 30, 40, 50, 'DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL'),
        default_value=logging.INFO,
        help="Set the log level by value or name."
    ).tag(config=True)

    timezone = Unicode(
        "UTC",
        help="Timezone for displaying timestamps"
    ).tag(config=True)

    timestamp_format = Unicode(
        "%Y-%m-%d %H:%M:%S %Z",
        help="Format string for displaying timestamps"
    ).tag(config=True)

    @observe('log_level')
    def _log_level_changed(self, change):
        """Adjust the log level when log_level is set."""
        new = change.new
        if isinstance(new, six.string_types):
            new = getattr(logging, new)
            self.log_level = new
        self.log.setLevel(new)

    def __init__(self, coursedir=None, authenticator=None, **kwargs):
        """Initialize the API.

        Arguments
        ---------
        coursedir: :class:`nbgrader.coursedir.CourseDirectory`
            (Optional) A course directory object.
        authenticator : :class:~`nbgrader.auth.BaseAuthenticator`
            (Optional) An authenticator instance for communicating with an
            external database.
        kwargs:
            Additional keyword arguments (e.g. ``parent``, ``config``)

        """
        self.log.setLevel(self.log_level)
        super(NbGraderAPI, self).__init__(**kwargs)

        if coursedir is None:
            self.coursedir = CourseDirectory(parent=self)
        else:
            self.coursedir = coursedir

        if authenticator is None:
            self.authenticator = Authenticator(parent=self)
        else:
            self.authenticator = authenticator

        if sys.platform != 'win32':
            lister = ExchangeList(
                coursedir=self.coursedir,
                authenticator=self.authenticator,
                parent=self)
            self.course_id = self.coursedir.course_id
            self.exchange = lister.root

            try:
                lister.start()
            except ExchangeError:
                self.exchange_missing = True
            else:
                self.exchange_missing = False

        else:
            self.course_id = ''
            self.exchange = ''
            self.exchange_missing = True

    @property
    def exchange_is_functional(self):
        return self.course_id and not self.exchange_missing and sys.platform != 'win32'

    @property
    def gradebook(self):
        """An instance of :class:`nbgrader.api.Gradebook`.

        Note that each time this property is accessed, a new gradebook is
        created. The user is responsible for destroying the gradebook through
        :func:`~nbgrader.api.Gradebook.close`.

        """
        return Gradebook(self.coursedir.db_url, self.course_id)

    def get_source_assignments(self):
        """Get the names of all assignments in the `source` directory.

        Returns
        -------
        assignments: set
            A set of assignment names

        """
        filenames = glob.glob(self.coursedir.format_path(
            self.coursedir.source_directory,
            student_id='.',
            assignment_id='*'))

        assignments = set([])
        for filename in filenames:
            # skip files that aren't directories
            if not os.path.isdir(filename):
                continue

            # parse out the assignment name
            regex = self.coursedir.format_path(
                self.coursedir.source_directory,
                student_id='.',
                assignment_id='(?P<assignment_id>.*)',
                escape=True)

            matches = re.match(regex, filename)
            if matches:
                assignments.add(matches.groupdict()['assignment_id'])

        return assignments

    def get_released_assignments(self):
        """Get the names of all assignments that have been released to the
        exchange directory. If the course id is blank, this returns an empty
        set.

        Returns
        -------
        assignments: set
            A set of assignment names

        """
        if self.exchange_is_functional:
            lister = ExchangeList(
                coursedir=self.coursedir,
                authenticator=self.authenticator,
                parent=self)
            released = set([x['assignment_id'] for x in lister.start()])
        else:
            released = set([])

        return released

    def get_submitted_students(self, assignment_id):
        """Get the ids of students that have submitted a given assignment
        (determined by whether or not a submission exists in the `submitted`
        directory).

        Arguments
        ---------
        assignment_id: string
            The name of the assignment. May be * to select for all assignments.

        Returns
        -------
        students: set
            A set of student ids

        """
        # get the names of all student submissions in the `submitted` directory
        filenames = glob.glob(self.coursedir.format_path(
            self.coursedir.submitted_directory,
            student_id='*',
            assignment_id=assignment_id))

        students = set([])
        for filename in filenames:
            # skip files that aren't directories
            if not os.path.isdir(filename):
                continue

            # parse out the student id
            if assignment_id == "*":
                assignment_id = ".*"
            regex = self.coursedir.format_path(
                self.coursedir.submitted_directory,
                student_id='(?P<student_id>.*)',
                assignment_id=assignment_id,
                escape=True)

            matches = re.match(regex, filename)
            if matches:
                students.add(matches.groupdict()['student_id'])

        return students

    def get_submitted_timestamp(self, assignment_id, student_id):
        """Gets the timestamp of a submitted assignment.

        Arguments
        ---------
        assignment_id: string
            The assignment name
        student_id: string
            The student id

        Returns
        -------
        timestamp: datetime.datetime or None
            The timestamp of the submission, or None if the timestamp does
            not exist

        """
        assignment_dir = os.path.abspath(self.coursedir.format_path(
            self.coursedir.submitted_directory,
            student_id,
            assignment_id))

        timestamp_pth = os.path.join(assignment_dir, 'timestamp.txt')
        if os.path.exists(timestamp_pth):
            with open(timestamp_pth, 'r') as fh:
                return parse_utc(fh.read().strip())

    def get_autograded_students(self, assignment_id):
        """Get the ids of students whose submission for a given assignment
        has been autograded. This is determined based on satisfying all of the
        following criteria:

        1. There is a directory present in the `autograded` directory.
        2. The submission is present in the database.
        3. The timestamp of the autograded submission is the same as the
           timestamp of the original submission (in the `submitted` directory).

        Returns
        -------
        students: set
            A set of student ids

        """
        # get all autograded submissions
        with self.gradebook as gb:
            ag_timestamps = dict(gb.db\
                .query(Student.id, SubmittedAssignment.timestamp)\
                .join(SubmittedAssignment)\
                .filter(SubmittedAssignment.name == assignment_id)\
                .all())
            ag_students = set(ag_timestamps.keys())

        students = set([])
        for student_id in ag_students:
            # skip files that aren't directories
            filename = self.coursedir.format_path(
                self.coursedir.autograded_directory,
                student_id=student_id,
                assignment_id=assignment_id)
            if not os.path.isdir(filename):
                continue

            # get the timestamps and check whether the submitted timestamp is
            # newer than the autograded timestamp
            submitted_timestamp = self.get_submitted_timestamp(assignment_id, student_id)
            autograded_timestamp = ag_timestamps[student_id]
            if submitted_timestamp != autograded_timestamp:
                continue

            students.add(student_id)

        return students

    def get_assignment(self, assignment_id, released=None):
        """Get information about an assignment given its name.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment
        released: list
            (Optional) A set of names of released assignments, obtained via
            self.get_released_assignments().

        Returns
        -------
        assignment: dict
            A dictionary containing information about the assignment

        """
        # get the set of released assignments if not given
        if not released:
            released = self.get_released_assignments()

        # check whether there is a source version of the assignment
        sourcedir = os.path.abspath(self.coursedir.format_path(
            self.coursedir.source_directory,
            student_id='.',
            assignment_id=assignment_id))
        if not os.path.isdir(sourcedir):
            return

        # see if there is information about the assignment in the database
        try:
            with self.gradebook as gb:
                db_assignment = gb.find_assignment(assignment_id)
                assignment = db_assignment.to_dict()
                if db_assignment.duedate:
                    ts = as_timezone(db_assignment.duedate, self.timezone)
                    assignment["display_duedate"] = ts.strftime(self.timestamp_format)
                    assignment["duedate_notimezone"] = ts.replace(tzinfo=None).isoformat()
                else:
                    assignment["display_duedate"] = None
                    assignment["duedate_notimezone"] = None
                assignment["duedate_timezone"] = to_numeric_tz(self.timezone)
                assignment["average_score"] = gb.average_assignment_score(assignment_id)
                assignment["average_code_score"] = gb.average_assignment_code_score(assignment_id)
                assignment["average_written_score"] = gb.average_assignment_written_score(assignment_id)
                assignment["average_task_score"] = gb.average_assignment_task_score(assignment_id)

        except MissingEntry:
            assignment = {
                "id": None,
                "name": assignment_id,
                "duedate": None,
                "display_duedate": None,
                "duedate_notimezone": None,
                "duedate_timezone": to_numeric_tz(self.timezone),
                "average_score": 0,
                "average_code_score": 0,
                "average_written_score": 0,
                "average_task_score": 0,
                "max_score": 0,
                "max_code_score": 0,
                "max_written_score": 0,
                "max_task_score": 0
            }

        # get released status
        if not self.exchange_is_functional:
            assignment["releaseable"] = False
            assignment["status"] = "draft"
        else:
            assignment["releaseable"] = True
            if assignment_id in released:
                assignment["status"] = "released"
            else:
                assignment["status"] = "draft"

        # get source directory
        assignment["source_path"] = os.path.relpath(sourcedir, self.coursedir.root)

        # get release directory
        releasedir = os.path.abspath(self.coursedir.format_path(
            self.coursedir.release_directory,
            student_id='.',
            assignment_id=assignment_id))
        if os.path.exists(releasedir):
            assignment["release_path"] = os.path.relpath(releasedir, self.coursedir.root)
        else:
            assignment["release_path"] = None

        # number of submissions
        assignment["num_submissions"] = len(self.get_submitted_students(assignment_id))

        return assignment

    def get_assignments(self):
        """Get a list of information about all assignments.

        Returns
        -------
        assignments: list
            A list of dictionaries containing information about each assignment

        """
        released = self.get_released_assignments()

        assignments = []
        for x in self.get_source_assignments():
            assignments.append(self.get_assignment(x, released=released))

        assignments.sort(key=lambda x: (x["duedate"] if x["duedate"] is not None else "None", x["name"]))
        return assignments

    def get_notebooks(self, assignment_id):
        """Get a list of notebooks in an assignment.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment

        Returns
        -------
        notebooks: list
            A list of dictionaries containing information about each notebook

        """
        with self.gradebook as gb:
            try:
                assignment = gb.find_assignment(assignment_id)
            except MissingEntry:
                assignment = None

            # if the assignment exists in the database
            if assignment and assignment.notebooks:
                notebooks = []
                for notebook in assignment.notebooks:
                    x = notebook.to_dict()
                    x["average_score"] = gb.average_notebook_score(notebook.name, assignment.name)
                    x["average_code_score"] = gb.average_notebook_code_score(notebook.name, assignment.name)
                    x["average_written_score"] = gb.average_notebook_written_score(notebook.name, assignment.name)
                    x["average_task_score"] = gb.average_notebook_task_score(notebook.name, assignment.name)
                    notebooks.append(x)

            # if it doesn't exist in the database
            else:
                sourcedir = self.coursedir.format_path(
                    self.coursedir.source_directory,
                    student_id='.',
                    assignment_id=assignment_id)
                escaped_sourcedir = self.coursedir.format_path(
                    self.coursedir.source_directory,
                    student_id='.',
                    assignment_id=assignment_id,
                    escape=True)

                notebooks = []
                for filename in glob.glob(os.path.join(sourcedir, "*.ipynb")):
                    regex = re.escape(os.path.sep).join([escaped_sourcedir, "(?P<notebook_id>.*).ipynb"])
                    matches = re.match(regex, filename)
                    notebook_id = matches.groupdict()['notebook_id']
                    notebooks.append({
                        "name": notebook_id,
                        "id": None,
                        "average_score": 0,
                        "average_code_score": 0,
                        "average_written_score": 0,
                        "average_task_score": 0,
                        "max_score": 0,
                        "max_code_score": 0,
                        "max_written_score": 0,
                        "max_task_score": 0,
                        "needs_manual_grade": False,
                        "num_submissions": 0
                    })

        return notebooks

    def get_submission(self, assignment_id, student_id, ungraded=None, students=None):
        """Get information about a student's submission of an assignment.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment
        student_id: string
            The student's id
        ungraded: set
            (Optional) A set of student ids corresponding to students whose
            submissions have not yet been autograded.
        students: dict
            (Optional) A dictionary of dictionaries, keyed by student id,
            containing information about students.

        Returns
        -------
        submission: dict
            A dictionary containing information about the submission

        """
        if ungraded is None:
            autograded = self.get_autograded_students(assignment_id)
            ungraded = self.get_submitted_students(assignment_id) - autograded
        if students is None:
            students = {x['id']: x for x in self.get_students()}

        if student_id in ungraded:
            ts = self.get_submitted_timestamp(assignment_id, student_id)
            if ts:
                timestamp = ts.isoformat()
                display_timestamp = as_timezone(ts, self.timezone).strftime(self.timestamp_format)
            else:
                timestamp = None
                display_timestamp = None

            submission = {
                "id": None,
                "name": assignment_id,
                "timestamp": timestamp,
                "display_timestamp": display_timestamp,
                "score": 0.0,
                "max_score": 0.0,
                "code_score": 0.0,
                "max_code_score": 0.0,
                "written_score": 0.0,
                "max_written_score": 0.0,
                "task_score": 0.0,
                "max_task_score": 0.0,
                "needs_manual_grade": False,
                "autograded": False,
                "submitted": True,
                "student": student_id,
            }

            if student_id not in students:
                submission["last_name"] = None
                submission["first_name"] = None
            else:
                submission["last_name"] = students[student_id]["last_name"]
                submission["first_name"] = students[student_id]["first_name"]

        elif student_id in autograded:
            with self.gradebook as gb:
                try:
                    db_submission = gb.find_submission(assignment_id, student_id)
                    submission = db_submission.to_dict()
                    if db_submission.timestamp:
                        submission["display_timestamp"] = as_timezone(
                            db_submission.timestamp, self.timezone).strftime(self.timestamp_format)
                    else:
                        submission["display_timestamp"] = None

                except MissingEntry:
                    return None

            submission["autograded"] = True
            submission["submitted"] = True

        else:
            submission = {
                "id": None,
                "name": assignment_id,
                "timestamp": None,
                "display_timestamp": None,
                "score": 0.0,
                "max_score": 0.0,
                "code_score": 0.0,
                "max_code_score": 0.0,
                "written_score": 0.0,
                "max_written_score": 0.0,
                "task_score": 0.0,
                "max_task_score": 0.0,
                "needs_manual_grade": False,
                "autograded": False,
                "submitted": False,
                "student": student_id,
            }

            if student_id not in students:
                submission["last_name"] = None
                submission["first_name"] = None
            else:
                submission["last_name"] = students[student_id]["last_name"]
                submission["first_name"] = students[student_id]["first_name"]

        return submission

    def get_submissions(self, assignment_id):
        """Get a list of submissions of an assignment. Each submission
        corresponds to a student.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment

        Returns
        -------
        notebooks: list
            A list of dictionaries containing information about each submission

        """
        with self.gradebook as gb:
            db_submissions = gb.submission_dicts(assignment_id)

        ungraded = self.get_submitted_students(assignment_id) - self.get_autograded_students(assignment_id)
        students = {x['id']: x for x in self.get_students()}
        submissions = []
        for submission in db_submissions:
            if submission["student"] in ungraded:
                continue
            ts = submission["timestamp"]
            if ts:
                submission["timestamp"] = ts.isoformat()
                submission["display_timestamp"] = as_timezone(
                    ts, self.timezone).strftime(self.timestamp_format)
            else:
                submission["timestamp"] = None
                submission["display_timestamp"] = None
            submission["autograded"] = True
            submission["submitted"] = True
            submissions.append(submission)

        for student_id in ungraded:
            submission = self.get_submission(
                assignment_id, student_id, ungraded=ungraded, students=students)
            submissions.append(submission)

        submissions.sort(key=lambda x: x["student"])
        return submissions

    def _filter_existing_notebooks(self, assignment_id, notebooks):
        """Filters a list of notebooks so that it only includes those notebooks
        which actually exist on disk.

        This functionality is necessary for cases where student delete or rename
        on or more notebooks in their assignment, but still submit the assignment.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment
        notebooks: list
            List of :class:`~nbgrader.api.SubmittedNotebook` objects

        Returns
        -------
        submissions: list
            List of :class:`~nbgrader.api.SubmittedNotebook` objects

        """
        # Making a filesystem call for every notebook in the assignment
        # can be very slow on certain setups, such as using NFS, see
        # https://github.com/jupyter/nbgrader/issues/929
        #
        # If students are using the exchange and submitting with
        # ExchangeSubmit.strict == True, then all the notebooks we expect
        # should be here already so we don't need to filter for only
        # existing notebooks in that case.
        if self.exchange_is_functional:
            app = ExchangeSubmit(
                    coursedir=self.coursedir,
                    authenticator=self.authenticator,
                    parent=self)
            if app.strict:
                return sorted(notebooks, key=lambda x: x.id)

        submissions = list()
        for nb in notebooks:
            filename = os.path.join(
                os.path.abspath(self.coursedir.format_path(
                    self.coursedir.autograded_directory,
                    student_id=nb.student.id,
                    assignment_id=assignment_id)),
                "{}.ipynb".format(nb.name))

            if os.path.exists(filename):
                submissions.append(nb)

        return sorted(submissions, key=lambda x: x.id)

    def get_notebook_submission_indices(self, assignment_id, notebook_id):
        """Get a dictionary mapping unique submission ids to indices of the
        submissions relative to the full list of submissions.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment
        notebook_id: string
            The name of the notebook

        Returns
        -------
        indices: dict
            A dictionary mapping submission ids to the index of each submission

        """
        with self.gradebook as gb:
            notebooks = gb.notebook_submissions(notebook_id, assignment_id)
            submissions = self._filter_existing_notebooks(assignment_id, notebooks)
        return dict([(x.id, i) for i, x in enumerate(submissions)])

    def get_notebook_submissions(self, assignment_id, notebook_id):
        """Get a list of submissions for a particular notebook in an assignment.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment
        notebook_id: string
            The name of the notebook

        Returns
        -------
        submissions: list
            A list of dictionaries containing information about each submission.

        """
        with self.gradebook as gb:
            try:
                gb.find_notebook(notebook_id, assignment_id)
            except MissingEntry:
                return []

            submissions = gb.notebook_submission_dicts(notebook_id, assignment_id)

        indices = self.get_notebook_submission_indices(assignment_id, notebook_id)
        for nb in submissions:
            nb['index'] = indices.get(nb['id'], None)

        submissions = [x for x in submissions if x['index'] is not None]
        submissions.sort(key=lambda x: x["id"])
        return submissions

    def get_student(self, student_id, submitted=None):
        """Get a dictionary containing information about the given student.

        Arguments
        ---------
        student_id: string
            The unique id of the student
        submitted: set
            (Optional) A set of unique ids of students who have submitted an assignment

        Returns
        -------
        student: dictionary
            A dictionary containing information about the student, or None if
            the student does not exist

        """
        if submitted is None:
            submitted = self.get_submitted_students("*")

        try:
            with self.gradebook as gb:
                student = gb.find_student(student_id).to_dict()

        except MissingEntry:
            if student_id in submitted:
                student = {
                    "id": student_id,
                    "last_name": None,
                    "first_name": None,
                    "email": None,
                    "lms_user_id": None,
                    "score": 0.0,
                    "max_score": 0.0
                }

            else:
                return None

        return student

    def get_students(self):
        """Get a list containing information about all the students in class.

        Returns
        -------
        students: list
            A list of dictionaries containing information about all the students

        """
        with self.gradebook as gb:
            in_db = set([x.id for x in gb.students])
            students = gb.student_dicts()

        submitted = self.get_submitted_students("*")
        for student_id in (submitted - in_db):
            students.append({
                "id": student_id,
                "last_name": None,
                "first_name": None,
                "email": None,
                "lms_user_id": None,
                "score": 0.0,
                "max_score": 0.0
            })

        students.sort(key=lambda x: (x["last_name"] or "None", x["first_name"] or "None", x["id"]))
        return students

    def get_student_submissions(self, student_id):
        """Get information about all submissions from a particular student.

        Arguments
        ---------
        student_id: string
            The unique id of the student

        Returns
        -------
        submissions: list
            A list of dictionaries containing information about all the student's
            submissions

        """
        # return just an empty list if the student doesn't exist
        submissions = []
        for assignment_id in self.get_source_assignments():
            submission = self.get_submission(assignment_id, student_id)
            submissions.append(submission)

        submissions.sort(key=lambda x: x["name"])
        return submissions

    def get_student_notebook_submissions(self, student_id, assignment_id):
        """Gets information about all notebooks within a submitted assignment.

        Arguments
        ---------
        student_id: string
            The unique id of the student
        assignment_id: string
            The name of the assignment

        Returns
        -------
        submissions: list
            A list of dictionaries containing information about the submissions

        """
        with self.gradebook as gb:
            try:
                assignment = gb.find_submission(assignment_id, student_id)
                student = assignment.student
            except MissingEntry:
                return []

            submissions = []
            for notebook in assignment.notebooks:
                filename = os.path.join(
                    os.path.abspath(self.coursedir.format_path(
                        self.coursedir.autograded_directory,
                        student_id=student_id,
                        assignment_id=assignment_id)),
                    "{}.ipynb".format(notebook.name))

                if os.path.exists(filename):
                    submissions.append(notebook.to_dict())
                else:
                    submissions.append({
                        "id": None,
                        "name": notebook.name,
                        "student": student_id,
                        "last_name": student.last_name,
                        "first_name": student.first_name,
                        "score": 0,
                        "max_score": notebook.max_score,
                        "code_score": 0,
                        "max_code_score": notebook.max_code_score,
                        "written_score": 0,
                        "max_written_score": notebook.max_written_score,
                        "task_score": 0,
                        "max_task_score": notebook.max_task_score,
                        "needs_manual_grade": False,
                        "failed_tests": False,
                        "flagged": False
                    })

        submissions.sort(key=lambda x: x["name"])
        return submissions

    def assign(self, *args, **kwargs):
        """Deprecated, please use `generate_assignment` instead."""
        msg = (
            "The `assign` method is deprecated, please use `generate_assignment` "
            "instead. This method will be removed in a future version of nbgrader.")
        warnings.warn(msg, DeprecationWarning)
        self.log.warning(msg)
        return self.generate_assignment(*args, **kwargs)

    def generate_assignment(self, assignment_id, force=True, create=True):
        """Run ``nbgrader generate_assignment`` for a particular assignment.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment
        force: bool
            Whether to force creating the student version, even if it already
            exists.
        create: bool
            Whether to create the assignment in the database, if it doesn't
            already exist.

        Returns
        -------
        result: dict
            A dictionary with the following keys (error and log may or may not be present):

            - success (bool): whether or not the operation completed successfully
            - error (string): formatted traceback
            - log (string): captured log output

        """
        with temp_attrs(self.coursedir, assignment_id=assignment_id):
            app = GenerateAssignment(coursedir=self.coursedir, parent=self)
            app.force = force
            app.create_assignment = create
            return capture_log(app)

    def unrelease(self, assignment_id):
        """Run ``nbgrader list --remove`` for a particular assignment.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment

        Returns
        -------
        result: dict
            A dictionary with the following keys (error and log may or may not be present):

            - success (bool): whether or not the operation completed successfully
            - error (string): formatted traceback
            - log (string): captured log output

        """
        if sys.platform != 'win32':
            with temp_attrs(self.coursedir, assignment_id=assignment_id):
                app = ExchangeList(
                    coursedir=self.coursedir,
                    authenticator=self.authenticator,
                    parent=self)
                app.remove = True
                return capture_log(app)

    def release(self, *args, **kwargs):
        """Deprecated, please use `release_assignment` instead."""
        msg = (
            "The `release` method is deprecated, please use `release_assignment` "
            "instead. This method will be removed in a future version of nbgrader.")
        warnings.warn(msg, DeprecationWarning)
        self.log.warning(msg)
        return self.release_assignment(*args, **kwargs)

    def release_assignment(self, assignment_id):
        """Run ``nbgrader release_assignment`` for a particular assignment.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment

        Returns
        -------
        result: dict
            A dictionary with the following keys (error and log may or may not be present):

            - success (bool): whether or not the operation completed successfully
            - error (string): formatted traceback
            - log (string): captured log output

        """
        if sys.platform != 'win32':
            with temp_attrs(self.coursedir, assignment_id=assignment_id):
                app = ExchangeReleaseAssignment(
                    coursedir=self.coursedir,
                    authenticator=self.authenticator,
                    parent=self)
                return capture_log(app)

    def collect(self, assignment_id, update=True):
        """Run ``nbgrader collect`` for a particular assignment.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment
        update: bool
            Whether to update already-collected assignments with newer
            submissions, if they exist

        Returns
        -------
        result: dict
            A dictionary with the following keys (error and log may or may not be present):

            - success (bool): whether or not the operation completed successfully
            - error (string): formatted traceback
            - log (string): captured log output

        """
        if sys.platform != 'win32':
            with temp_attrs(self.coursedir, assignment_id=assignment_id):
                app = ExchangeCollect(
                    coursedir=self.coursedir,
                    authenticator=self.authenticator,
                    parent=self)
                app.update = update
                return capture_log(app)

    def autograde(self, assignment_id, student_id, force=True, create=True):
        """Run ``nbgrader autograde`` for a particular assignment and student.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment
        student_id: string
            The unique id of the student
        force: bool
            Whether to autograde the submission, even if it's already been
            autograded
        create: bool
            Whether to create students in the database if they don't already
            exist

        Returns
        -------
        result: dict
            A dictionary with the following keys (error and log may or may not be present):

            - success (bool): whether or not the operation completed successfully
            - error (string): formatted traceback
            - log (string): captured log output

        """
        with temp_attrs(self.coursedir, assignment_id=assignment_id, student_id=student_id):
            app = Autograde(coursedir=self.coursedir, parent=self)
            app.force = force
            app.create_student = create
            return capture_log(app)

    def generate_feedback(self, assignment_id, student_id=None, force=True):
        """Run ``nbgrader generate_feedback`` for a particular assignment and student.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment
        student_id: string
            The name of the student (optional). If not provided, then generate
            feedback from autograded submissions.
        force: bool
            Whether to force generating feedback, even if it already exists.

        Returns
        -------
        result: dict
            A dictionary with the following keys (error and log may or may not be present):

            - success (bool): whether or not the operation completed successfully
            - error (string): formatted traceback
            - log (string): captured log output

        """
        # Because we may be using HTMLExporter.template_file in other
        # parts of the the UI, we need to make sure that the template
        # is explicitply 'feedback.tpl` here:
        c = Config()
        c.HTMLExporter.template_file = 'feedback.tpl'
        if student_id is not None:
            with temp_attrs(self.coursedir,
                            assignment_id=assignment_id,
                            student_id=student_id):
                app = GenerateFeedback(coursedir=self.coursedir, parent=self)
                app.update_config(c)
                app.force = force
                return capture_log(app)
        else:
            with temp_attrs(self.coursedir,
                            assignment_id=assignment_id):
                app = GenerateFeedback(coursedir=self.coursedir, parent=self)
                app.update_config(c)
                app.force = force
                return capture_log(app)

    def release_feedback(self, assignment_id, student_id=None):
        """Run ``nbgrader release_feedback`` for a particular assignment/student.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment
        assignment_id: string
            The name of the student (optional). If not provided, then release
            all generated feedback.

        Returns
        -------
        result: dict
            A dictionary with the following keys (error and log may or may not be present):

            - success (bool): whether or not the operation completed successfully
            - error (string): formatted traceback
            - log (string): captured log output

        """
        if student_id is not None:
            with temp_attrs(self.coursedir, assignment_id=assignment_id, student_id=student_id):
                app = ExchangeReleaseFeedback(
                    coursedir=self.coursedir,
                    authentictor=self.authenticator,
                    parent=self)
                return capture_log(app)
        else:
            with temp_attrs(self.coursedir, assignment_id=assignment_id, student_id='*'):
                app = ExchangeReleaseFeedback(
                    coursedir=self.coursedir,
                    authentictor=self.authenticator,
                    parent=self)
                return capture_log(app)

    def fetch_feedback(self, assignment_id, student_id):
        """Run ``nbgrader fetch_feedback`` for a particular assignment/student.

        Arguments
        ---------
        assignment_id: string
            The name of the assignment
        student_id: string
            The name of the student.

        Returns
        -------
        result: dict
            A dictionary with the following keys (error and log may or may not be present):

            - success (bool): whether or not the operation completed successfully
            - error (string): formatted traceback
            - log (string): captured log output
            - value (list of dict): all submitted assignments

        """
        with temp_attrs(self.coursedir, assignment_id=assignment_id, student_id=student_id):
            app = ExchangeFetchFeedback(
                coursedir=self.coursedir,
                authentictor=self.authenticator,
                parent=self)
            ret_dic = capture_log(app)
            # assignment tab needs a 'value' field with the info needed to repopulate
            # the tables.
        with temp_attrs(self.coursedir, assignment_id='*', student_id=student_id):
            lister_rel = ExchangeList(
                inbound=False, cached=True,
                coursedir=self.coursedir,
                authenticator=self.authenticator,
                config=self.config)
            assignments = lister_rel.start()
            ret_dic["value"] = sorted(assignments, key=lambda x: (x['course_id'], x['assignment_id']))
        return ret_dic
