diff --git a/nbgrader/exchange/default/list.py b/nbgrader/exchange/default/list.py
index 24b15b015..f918a024a 100644
--- a/nbgrader/exchange/default/list.py
+++ b/nbgrader/exchange/default/list.py
@@ -3,6 +3,7 @@
import shutil
import re
import hashlib
+from lxml import html
from nbgrader.exchange.abc import ExchangeList as ABCExchangeList
from nbgrader.utils import notebook_hash, make_unique_key
@@ -14,6 +15,13 @@ def _checksum(path):
m.update(open(path, 'rb').read())
return m.hexdigest()
+def get_meta_value(html_data, key):
+ document = html.fromstring(html_data)
+ meta_content = document.xpath(f'//meta[@name="nbgrader-{key}"]/@content')
+ if meta_content:
+ return meta_content[0]
+ return None
+
class ExchangeList(ABCExchangeList, Exchange):
@@ -209,11 +217,22 @@ def parse_assignments(self):
for key in assignment_keys:
submissions = [x for x in assignments if _match_key(x, key)]
submissions = sorted(submissions, key=lambda x: x['timestamp'])
+
+ submisstions_with_feedback = [x for x in submissions if x['has_local_feedback']]
+ score, max_score = None, None
+ if len(submisstions_with_feedback) > 0 and submisstions_with_feedback[-1]['local_feedback_path'] is not None:
+ feedback_file = os.path.join(submisstions_with_feedback[-1]['local_feedback_path'], key[2] + ".html")
+ feedback_html = open(feedback_file, 'r').read()
+ score = get_meta_value(feedback_html, 'score')
+ max_score = get_meta_value(feedback_html, 'max-score')
+
info = {
'course_id': key[0],
'student_id': key[1],
'assignment_id': key[2],
'status': submissions[0]['status'],
+ 'score': float(score) if score is not None else None,
+ 'max_score': float(max_score) if max_score is not None else None,
'submissions': submissions
}
assignment_submissions.append(info)
diff --git a/nbgrader/server_extensions/formgrader/templates/feedback/index.html.j2 b/nbgrader/server_extensions/formgrader/templates/feedback/index.html.j2
index 24f7a4249..08af9f2f3 100644
--- a/nbgrader/server_extensions/formgrader/templates/feedback/index.html.j2
+++ b/nbgrader/server_extensions/formgrader/templates/feedback/index.html.j2
@@ -6,6 +6,8 @@
+
+
{{ resources.nbgrader.notebook }}
{{ resources.include_css('bootstrap.min.css')}}
diff --git a/pyproject.toml b/pyproject.toml
index 645a936a1..6edbf2bde 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,6 +49,7 @@ dependencies = [
"setuptools",
"sqlalchemy>=1.4,<3",
"PyYAML>=6.0",
+ "lxml>=5.3.0",
]
version = "0.9.3"
diff --git a/src/assignment_list/assignmentlist.ts b/src/assignment_list/assignmentlist.ts
index 7c61db091..6573c9ea1 100644
--- a/src/assignment_list/assignmentlist.ts
+++ b/src/assignment_list/assignmentlist.ts
@@ -88,6 +88,9 @@ export class AssignmentList {
private load_list_success(data: string | any[]): void {
this.clear_list(false);
+ var total_score = 0;
+ var total_max_score = 0;
+ var show_score = false;
var len = data.length;
for (var i=0; ithis.submitted_element.children.namedItem('submitted_assignments_list_placeholder')).hidden = true;
+
+ if (data[i]['score'] != null && data[i]['max_score'] != null) {
+ total_score += data[i]['score'];
+ total_max_score += data[i]['max_score'];
+ show_score = true;
+ }
}
}
+
+ var score_heading_element = document.getElementById(this.options.get('score_heading_id'));
+ var total_score_container = document.getElementById(this.options.get('total_score_container_id'));
+ var total_score_element = document.getElementById(this.options.get('total_score_id'));
+
+ if (score_heading_element) {
+ score_heading_element.style.visibility = show_score ? 'visible' : 'hidden';
+ }
+ if (total_score_container) {
+ total_score_container.style.visibility = show_score ? 'visible' : 'hidden';
+ }
+ if (total_score_element) {
+ total_score_element.innerText = `${total_score}/${total_max_score}`;
+ }
var assignments = this.fetched_element.getElementsByClassName('assignment-notebooks-link');
for(let a of assignments){
@@ -226,7 +249,7 @@ class Assignment {
private make_link(): HTMLSpanElement {
var container = document.createElement('span');;
- container.classList.add('item_name', 'col-sm-6');
+ container.classList.add('item_name', 'col-sm-4');
var link;
if (this.data['status'] === 'fetched') {
@@ -360,10 +383,18 @@ class Assignment {
s.classList.add('item_course', 'col-sm-2')
s.innerText = this.data['course_id']
row.append(s)
+ var score = document.createElement('span');
+ score.classList.add('item_status', 'col-sm-2');
+ score.setAttribute('style', 'text-align:left');
+ row.append(score);
var id, element;
var children = document.createElement('div');
if (this.data['status'] == 'submitted') {
+ if (this.data['score'] != null && this.data['max_score'] != null) {
+ score.innerText = this.data['score'] + '/' + this.data['max_score'];
+ }
+
id = this.escape_id() + '-submissions';
children.id = id;
children.classList.add('panel-collapse', 'list_container', 'assignment-notebooks');
diff --git a/src/assignment_list/index.ts b/src/assignment_list/index.ts
index f682ff939..d9c5fda31 100644
--- a/src/assignment_list/index.ts
+++ b/src/assignment_list/index.ts
@@ -82,7 +82,8 @@ export class AssignmentListWidget extends Widget {
' ',
' ',
'
',
- ' Submitted assignments',
+ ' Submitted assignments',
+ ' Score',
'
',
'
',
'
',
@@ -97,6 +98,10 @@ export class AssignmentListWidget extends Widget {
'
',
'
',
'
',
+ ' ',
+ ' Total Score',
+ ' ',
+ '
',
' ',
' ',
''
@@ -108,6 +113,9 @@ export class AssignmentListWidget extends Widget {
let base_url = PageConfig.getBaseUrl();
let options = new Map();
options.set('base_url',base_url);
+ options.set('score_heading_id', 'score-heading');
+ options.set('total_score_container_id', 'total-score-row');
+ options.set('total_score_id', 'total-score');
var assignment_l = new AssignmentList(this,
'released_assignments_list',
'fetched_assignments_list',