Skip to content

Commit

Permalink
Merge pull request #1113 from jhamrick/course-list
Browse files Browse the repository at this point in the history
Add a course list extension that shows all courses an instructor can manage
  • Loading branch information
jhamrick authored Jun 1, 2019
2 parents 4b7013a + 1e732e6 commit 304a001
Show file tree
Hide file tree
Showing 12 changed files with 727 additions and 18 deletions.
9 changes: 9 additions & 0 deletions nbgrader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ def _jupyter_nbextension_paths():
require="assignment_list/main"
)
)
paths.append(
dict(
section="tree",
src=os.path.join('nbextensions', 'course_list'),
dest="course_list",
require="course_list/main"
)
)

return paths

Expand All @@ -50,5 +58,6 @@ def _jupyter_server_extension_paths():

if sys.platform != 'win32':
paths.append(dict(module="nbgrader.server_extensions.assignment_list"))
paths.append(dict(module="nbgrader.server_extensions.course_list"))

return paths
41 changes: 28 additions & 13 deletions nbgrader/auth/jupyterhub.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,31 @@ class JupyterhubApiError(Exception):
pass


def get_jupyterhub_user():
if os.getenv('JUPYTERHUB_USER'):
return os.environ['JUPYTERHUB_USER']
else:
raise JupyterhubEnvironmentError("JUPYTERHUB_USER env is required to run the exchange features of nbgrader.")


def get_jupyterhub_url():
return os.environ.get('JUPYTERHUB_BASE_URL') or 'http://127.0.0.1:8000'


def get_jupyterhub_api_url():
return os.environ.get('JUPYTERHUB_API_URL') or 'http://127.0.0.1:8081/hub/api'


def get_jupyterhub_authorization():
if os.getenv('JUPYTERHUB_API_TOKEN'):
api_token = os.environ['JUPYTERHUB_API_TOKEN']
else:
raise JupyterhubEnvironmentError("JUPYTERHUB_API_TOKEN env is required to run the exchange features of nbgrader.")
return {
'Authorization': 'token %s' % api_token
}


def _query_jupyterhub_api(method, api_path, post_data=None):
"""Query Jupyterhub api
Expand All @@ -32,19 +57,9 @@ def _query_jupyterhub_api(method, api_path, post_data=None):
JSON response converted to dictionary
"""
if os.getenv('JUPYTERHUB_API_TOKEN'):
api_token = os.environ['JUPYTERHUB_API_TOKEN']
else:
raise JupyterhubEnvironmentError("JUPYTERHUB_API_TOKEN env is required to run the exchange features of nbgrader.")
hub_api_url = os.environ.get('JUPYTERHUB_API_URL') or 'http://127.0.0.1:8081/hub/api'
if os.getenv('JUPYTERHUB_USER'):
user = os.environ['JUPYTERHUB_USER']
else:
raise JupyterhubEnvironmentError("JUPYTERHUB_USER env is required to run the exchange features of nbgrader.")
auth_header = {
'Authorization': 'token %s' % api_token
}

hub_api_url = get_jupyterhub_api_url()
user = get_jupyterhub_user()
auth_header = get_jupyterhub_authorization()
api_path = api_path.format(authenticated_user=user)
req = requests.request(
url=hub_api_url + api_path,
Expand Down
5 changes: 5 additions & 0 deletions nbgrader/docs/source/user_guide/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ or to disable the Formgrader extension::
jupyter nbextension disable --sys-prefix formgrader/main --section=tree
jupyter serverextension disable --sys-prefix nbgrader.server_extensions.formgrader

or to disable the Course List extension::

jupyter nbextension disable --sys-prefix course_list/main --section=tree
jupyter serverextension disable --sys-prefix nbgrader.server_extensions.course_list

For example lets assume you have installed nbgrader via `Anaconda
<https://www.anaconda.com/download>`__ (meaning all extensions are installed
and enabled with the ``--sys-prefix`` flag, i.e. anyone using the particular
Expand Down
67 changes: 67 additions & 0 deletions nbgrader/nbextensions/course_list/course_list.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#courses .panel-group .panel {
margin-top: 3px;
margin-bottom: 1em;
}

#courses .panel-group .panel .panel-heading {
background-color: #eee;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 7px;
padding-right: 7px;
line-height: 22px;
}

#courses .panel-group .panel .panel-heading a:focus, a:hover {
text-decoration: none;
}

#courses .panel-group .panel .panel-body {
padding: 0;
}

#courses .panel-group .panel .panel-body .list_container {
margin-top: 0px;
margin-bottom: 0px;
border: 0px;
border-radius: 0px;
}

#courses .panel-group .panel .panel-body .list_container .list_item {
border-bottom: 1px solid #ddd;
}

#courses .panel-group .panel .panel-body .list_container .list_item:last-child {
border-bottom: 0px;
}

#courses .list_item {
padding-top: 4px;
padding-bottom: 4px;
padding-left: 7px;
padding-right: 7px;
line-height: 22px;
}

#courses .list_item > div {
padding-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
}

#courses .list_placeholder {
display: none;
}

#courses .list_placeholder, #courses .list_loading, #courses .list_error {
font-weight: bold;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 7px;
padding-right: 7px;
}

#courses .list_error, #courses .version_error {
display: none;
}
139 changes: 139 additions & 0 deletions nbgrader/nbextensions/course_list/course_list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

define([
'base/js/namespace',
'jquery',
'base/js/utils',
'base/js/dialog',
], function(Jupyter, $, utils, dialog) {
"use strict";

var ajax = utils.ajax || $.ajax;
// Notebook v4.3.1 enabled xsrf so use notebooks ajax that includes the
// xsrf token in the header data

var CourseList = function (course_list_selector, refresh_selector, options) {
this.course_list_selector = course_list_selector;
this.refresh_selector = refresh_selector;

this.course_list_element = $(course_list_selector);
this.refresh_element = $(refresh_selector);

this.current_course = undefined;
this.bind_events()

options = options || {};
this.options = options;
this.base_url = options.base_url || utils.get_body_data("baseUrl");

this.data = undefined;
};

CourseList.prototype.bind_events = function () {
var that = this;
this.refresh_element.click(function () {
that.load_list();
});
};


CourseList.prototype.clear_list = function (loading) {
this.course_list_element.children('.list_item').remove();
if (loading) {
// show loading
this.course_list_element.children('.list_loading').show();
// hide placeholders and errors
this.course_list_element.children('.list_placeholder').hide();
this.course_list_element.children('.list_error').hide();

} else {
// show placeholders
this.course_list_element.children('.list_placeholder').show();
// hide loading and errors
this.course_list_element.children('.list_loading').hide();
this.course_list_element.children('.list_error').hide();
}
};

CourseList.prototype.show_error = function (error) {
this.course_list_element.children('.list_item').remove();
// show errors
this.course_list_element.children('.list_error').show();
this.course_list_element.children('.list_error').text(error);
// hide loading and placeholding
this.course_list_element.children('.list_loading').hide();
this.course_list_element.children('.list_placeholder').hide();
};

CourseList.prototype.load_list = function () {
this.clear_list(true);

var settings = {
processData : false,
cache : false,
type : "GET",
dataType : "json",
success : $.proxy(this.handle_load_list, this),
error : utils.log_ajax_error,
};
var url = utils.url_path_join(this.base_url, 'formgraders');
ajax(url, settings);
};

CourseList.prototype.handle_load_list = function (data, status, xhr) {
if (data.success) {
this.load_list_success(data.value);
} else {
this.show_error(data.value);
}
};

CourseList.prototype.load_list_success = function (data) {
this.clear_list();
var len = data.length;
for (var i=0; i<len; i++) {
var element = $('<div/>');
var item = new Course(element, data[i], this.course_list_selector, $.proxy(this.handle_load_list, this), this.options);
this.course_list_element.append(element);
this.course_list_element.children('.list_placeholder').hide()
}

if (this.callback) {
this.callback();
this.callback = undefined;
}
};

var Course = function (element, data, parent, on_refresh, options) {
this.element = $(element);
this.course_id = data['course_id'];
this.formgrader_kind = data['kind'];
this.url = data['url'];
this.parent = parent;
this.on_refresh = on_refresh;
this.options = options;
this.style();
this.make_row();
};

Course.prototype.style = function () {
this.element.addClass('list_item').addClass("row");
};

Course.prototype.make_row = function () {
var row = $('<div/>').addClass('col-md-12');
var container = $('<span/>').addClass('item_name col-sm-2').append(
$('<a/>')
.attr('href', this.url)
.attr('target', '_blank')
.text(this.course_id));
row.append(container);
row.append($('<span/>').addClass('item_course col-sm-2').text(this.formgrader_kind));
this.element.empty().append(row);
};

return {
'CourseList': CourseList,
};
});
101 changes: 101 additions & 0 deletions nbgrader/nbextensions/course_list/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
define([
'base/js/namespace',
'jquery',
'base/js/utils',
'./course_list'
], function(Jupyter, $, utils, CourseList) {
"use strict";

var nbgrader_version = "0.6.0.dev";

var ajax = utils.ajax || $.ajax;
// Notebook v4.3.1 enabled xsrf so use notebooks ajax that includes the
// xsrf token in the header data

var course_html = $([
'<div id="courses" class="tab-pane">',
' <div class="alert alert-danger version_error">',
' </div>',
' <div class="panel-group">',
' <div class="panel panel-default">',
' <div class="panel-heading">',
' Available formgraders',
' <span id="formgrader_buttons" class="pull-right toolbar_buttons">',
' <button id="refresh_formgrader_list" title="Refresh formgrader list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>',
' </span>',
' </div>',
' <div class="panel-body">',
' <div id="formgrader_list" class="list_container">',
' <div id="formgrader_list_placeholder" class="row list_placeholder">',
' <div> There are no available formgrader services. </div>',
' </div>',
' <div id="formgrader_list_loading" class="row list_loading">',
' <div> Loading, please wait... </div>',
' </div>',
' <div id="formgrader_list_error" class="row list_error">',
' <div></div>',
' </div>',
' </div>',
' </div>',
' </div>',
' </div> ',
'</div>'
].join('\n'));

function checkNbGraderVersion(base_url) {
var settings = {
cache : false,
type : "GET",
dataType : "json",
data : {
version: nbgrader_version
},
success : function (response) {
if (!response['success']) {
var err = $("#courses .version_error");
err.text(response['message']);
err.show();
}
},
error : utils.log_ajax_error,
};
var url = utils.url_path_join(base_url, 'nbgrader_version');
ajax(url, settings);
}

function load() {
if (!Jupyter.notebook_list) return;
var base_url = Jupyter.notebook_list.base_url;
$('head').append(
$('<link>')
.attr('rel', 'stylesheet')
.attr('type', 'text/css')
.attr('href', base_url + 'nbextensions/course_list/course_list.css')
);
$(".tab-content").append(course_html);
$("#tabs").append(
$('<li>')
.append(
$('<a>')
.attr('href', '#courses')
.attr('data-toggle', 'tab')
.text('Courses')
.click(function (e) {
window.history.pushState(null, null, '#courses');
course_list.load_list();
})
)
);
var course_list = new CourseList.CourseList(
'#formgrader_list',
'#refresh_formgrader_list',
{
base_url: Jupyter.notebook_list.base_url
}
);
checkNbGraderVersion(base_url);
}
return {
load_ipython_extension: load
};
});
Loading

0 comments on commit 304a001

Please sign in to comment.