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',