From 507dd0e16f16588817a4462cdcc0d90540404e95 Mon Sep 17 00:00:00 2001 From: Jens Flemming Date: Wed, 5 Jul 2023 11:37:18 +0200 Subject: [PATCH] Show course title instead of course ID in course list and assignment list GUI --- .../global_nbgrader_config.py | 4 ++++ nbgrader/apps/baseapp.py | 12 +++++++++- nbgrader/coursedir.py | 15 ++++++++++++ .../assignment_list/assignment_list.js | 8 +++---- .../nbextensions/course_list/course_list.js | 9 ++++---- .../assignment_list/handlers.py | 15 +++++++++++- .../server_extensions/course_list/handlers.py | 23 +++++++++++++++++++ src/assignment_list/assignmentlist.ts | 18 +++++++-------- src/course_list/courselist.ts | 2 +- 9 files changed, 86 insertions(+), 20 deletions(-) diff --git a/demos/demo_multiple_classes/global_nbgrader_config.py b/demos/demo_multiple_classes/global_nbgrader_config.py index ffb5df4a8..9c44edf70 100644 --- a/demos/demo_multiple_classes/global_nbgrader_config.py +++ b/demos/demo_multiple_classes/global_nbgrader_config.py @@ -2,3 +2,7 @@ c = get_config() c.Exchange.path_includes_course = True c.Authenticator.plugin_class = JupyterHubAuthPlugin +c.NbGrader.course_titles = { + 'course101': 'Course 101', + 'course123': 'Course 123' +} diff --git a/nbgrader/apps/baseapp.py b/nbgrader/apps/baseapp.py index 48585fbf7..5be826c17 100644 --- a/nbgrader/apps/baseapp.py +++ b/nbgrader/apps/baseapp.py @@ -12,7 +12,7 @@ from jupyter_core.application import JupyterApp from textwrap import dedent from tornado.log import LogFormatter -from traitlets import Unicode, List, Bool, Instance, default +from traitlets import Unicode, List, Bool, Instance, default, Dict from traitlets.config.application import catch_config_error from traitlets.config.loader import Config @@ -66,6 +66,16 @@ class NbGrader(JupyterApp): _log_formatter_cls = LogFormatter + course_titles = Dict( + {}, + help=dedent( + """ + Dict mapping course IDs to human readable course titles. If there is + no title for a course, ID is shown. + """ + ) + ).tag(config=True) + @default("log_level") def _log_level_default(self) -> int: return logging.INFO diff --git a/nbgrader/coursedir.py b/nbgrader/coursedir.py index bbbe6c54d..b5870a6d5 100644 --- a/nbgrader/coursedir.py +++ b/nbgrader/coursedir.py @@ -31,6 +31,21 @@ def _validate_course_id(self, proposal): self.log.warning("course_id '%s' has trailing whitespace, stripping it away", proposal['value']) return proposal['value'].strip() + course_title = Unicode( + '', + help=dedent( + """ + A human readable course name for display purposes. + """ + ) + ).tag(config=True) + + @validate('course_title') + def _validate_course_title(self, proposal): + if proposal['value'].strip() != proposal['value']: + self.log.warning("course_title '%s' has trailing whitespace, stripping it away", proposal['value']) + return proposal['value'].strip() + student_id = Unicode( "*", help=dedent( diff --git a/nbgrader/nbextensions/assignment_list/assignment_list.js b/nbgrader/nbextensions/assignment_list/assignment_list.js index b35cbb39c..99bb34368 100644 --- a/nbgrader/nbextensions/assignment_list/assignment_list.js +++ b/nbgrader/nbextensions/assignment_list/assignment_list.js @@ -115,12 +115,12 @@ define([ CourseList.prototype.change_course = function (course) { this.disable_list(); if (this.current_course !== undefined) { - this.default_course_element.text(course); + this.default_course_element.text(course['course_title']); } this.current_course = course; - this.default_course_element.text(this.current_course); + this.default_course_element.text(this.current_course['course_title']); var success = $.proxy(this.load_assignment_list_success, this); - this.assignment_list.load_list(course, success); + this.assignment_list.load_list(course['course_id'], success); }; @@ -132,7 +132,7 @@ define([ } for (var i=0; i').append($('').attr("href", "#").text(this.data[i])); + var element = $('
  • ').append($('').attr("href", "#").text(this.data[i]['course_title'])); element.click(set_course(this.data[i])); this.course_list_element.append(element); } diff --git a/nbgrader/nbextensions/course_list/course_list.js b/nbgrader/nbextensions/course_list/course_list.js index 5bd8a044f..4b591a376 100644 --- a/nbgrader/nbextensions/course_list/course_list.js +++ b/nbgrader/nbextensions/course_list/course_list.js @@ -108,6 +108,7 @@ define([ var Course = function (element, data, parent, on_refresh, options) { this.element = $(element); this.course_id = data['course_id']; + this.course_title = data['course_title']; this.formgrader_kind = data['kind']; this.url = data['url']; this.parent = parent; @@ -123,17 +124,17 @@ define([ Course.prototype.make_row = function () { var row = $('
    ').addClass('col-md-12'); - var course_id = this.course_id; + var course_title = this.course_title; - if(course_id === '') { - course_id = 'Default formgrader'; + if(course_title === '') { + course_title = 'Default formgrader'; } var container = $('').addClass('item_name col-sm-2').append( $('') .attr('href', this.url) .attr('target', '_blank') - .text(course_id)); + .text(course_title)); row.append(container); row.append($('').addClass('item_course col-sm-2').text(this.formgrader_kind)); this.element.empty().append(row); diff --git a/nbgrader/server_extensions/assignment_list/handlers.py b/nbgrader/server_extensions/assignment_list/handlers.py index 49b00288c..20d79ad3e 100644 --- a/nbgrader/server_extensions/assignment_list/handlers.py +++ b/nbgrader/server_extensions/assignment_list/handlers.py @@ -51,6 +51,16 @@ def load_config(self): return app.config + def get_course_titles(self): + paths = jupyter_config_path() + paths.insert(0, os.getcwd()) + + app = NbGrader() + app.config_file_paths.append(paths) + app.load_config_file() + + return app.course_titles + @contextlib.contextmanager def get_assignment_dir_config(self): @@ -186,9 +196,12 @@ def list_courses(self): if not assignments["success"]: return assignments + course_ids = list(set([x["course_id"] for x in assignments["value"]])) + titles_dict = self.get_course_titles() + course_ids.sort(key=lambda x: titles_dict.get(x, x)) retvalue = { "success": True, - "value": sorted(list(set([x["course_id"] for x in assignments["value"]]))) + "value": [{"course_id": x, "course_title": titles_dict.get(x, x)} for x in course_ids] } return retvalue diff --git a/nbgrader/server_extensions/course_list/handlers.py b/nbgrader/server_extensions/course_list/handlers.py index a06f9bd3b..eb100ac4a 100644 --- a/nbgrader/server_extensions/course_list/handlers.py +++ b/nbgrader/server_extensions/course_list/handlers.py @@ -51,6 +51,16 @@ def load_config(self): app.load_config_file() return app.config + + def get_course_titles(self): + paths = jupyter_config_path() + paths.insert(0, os.getcwd()) + + app = NbGrader() + app.config_file_paths.append(paths) + app.load_config_file() + + return app.course_titles @gen.coroutine def check_for_local_formgrader(self, config): @@ -77,8 +87,12 @@ def check_for_local_formgrader(self, config): coursedir = CourseDirectory(config=config) if status: + title = coursedir.course_title + if not title: + title = coursedir.course_id raise gen.Return([{ 'course_id': coursedir.course_id, + 'course_title': title, 'url': base_url + '/formgrader', 'kind': 'local' }]) @@ -111,8 +125,12 @@ def check_for_noauth_jupyterhub_formgraders(self, config): self.log.error("Formgrader not available at URL: %s", url) raise gen.Return([]) + title = coursedir.course_title + if not title: + title = coursedir.course_id courses = [{ 'course_id': coursedir.course_id, + 'course_title': title, 'url': url, 'kind': 'jupyterhub' }] @@ -149,13 +167,18 @@ def check_for_jupyterhub_formgraders(self, config): raise gen.Return([]) courses = [] + course_titles = self.get_course_titles() for course in course_names: if course not in services: self.log.warning("Couldn't find formgrader for course '%s'", course) continue service = services[course] + title = course_titles.get(course) + if not title: + title = course courses.append({ 'course_id': course, + 'course_title': title, 'url': self.get_base_url() + service['prefix'].rstrip('/') + "/formgrader", 'kind': 'jupyterhub' }) diff --git a/src/assignment_list/assignmentlist.ts b/src/assignment_list/assignmentlist.ts index ff3f6cd68..5e9689739 100644 --- a/src/assignment_list/assignmentlist.ts +++ b/src/assignment_list/assignmentlist.ts @@ -580,10 +580,10 @@ export class CourseList{ dropdown_selector: string; refresh_selector: string; assignment_list: AssignmentList; - current_course: string; + current_course: { [key: string]: string } options = new Map(); base_url: string; - data : string[]; + data : { [key: string]: string }[]; course_list_element : HTMLUListElement; default_course_element: HTMLButtonElement; dropdown_element: HTMLButtonElement; @@ -673,7 +673,7 @@ private handle_load_list(data: { success: any; value: any; }): void { } }; -private load_list_success(data: string[]): void { +private load_list_success(data: { [key: string]: string }[]): void { this.data = data; this.disable_list() this.clear_list(); @@ -698,28 +698,28 @@ private load_list_success(data: string[]): void { } }; -private change_course(course: string): void { +private change_course(course: { [key: string]: string }): void { this.disable_list(); if (this.current_course !== undefined) { - this.default_course_element.innerText = course; + this.default_course_element.innerText = course['course_title']; } this.current_course = course; - this.default_course_element.innerText = this.current_course; + this.default_course_element.innerText = this.current_course['course_title']; var success = ()=>{this.load_assignment_list_success()}; - this.assignment_list.load_list(course, success); + this.assignment_list.load_list(course['course_id'], success); }; private load_assignment_list_success(): void { if (this.data) { var that = this; - var set_course = function (course: string) { + var set_course = function (course: { [key: string]: string }) { return function () { that.change_course(course); }; } for (var i=0; i