Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a course list extension that shows all courses an instructor can manage #1113

Merged
merged 9 commits into from
Jun 1, 2019
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