diff --git a/nbgrader/apps/api.py b/nbgrader/apps/api.py index 37dbc379b..0480647c6 100644 --- a/nbgrader/apps/api.py +++ b/nbgrader/apps/api.py @@ -1066,9 +1066,10 @@ def generate_feedback(self, assignment_id, student_id=None, force=True): """ # 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: + # is explicitly 'feedback.html.j2` here: c = Config() - c.HTMLExporter.template_file = 'feedback.tpl' + c.HTMLExporter.template_name = 'feedback' + c.HTMLExporter.template_file = 'feedback.html.j2' if student_id is not None: with temp_attrs(self.coursedir, assignment_id=assignment_id, diff --git a/nbgrader/apps/nbgraderapp.py b/nbgrader/apps/nbgraderapp.py index ba648fd7b..fb9c5b5c2 100755 --- a/nbgrader/apps/nbgraderapp.py +++ b/nbgrader/apps/nbgraderapp.py @@ -4,6 +4,7 @@ import sys import os +import asyncio from textwrap import dedent from traitlets import default @@ -53,6 +54,12 @@ }) +# See https://bugs.python.org/issue37373 :( +# Workaround from https://github.com/jupyter/nbconvert/issues/1372 +if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'): + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + class NbGraderApp(NbGrader): name = u'nbgrader' diff --git a/nbgrader/converters/autograde.py b/nbgrader/converters/autograde.py index ec002b2ef..7f0ced0da 100644 --- a/nbgrader/converters/autograde.py +++ b/nbgrader/converters/autograde.py @@ -7,7 +7,7 @@ from .base import BaseConverter, NbGraderException from ..preprocessors import ( AssignLatePenalties, ClearOutput, DeduplicateIds, OverwriteCells, SaveAutoGrades, - Execute, LimitOutput, OverwriteKernelspec, CheckCellMetadata) + Execute, LimitOutput, OverwriteKernelspec, CheckCellMetadata, RemoveExecutionInfo) from ..api import Gradebook, MissingEntry from .. import utils @@ -59,6 +59,7 @@ def _output_directory(self) -> str: ]).tag(config=True) autograde_preprocessors = List([ Execute, + RemoveExecutionInfo, LimitOutput, SaveAutoGrades, AssignLatePenalties, diff --git a/nbgrader/converters/generate_feedback.py b/nbgrader/converters/generate_feedback.py index 7bb1459da..75d88cd02 100644 --- a/nbgrader/converters/generate_feedback.py +++ b/nbgrader/converters/generate_feedback.py @@ -55,9 +55,8 @@ def _load_config(self, cfg, **kwargs): def __init__(self, coursedir=None, **kwargs): super(GenerateFeedback, self).__init__(coursedir=coursedir, **kwargs) c = Config() - if 'template_file' not in self.config.HTMLExporter: - c.HTMLExporter.template_file = 'feedback.tpl' - if 'template_path' not in self.config.HTMLExporter: - template_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'server_extensions', 'formgrader', 'templates')) - c.HTMLExporter.template_path = ['.', template_path] + c.HTMLExporter.template_name = 'feedback' + c.HTMLExporter.template_file = 'feedback.html.j2' + template_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'server_extensions', 'formgrader', 'templates')) + c.HTMLExporter.extra_template_basedirs.append(template_path) self.update_config(c) diff --git a/nbgrader/preprocessors/__init__.py b/nbgrader/preprocessors/__init__.py index 1507ee0aa..f58381d47 100644 --- a/nbgrader/preprocessors/__init__.py +++ b/nbgrader/preprocessors/__init__.py @@ -16,6 +16,7 @@ from .clearhiddentests import ClearHiddenTests from .clearmarkingscheme import ClearMarkScheme from .overwritekernelspec import OverwriteKernelspec +from .removeexecutioninfo import RemoveExecutionInfo __all__ = [ "AssignLatePenalties", @@ -35,4 +36,5 @@ "ClearHiddenTests", "ClearMarkScheme", "OverwriteKernelspec", + "RemoveExecutionInfo" ] diff --git a/nbgrader/preprocessors/execute.py b/nbgrader/preprocessors/execute.py index 29b09ea0f..3048b6517 100644 --- a/nbgrader/preprocessors/execute.py +++ b/nbgrader/preprocessors/execute.py @@ -5,7 +5,7 @@ from . import NbGraderPreprocessor from nbconvert.exporters.exporter import ResourcesDict from nbformat.notebooknode import NotebookNode -from typing import Any, Optional, Tuple +from typing import Any, Optional, Tuple, Dict class UnresponsiveKernelError(Exception): @@ -56,50 +56,48 @@ def preprocess(self, return output - def preprocess_cell(self, cell, resources, cell_index, store_history=True): - """ - Need to override preprocess_cell to check reply for errors - """ - # Copied from nbconvert ExecutePreprocessor - if cell.cell_type != 'code' or not cell.source.strip(): - return cell, resources - - reply, outputs = self.run_cell(cell, cell_index, store_history) - # Backwards compatibility for processes that wrap run_cell - cell.outputs = outputs - - cell_allows_errors = (self.allow_errors or "raises-exception" - in cell.metadata.get("tags", [])) - - if self.force_raise_errors or not cell_allows_errors: - if (reply is not None) and reply['content']['status'] == 'error': - raise CellExecutionError.from_cell_and_msg(cell, reply['content']) - - # Ensure errors are recorded to prevent false positives when autograding - if (reply is None) or reply['content']['status'] == 'error': - error_recorded = False - for output in cell.outputs: - if output.output_type == 'error': - error_recorded = True - if not error_recorded: - error_output = NotebookNode(output_type='error') - if reply is None: - # Occurs when - # IPython.core.interactiveshell.InteractiveShell.showtraceback - # = None - error_output.ename = "CellTimeoutError" - error_output.evalue = "" - error_output.traceback = ["ERROR: No reply from kernel"] - else: - # Occurs when - # IPython.core.interactiveshell.InteractiveShell.showtraceback - # = lambda *args, **kwargs : None - error_output.ename = reply['content']['ename'] - error_output.evalue = reply['content']['evalue'] - error_output.traceback = reply['content']['traceback'] - if error_output.traceback == []: - error_output.traceback = ["ERROR: An error occurred while" - " showtraceback was disabled"] - cell.outputs.append(error_output) - - return cell, resources + def _check_raise_for_error( + self, + cell: NotebookNode, + exec_reply: Optional[Dict]) -> None: + + exec_reply_content = None + if exec_reply is not None: + exec_reply_content = exec_reply['content'] + if exec_reply_content['status'] != 'error': + return None + + cell_allows_errors = (not self.force_raise_errors) and ( + self.allow_errors + or exec_reply_content.get('ename') in self.allow_error_names + or "raises-exception" in cell.metadata.get("tags", [])) + + if not cell_allows_errors: + raise CellExecutionError.from_cell_and_msg(cell, exec_reply_content) + + # Ensure errors are recorded to prevent false positives when autograding + if exec_reply is None or exec_reply_content['status'] == 'error': + error_recorded = False + for output in cell.outputs: + if output.output_type == 'error': + error_recorded = True + break + + if not error_recorded: + error_output = NotebookNode(output_type='error') + if exec_reply is None: + # Occurs when + # IPython.core.interactiveshell.InteractiveShell.showtraceback = None + error_output.ename = "CellTimeoutError" + error_output.evalue = "" + error_output.traceback = ["ERROR: No reply from kernel"] + else: + # Occurs when + # IPython.core.interactiveshell.InteractiveShell.showtraceback = lambda *args, **kwargs: None + error_output.ename = exec_reply_content['ename'] + error_output.evalue = exec_reply_content['evalue'] + error_output.traceback = exec_reply_content['traceback'] + if error_output.traceback == []: + error_output.traceback = ["ERROR: An error occurred while" + " showtraceback was disabled"] + cell.outputs.append(error_output) diff --git a/nbgrader/preprocessors/getgrades.py b/nbgrader/preprocessors/getgrades.py index 6dfbbbe5b..2f28bfefb 100644 --- a/nbgrader/preprocessors/getgrades.py +++ b/nbgrader/preprocessors/getgrades.py @@ -42,6 +42,9 @@ def preprocess(self, resources['nbgrader']['max_score'] = notebook.max_score resources['nbgrader']['late_penalty'] = late_penalty + # Set title + nb['metadata']['title'] = resources['nbgrader']['notebook'] + return nb, resources def _get_comment(self, cell: NotebookNode, resources: ResourcesDict) -> None: diff --git a/nbgrader/preprocessors/removeexecutioninfo.py b/nbgrader/preprocessors/removeexecutioninfo.py new file mode 100644 index 000000000..b07ee3a7c --- /dev/null +++ b/nbgrader/preprocessors/removeexecutioninfo.py @@ -0,0 +1,19 @@ +from . import NbGraderPreprocessor + +from traitlets import Integer +from nbformat.notebooknode import NotebookNode +from nbconvert.exporters.exporter import ResourcesDict +from typing import Tuple + + +class RemoveExecutionInfo(NbGraderPreprocessor): + """Preprocessor for removing execution info.""" + + def preprocess_cell(self, + cell: NotebookNode, + resources: ResourcesDict, + cell_index: int + ) -> Tuple[NotebookNode, ResourcesDict]: + if 'execution' in cell['metadata']: + del cell['metadata']['execution'] + return cell, resources diff --git a/nbgrader/server_extensions/formgrader/base.py b/nbgrader/server_extensions/formgrader/base.py index 795add503..5526f15ab 100644 --- a/nbgrader/server_extensions/formgrader/base.py +++ b/nbgrader/server_extensions/formgrader/base.py @@ -62,19 +62,19 @@ def render(self, name, **ns): def write_error(self, status_code, **kwargs): if status_code == 500: html = self.render( - 'base_500.tpl', + 'pages/base_500.html.j2', base_url=self.base_url, error_code=500) elif status_code == 502: html = self.render( - 'base_500.tpl', + 'pages/base_500.html.j2', base_url=self.base_url, error_code=502) elif status_code == 403: html = self.render( - 'base_403.tpl', + 'pages/base_403.html.j2', base_url=self.base_url, error_code=403) diff --git a/nbgrader/server_extensions/formgrader/formgrader.py b/nbgrader/server_extensions/formgrader/formgrader.py index 7ba769aef..dc0254f3c 100644 --- a/nbgrader/server_extensions/formgrader/formgrader.py +++ b/nbgrader/server_extensions/formgrader/formgrader.py @@ -25,13 +25,17 @@ def _classes_default(self): def build_extra_config(self): extra_config = super(FormgradeExtension, self).build_extra_config() - extra_config.HTMLExporter.template_file = 'formgrade' - extra_config.HTMLExporter.template_path = [handlers.template_path] + extra_config.HTMLExporter.template_name = 'formgrade' + extra_config.HTMLExporter.template_file = 'formgrade.html.j2' + extra_config.HTMLExporter.extra_template_basedirs.append(handlers.template_path) return extra_config def init_tornado_settings(self, webapp): # Init jinja environment - jinja_env = Environment(loader=FileSystemLoader([handlers.template_path])) + jinja_env = Environment(loader=FileSystemLoader([ + os.path.join(handlers.template_path, 'formgrade'), + os.path.join(handlers.template_path, 'pages') + ])) course_dir = self.coursedir.root notebook_dir = self.parent.notebook_dir diff --git a/nbgrader/server_extensions/formgrader/handlers.py b/nbgrader/server_extensions/formgrader/handlers.py index 2143eca17..025ec0ecb 100644 --- a/nbgrader/server_extensions/formgrader/handlers.py +++ b/nbgrader/server_extensions/formgrader/handlers.py @@ -14,7 +14,7 @@ class ManageAssignmentsHandler(BaseHandler): @check_notebook_dir def get(self): html = self.render( - "manage_assignments.tpl", + "manage_assignments.html.j2", url_prefix=self.url_prefix, base_url=self.base_url, windows=(sys.prefix == 'win32'), @@ -30,7 +30,7 @@ class ManageSubmissionsHandler(BaseHandler): @check_notebook_dir def get(self, assignment_id): html = self.render( - "manage_submissions.tpl", + "manage_submissions.html.j2", course_dir=self.coursedir.root, assignment_id=assignment_id, base_url=self.base_url) @@ -43,7 +43,7 @@ class GradebookAssignmentsHandler(BaseHandler): @check_notebook_dir def get(self): html = self.render( - "gradebook_assignments.tpl", + "gradebook_assignments.html.j2", base_url=self.base_url) self.write(html) @@ -54,7 +54,7 @@ class GradebookNotebooksHandler(BaseHandler): @check_notebook_dir def get(self, assignment_id): html = self.render( - "gradebook_notebooks.tpl", + "gradebook_notebooks.html.j2", assignment_id=assignment_id, base_url=self.base_url) self.write(html) @@ -66,7 +66,7 @@ class GradebookNotebookSubmissionsHandler(BaseHandler): @check_notebook_dir def get(self, assignment_id, notebook_id): html = self.render( - "gradebook_notebook_submissions.tpl", + "gradebook_notebook_submissions.html.j2", assignment_id=assignment_id, notebook_id=notebook_id, base_url=self.base_url @@ -116,7 +116,7 @@ def get(self, submission_id): if not os.path.exists(filename): resources['filename'] = filename - html = self.render('formgrade_404.tpl', resources=resources) + html = self.render('formgrade_404.html.j2', resources=resources) self.clear() self.set_status(404) self.write(html) @@ -235,7 +235,7 @@ class ManageStudentsHandler(BaseHandler): @check_notebook_dir def get(self): html = self.render( - "manage_students.tpl", + "manage_students.html.j2", base_url=self.base_url) self.write(html) @@ -246,7 +246,7 @@ class ManageStudentsAssignmentsHandler(BaseHandler): @check_notebook_dir def get(self, student_id): html = self.render( - "manage_students_assignments.tpl", + "manage_students_assignments.html.j2", student_id=student_id, base_url=self.base_url) @@ -259,7 +259,7 @@ class ManageStudentNotebookSubmissionsHandler(BaseHandler): @check_notebook_dir def get(self, student_id, assignment_id): html = self.render( - "manage_students_notebook_submissions.tpl", + "manage_students_notebook_submissions.html.j2", assignment_id=assignment_id, student_id=student_id, base_url=self.base_url diff --git a/nbgrader/server_extensions/formgrader/templates/feedback/conf.json b/nbgrader/server_extensions/formgrader/templates/feedback/conf.json new file mode 100644 index 000000000..aa66ccd95 --- /dev/null +++ b/nbgrader/server_extensions/formgrader/templates/feedback/conf.json @@ -0,0 +1,7 @@ +{ + "base_template": "classic", + "mimetypes": { + "text/html": true + }, + "preprocessors": {} +} \ No newline at end of file diff --git a/nbgrader/server_extensions/formgrader/templates/feedback.tpl b/nbgrader/server_extensions/formgrader/templates/feedback/feedback.html.j2 similarity index 85% rename from nbgrader/server_extensions/formgrader/templates/feedback.tpl rename to nbgrader/server_extensions/formgrader/templates/feedback/feedback.html.j2 index f28c54e5e..dd424cdc6 100644 --- a/nbgrader/server_extensions/formgrader/templates/feedback.tpl +++ b/nbgrader/server_extensions/formgrader/templates/feedback/feedback.html.j2 @@ -1,60 +1,12 @@ -{%- extends 'basic.tpl' -%} -{% from 'mathjax.tpl' import mathjax %} - -{%- block header -%} - - -
- - -