Skip to content

Commit

Permalink
Merge pull request #111 from DigiKlausur/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
tmetzl authored Aug 8, 2022
2 parents c2c4cf8 + a505c72 commit 3a1ba76
Show file tree
Hide file tree
Showing 44 changed files with 1,801 additions and 454 deletions.
5 changes: 1 addition & 4 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
python-version: [3.7, 3.8, 3.9]

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install custom nbgrader
run: |
pip install --no-cache-dir git+https://github.com/jupyter/nbgrader.git@master
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/sonar-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install custom nbgrader
run: |
pip install --no-cache-dir git+https://github.com/jupyter/nbgrader.git@master
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
47 changes: 46 additions & 1 deletion e2xgrader/apps/api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,55 @@
import os
from traitlets.config import Config
from nbgrader.apps.api import NbGraderAPI
from nbgrader.api import BaseCell, Grade, GradeCell, MissingEntry
from nbgrader.utils import as_timezone, to_numeric_tz
from nbgrader.utils import as_timezone, to_numeric_tz, temp_attrs, capture_log
from nbgrader.converters import GenerateFeedback
from ..exporters import E2xExporter


class E2xAPI(NbGraderAPI):
def generate_feedback(
self, assignment_id, student_id=None, force=True, hidecells=False
):
"""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_name = "feedback"
c.FilterTests.hide_cells = hidecells
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 get_solution_cell_ids(self, assignment_id, notebook_id):
"""Get information about the solution cells of a notebook
given its name.
Expand Down
26 changes: 20 additions & 6 deletions e2xgrader/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
from ..exporters import E2xExporter
from ..server_extensions.formgrader.handlers import (
template_path as e2x_template_path,
)
from nbgrader.server_extensions.formgrader.handlers import (
template_path as nbgrader_template_path,
)


def configure_feedback(config):
config.GenerateFeedback.preprocessors = [
"nbgrader.preprocessors.GetGrades",
"e2xgrader.preprocessors.FilterTests",
"nbconvert.preprocessors.CSSHTMLHeaderPreprocessor",
]
config.GenerateFeedback.exporter_class = E2xExporter
config.HTMLExporter.extra_template_basedirs = [
e2x_template_path,
nbgrader_template_path,
]


def configure_base(config):
Expand Down Expand Up @@ -33,12 +52,7 @@ def configure_base(config):
"nbgrader.preprocessors.ComputeChecksums",
"nbgrader.preprocessors.CheckCellMetadata",
]
config.GenerateFeedback.preprocessors = [
"nbgrader.preprocessors.GetGrades",
"e2xgrader.preprocessors.FilterTests",
"nbconvert.preprocessors.CSSHTMLHeaderPreprocessor",
]
config.GenerateFeedback.exporter_class = E2xExporter
configure_feedback(config)


def configure_exchange(config):
Expand Down
2 changes: 1 addition & 1 deletion e2xgrader/exchange/release_assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def copy_files(self):
self.log.info(f"Source: {src_assignment}")
self.log.info(f"Destination: {released_user_assignment}")
self.do_copy(src_assignment, released_user_assignment)
self.set_released_assignment_perm(released_assignment_root)
self.set_released_assignment_perm(released_user_assignment)
else:
self.log.info(f"Src assignment not found: {src_assignment}")
else:
Expand Down
2 changes: 1 addition & 1 deletion e2xgrader/exchange/submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def copy_files(self):
)

if os.path.isfile(student_notebook_file):
nb = nbformat.read(student_notebook_file, as_version=4)
nb = nbformat.read(student_notebook_file, as_version=nbformat.NO_CONVERT)
nb = append_timestamp(nb, self.timestamp)
nbformat.write(nb, student_notebook_file)
hashcode = truncate_hashcode(
Expand Down
2 changes: 1 addition & 1 deletion e2xgrader/exchange/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def generate_student_info(filename, username, hashcode, timestamp):

def generate_html(nb, dest):
exporter = E2xExporter()
exporter.template_file = "form.tpl"
exporter.template_name = "form"
html, _ = exporter.from_notebook_node(nb)

with open(dest, "w") as f:
Expand Down
52 changes: 31 additions & 21 deletions e2xgrader/exporters/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import glob

from traitlets import Unicode
from nbconvert.exporters.html import HTMLExporter
from jinja2 import contextfilter
from nbconvert.exporters import HTMLExporter
from jinja2.filters import pass_context
from bs4 import BeautifulSoup
from nbgrader.server_extensions.formgrader import handlers as nbgrader_handlers

Expand All @@ -15,7 +15,7 @@

class E2xExporter(HTMLExporter):
"""
My custom exporter
Custom E2x notebook exporter
"""

extra_cell_field = Unicode(
Expand All @@ -25,23 +25,35 @@ class E2xExporter(HTMLExporter):
def __init__(self, **kwargs):
super().__init__(**kwargs)
if kwargs and "config" in kwargs and "HTMLExporter" in kwargs["config"]:
self.template_file = kwargs["config"].HTMLExporter.template_file
self.template_path.extend(
[
os.path.abspath(
os.path.join(
os.path.dirname(__file__),
"..",
"server_extensions",
"formgrader",
"templates",
)
self.template_name = kwargs["config"].HTMLExporter.template_name
self.extra_template_basedirs = [
os.path.abspath(
os.path.join(
os.path.dirname(__file__),
"..",
"server_extensions",
"formgrader",
"templates",
)
]
+ [nbgrader_handlers.template_path]
)
),
nbgrader_handlers.template_path,
]
# The notebook seems to sometimes set exclude_input to true
self.exclude_input = False

@property
def template_paths(self):
return super()._template_paths() + [
os.path.join(
os.path.dirname(__file__),
"..",
"server_extensions",
"formgrader",
"templates",
)
]

@contextfilter
@pass_context
def to_choicecell(self, context, source):
cell = context.get("cell", {})
soup = BeautifulSoup(source, "html.parser")
Expand Down Expand Up @@ -87,9 +99,6 @@ def default_filters(self):
yield pair
yield ("to_choicecell", self.to_choicecell)

def _template_file_default(self):
return "formgrade.tpl"

def discover_annotations(self, resources):
if resources is None:
return
Expand All @@ -106,6 +115,7 @@ def discover_annotations(self, resources):

def from_notebook_node(self, nb, resources=None, **kw):
self.discover_annotations(resources)
self.exclude_input = False
langinfo = nb.metadata.get("language_info", {})
lexer = langinfo.get("pygments_lexer", langinfo.get("name", None))
highlight_code = self.filters.get(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
define([], function() {

class Observable {

constructor() {
this.observers = [];
}

registerObserver(observer) {
this.observers.push(observer);
}

notifyAll(event) {
this.observers.forEach(observer => observer.notify(event));
}

}

class AttachmentModel extends Observable {

constructor(cell) {
super();
this.ids = {};
this.cell = cell;
this.typePattern = /data:([^;]*)/;
this.imagePattern = /!\[[^\(]+\]\(attachment:[^)]+\)/g;
this.infoPattern = /\n### You uploaded \d+ attachments.\n\n/g;
this.attachments = {};
this.load();
}

load() {
let that = this;
this.id = 0;
Object.assign(this.attachments, this.cell.attachments);
Object.keys(this.attachments).forEach(function(key) {
that.id += 1;
that.ids[key] = that.id;
});
}

save() {
if (this.attachments !== undefined) {
this.cell.attachments = this.attachments;
console.log('Saved attachments.');

} else {
console.log('Attachments not defined.');
}
this.postSaveHook();
}

postSaveHook() {
// Invoked after attachments are saved

}

hasAttachment(key) {
return key in this.attachments;
}

getAttachment(key) {
return {
id: this.ids[key],
name: key,
type: Object.keys(this.attachments[key])[0],
data: Object.values(this.attachments[key])[0]
}
}

setAttachment(key, dataUrl) {
let type = this.typePattern.exec(dataUrl)[1];
let data = dataUrl.replace('data:' + type + ';base64,', '');
this.id += 1;
this.attachments[key] = {};
this.attachments[key][type] = data;
this.ids[key] = this.id;
this.notifyAll({
type: 'add',
key: key,
id: this.ids[key]
});
this.save();
}

removeAttachment(key) {
let id = this.ids[key];
delete this.attachments[key];
delete this.ids[key];
this.notifyAll({
type: 'delete',
key: key,
id: id
});
this.save();
}

getAttachments() {
let that = this;
let attachments = [];
Object.keys(this.attachments).forEach(function(key) {
attachments.push(that.getAttachment(key));
});
return attachments;
}
}

class AttachmentCellModel extends AttachmentModel {

postSaveHook() {
let cleaned = this.cell.get_text().replace(this.imagePattern, '');
cleaned = cleaned.replace(this.infoPattern, '');
let n_attachments = Object.keys(this.attachments).length;
cleaned += '\n### You uploaded ' + n_attachments + ' attachments.\n\n'
this.cell.set_text(cleaned);
this.cell.unrender_force();
this.cell.render();
}

getName(name, type) {
let current_name = name + '.' + type;
let counter = 0;
while (current_name in this.attachments) {
counter += 1;
current_name = name + '_' + counter + '.' + type;
}
return current_name;
}

}

class DiagramCellModel extends AttachmentModel {

postSaveHook() {
this.cell.unrender_force();
this.cell.render();
}

}

return {
AttachmentModel: AttachmentModel,
AttachmentCellModel: AttachmentCellModel,
DiagramCellModel: DiagramCellModel
}

});
Loading

0 comments on commit 3a1ba76

Please sign in to comment.