import glob
import re
import sys
import os
import logging
import warnings
from traitlets.config import LoggingConfigurable, Config, get_config
from traitlets import Instance, Enum, Unicode, observe
from ..coursedir import CourseDirectory
from ..converters import GenerateAssignment, Autograde, GenerateFeedback, GenerateSolution
from ..exchange import ExchangeFactory, ExchangeError
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
[docs]
class NbGraderAPI(LoggingConfigurable):
"""A high-level API for using nbgrader."""
coursedir = Instance(CourseDirectory, allow_none=True)
authenticator = Instance(Authenticator, allow_none=True)
exchange = Instance(ExchangeFactory, 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, str):
new = getattr(logging, new)
self.log_level = new
self.log.setLevel(new)
[docs]
def __init__(self, coursedir=None, authenticator=None, exchange=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.
exchange : :class:~`nbgrader.exchange.ExchangeFactory`
(Optional) A factory for creating the exchange classes used
for distributing assignments and feedback.
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 exchange is None:
self.exchange = ExchangeFactory(parent=self)
else:
self.exchange = exchange
if sys.platform != 'win32':
lister = self.exchange.List(
coursedir=self.coursedir,
authenticator=self.authenticator,
parent=self)
self.course_id = self.coursedir.course_id
if hasattr(lister, "root"):
self.exchange_root = lister.root
else:
# For non-fs based exchanges
self.exchange_root = ''
try:
lister.start()
except ExchangeError:
self.exchange_missing = True
else:
self.exchange_missing = False
else:
self.course_id = ''
self.exchange_root = ''
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)
[docs]
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
[docs]
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 = self.exchange.List(
coursedir=self.coursedir,
authenticator=self.authenticator,
parent=self)
released = set([x['assignment_id'] for x in lister.start()])
else:
released = set([])
return released
[docs]
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
[docs]
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())
[docs]
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
[docs]
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
[docs]
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
[docs]
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
[docs]
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
[docs]
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 = self.exchange.Submit(
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)
[docs]
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)])
[docs]
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
[docs]
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
[docs]
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
[docs]
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
[docs]
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
[docs]
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)
[docs]
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 = self.exchange.List(
coursedir=self.coursedir,
authenticator=self.authenticator,
parent=self)
app.remove = True
return capture_log(app)
[docs]
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 = self.exchange.ReleaseAssignment(
coursedir=self.coursedir,
authenticator=self.authenticator,
parent=self)
return capture_log(app)
[docs]
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 = self.exchange.Collect(
coursedir=self.coursedir,
authenticator=self.authenticator,
parent=self)
app.update = update
return capture_log(app)
[docs]
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)
[docs]
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_name in other
# parts of the the UI, we need to make sure that the template
# is explicitply 'feedback` here:
c = Config()
c.HTMLExporter.template_name = 'feedback'
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)
[docs]
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 = self.exchange.ReleaseFeedback(
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 = self.exchange.ReleaseFeedback(
coursedir=self.coursedir,
authentictor=self.authenticator,
parent=self)
return capture_log(app)
[docs]
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 = self.exchange.FetchFeedback(
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 = self.exchange.List(
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