Merge pull request #12372 from edx/feature/catalog-admin
Merge catalog admin into master.
This commit is contained in:
@@ -769,6 +769,8 @@ CREDIT_HELP_LINK_URL = ENV_TOKENS.get('CREDIT_HELP_LINK_URL', CREDIT_HELP_LINK_U
|
||||
JWT_ISSUER = ENV_TOKENS.get('JWT_ISSUER', JWT_ISSUER)
|
||||
JWT_EXPIRATION = ENV_TOKENS.get('JWT_EXPIRATION', JWT_EXPIRATION)
|
||||
JWT_AUTH.update(ENV_TOKENS.get('JWT_AUTH', {}))
|
||||
PUBLIC_RSA_KEY = ENV_TOKENS.get('PUBLIC_RSA_KEY', PUBLIC_RSA_KEY)
|
||||
PRIVATE_RSA_KEY = ENV_TOKENS.get('PRIVATE_RSA_KEY', PRIVATE_RSA_KEY)
|
||||
|
||||
################# PROCTORING CONFIGURATION ##################
|
||||
|
||||
|
||||
@@ -2784,6 +2784,10 @@ LTI_AGGREGATE_SCORE_PASSBACK_DELAY = 15 * 60
|
||||
JWT_EXPIRATION = 30
|
||||
JWT_ISSUER = None
|
||||
|
||||
# For help generating a key pair import and run `openedx.core.lib.rsa_key_utils.generate_rsa_key_pair()`
|
||||
PUBLIC_RSA_KEY = None
|
||||
PRIVATE_RSA_KEY = None
|
||||
|
||||
# Credit notifications settings
|
||||
NOTIFICATION_EMAIL_CSS = "templates/credit_notifications/credit_notification.css"
|
||||
NOTIFICATION_EMAIL_EDX_LOGO = "templates/credit_notifications/edx-logo-header.png"
|
||||
|
||||
@@ -225,6 +225,47 @@ CORS_ORIGIN_WHITELIST = ()
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
|
||||
# JWT settings for devstack
|
||||
PUBLIC_RSA_KEY = """\
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApCujf5oZBGK4MafMRGY9
|
||||
+zdRRI9YDm1r+81coDCysSrwkhTkFIwP2dmS6lYvJuQ5wifuQa3WFv1Kh9Nr2XRJ
|
||||
1m9OL3/JpmMyTi/YuwD7tIf65tab1SOSRYkoxOKRuuvZuXQG9nWbXrGDncnwuWxf
|
||||
eymwWaIrAhALUS5+nDa7dauj8VngsWauMrEA/MWShEzsR53wGKlciEZA1r/AfQ55
|
||||
XS42GvBobhhy9SeZ3B6LHiaAEywpwFmKPssuoHSNhbPa49LW3gXJ6CsFGRDcBFKd
|
||||
xJ/l8O847Q7kg1lvckpLsKyu5167NK9Qj1X/O3SwVBL3cxx1HpQ6+q3SGLZ4ngow
|
||||
hwIDAQAB
|
||||
-----END PUBLIC KEY-----"""
|
||||
|
||||
PRIVATE_RSA_KEY = """\
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCkK6N/mhkEYrgx
|
||||
p8xEZj37N1FEj1gObWv7zVygMLKxKvCSFOQUjA/Z2ZLqVi8m5DnCJ+5BrdYW/UqH
|
||||
02vZdEnWb04vf8mmYzJOL9i7APu0h/rm1pvVI5JFiSjE4pG669m5dAb2dZtesYOd
|
||||
yfC5bF97KbBZoisCEAtRLn6cNrt1q6PxWeCxZq4ysQD8xZKETOxHnfAYqVyIRkDW
|
||||
v8B9DnldLjYa8GhuGHL1J5ncHoseJoATLCnAWYo+yy6gdI2Fs9rj0tbeBcnoKwUZ
|
||||
ENwEUp3En+Xw7zjtDuSDWW9ySkuwrK7nXrs0r1CPVf87dLBUEvdzHHUelDr6rdIY
|
||||
tnieCjCHAgMBAAECggEBAJvTiAdQPzq4cVlAilTKLz7KTOsknFJlbj+9t5OdZZ9g
|
||||
wKQIDE2sfEcti5O+Zlcl/eTaff39gN6lYR73gMEQ7h0J3U6cnsy+DzvDkpY94qyC
|
||||
/ZYqUhPHBcnW3Mm0vNqNj0XGae15yBXjrKgSy9lUknSXJ3qMwQHeNL/DwA2KrfiL
|
||||
g0iVjk32dvSSHWcBh0M+Qy1WyZU0cf9VWzx+Q1YLj9eUCHteStVubB610XV3JUZt
|
||||
UTWiUCffpo2okHsTBuKPVXK/5BL+BpGplcxRSlnSbMaI611kN3iKlO8KGISXHBz7
|
||||
nOPdkfZC9poEXt5SshtINuGGCCc8hDxpg1otYqCLaYECgYEA1MSCPs3pBkEagchV
|
||||
g0rxYmDUC8QkeIOBuZFjhkdoUgZ6rFntyRZd1NbCUi3YBbV1YC12ZGohqWUWom1S
|
||||
AtNbQ2ZTbqEnDKWbNvLBRwkdp/9cKBce85lCCD6+U2o2Ha8C0+hKeLBn8un1y0zY
|
||||
1AQTqLAz9ItNr0aDPb89cs5voWcCgYEAxYdC8vR3t8iYMUnK6LWYDrKSt7YiorvF
|
||||
qXIMANcXQrnO0ptC0B56qrUCgKHNrtPi5bGpNBJ0oKMfbmGfwX+ca8sCUlLvq/O8
|
||||
S2WZwSJuaHH4lEBi8ErtY++8F4B4l3ENCT84Hyy5jiMpbpkHEnh/1GNcvvmyI8ud
|
||||
3jzovCNZ4+ECgYEA0r+Oz0zAOzyzV8gqw7Cw5iRJBRqUkXaZQUj8jt4eO9lFG4C8
|
||||
IolwCclrk2Drb8Qsbka51X62twZ1ZA/qwve9l0Y88ADaIBHNa6EKxyUFZglvrBoy
|
||||
w1GT8XzMou06iy52G5YkZeU+IYOSvnvw7hjXrChUXi65lRrAFqJd6GEIe5MCgYA/
|
||||
0LxDa9HFsWvh+JoyZoCytuSJr7Eu7AUnAi54kwTzzL3R8tE6Fa7BuesODbg6tD/I
|
||||
v4YPyaqePzUnXyjSxdyOQq8EU8EUx5Dctv1elTYgTjnmA4szYLGjKM+WtC3Bl4eD
|
||||
pkYGZFeqYRfAoHXVdNKvlk5fcKIpyF2/b+Qs7CrdYQKBgQCc/t+JxC9OpI+LhQtB
|
||||
tEtwvklxuaBtoEEKJ76P9vrK1semHQ34M1XyNmvPCXUyKEI38MWtgCCXcdmg5syO
|
||||
PBXdDINx+wKlW7LPgaiRL0Mi9G2aBpdFNI99CWVgCr88xqgSE24KsOxViMwmi0XB
|
||||
Ld/IRK0DgpGP5EJRwpKsDYe/UQ==
|
||||
-----END PRIVATE KEY-----"""
|
||||
|
||||
JWT_AUTH.update({
|
||||
'JWT_ALGORITHM': 'HS256',
|
||||
'JWT_SECRET_KEY': 'lms-secret',
|
||||
|
||||
14
lms/static/js/api_admin/catalog_preview_factory.js
Normal file
14
lms/static/js/api_admin/catalog_preview_factory.js
Normal file
@@ -0,0 +1,14 @@
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
|
||||
define(['js/api_admin/views/catalog_preview'], function (CatalogPreviewView) {
|
||||
return function (options) {
|
||||
var view = new CatalogPreviewView({
|
||||
el: '.catalog-body',
|
||||
previewUrl: options.previewUrl,
|
||||
catalogApiUrl: options.catalogApiUrl,
|
||||
});
|
||||
return view.render();
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
64
lms/static/js/api_admin/views/catalog_preview.js
Normal file
64
lms/static/js/api_admin/views/catalog_preview.js
Normal file
@@ -0,0 +1,64 @@
|
||||
;(function(define) {
|
||||
'use strict';
|
||||
|
||||
define([
|
||||
'backbone',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'text!../../../templates/api_admin/catalog-results.underscore',
|
||||
'text!../../../templates/api_admin/catalog-error.underscore'
|
||||
], function (Backbone, _, gettext, catalogResultsTpl, catalogErrorTpl) {
|
||||
return Backbone.View.extend({
|
||||
|
||||
events: {
|
||||
'click .preview-query': 'previewQuery'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
this.previewUrl = options.previewUrl;
|
||||
this.catalogApiUrl = options.catalogApiUrl;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$('#id_query').after(
|
||||
'<button class="preview-query">'+ gettext('Preview this query') + '</button>'
|
||||
);
|
||||
return this;
|
||||
},
|
||||
|
||||
/*
|
||||
* Return the user's query, URL-encoded.
|
||||
*/
|
||||
getQuery: function () {
|
||||
return encodeURIComponent(this.$("#id_query").val());
|
||||
},
|
||||
|
||||
/*
|
||||
* Make a request to get the list of courses associated
|
||||
* with the user's query. On success, displays the
|
||||
* results, and on failure, displays an error message.
|
||||
*/
|
||||
previewQuery: function (event) {
|
||||
event.preventDefault();
|
||||
$.ajax(this.previewUrl + '?q=' + this.getQuery(), {
|
||||
method: 'GET',
|
||||
success: _.bind(this.renderCourses, this),
|
||||
error: _.bind(function () {
|
||||
this.$('.preview-results').html(_.template(catalogErrorTpl)({}));
|
||||
}, this)
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* Render a list of courses with data returned by the
|
||||
* courses API.
|
||||
*/
|
||||
renderCourses: function (data) {
|
||||
this.$('.preview-results').html(_.template(catalogResultsTpl)({
|
||||
'courses': data.results,
|
||||
'catalogApiUrl': this.catalogApiUrl,
|
||||
}));
|
||||
},
|
||||
});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
58
lms/static/js/spec/api_admin/catalog_preview_spec.js
Normal file
58
lms/static/js/spec/api_admin/catalog_preview_spec.js
Normal file
@@ -0,0 +1,58 @@
|
||||
define([
|
||||
'js/api_admin/views/catalog_preview',
|
||||
'common/js/spec_helpers/ajax_helpers',
|
||||
], function (
|
||||
CatalogPreviewView, AjaxHelpers
|
||||
) {
|
||||
'use strict';
|
||||
|
||||
describe('Catalog preview view', function () {
|
||||
var view,
|
||||
previewUrl = 'http://example.com/api-admin/catalogs/preview/',
|
||||
catalogApiUrl = 'http://api.example.com/catalog/v1/courses/';
|
||||
|
||||
beforeEach(function () {
|
||||
setFixtures(
|
||||
'<div class="catalog-body">' +
|
||||
'<textarea id="id_query"></textarea>' +
|
||||
'<div class="preview-results"></div>' +
|
||||
'</div>'
|
||||
);
|
||||
view = new CatalogPreviewView({
|
||||
el: '.catalog-body',
|
||||
previewUrl: previewUrl,
|
||||
catalogApiUrl: catalogApiUrl,
|
||||
});
|
||||
view.render();
|
||||
});
|
||||
|
||||
it('can render itself', function () {
|
||||
expect(view.$('button.preview-query').length).toBe(1);
|
||||
});
|
||||
|
||||
it('can retrieve a list of catalogs and display them', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
view.$('#id_query').val('*');
|
||||
view.$('.preview-query').click();
|
||||
AjaxHelpers.expectRequest(requests, 'GET', previewUrl + '?q=*');
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
results: [{key: 'TestX', title: 'Test Course'}],
|
||||
count: 1,
|
||||
next: null,
|
||||
prev: null,
|
||||
});
|
||||
expect(view.$('.preview-results').text()).toContain('Test Course');
|
||||
expect(view.$('.preview-results-list li a').attr('href')).toEqual(catalogApiUrl + 'TestX');
|
||||
});
|
||||
|
||||
it('displays an error when courses cannot be retrieved', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
view.$('#id_query').val('*');
|
||||
view.$('.preview-query').click();
|
||||
AjaxHelpers.respondWithError(requests, 500);
|
||||
expect(view.$('.preview-results').text()).toContain(
|
||||
'There was an error retrieving preview results for this catalog.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -769,7 +769,8 @@
|
||||
'js/spec/learner_dashboard/sidebar_view_spec.js',
|
||||
'js/spec/learner_dashboard/program_card_view_spec.js',
|
||||
'js/spec/learner_dashboard/certificate_view_spec.js',
|
||||
'js/spec/commerce/receipt_spec.js'
|
||||
'js/spec/commerce/receipt_spec.js',
|
||||
'js/spec/api_admin/catalog_preview_spec.js',
|
||||
];
|
||||
|
||||
for (var i = 0; i < testFiles.length; i++) {
|
||||
|
||||
@@ -119,7 +119,8 @@ var fixtureFiles = [
|
||||
{pattern: 'templates/bookmarks/**/*.*', included: false},
|
||||
{pattern: 'templates/learner_dashboard/**/*.*', included: false},
|
||||
{pattern: 'templates/ccx/**/*.*', included: false},
|
||||
{pattern: 'templates/commerce/receipt.underscore', included: false}
|
||||
{pattern: 'templates/commerce/receipt.underscore', included: false},
|
||||
{pattern: 'templates/api_admin/**/*.*', included: false}
|
||||
];
|
||||
|
||||
// override fixture path and other config.
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
'support/js/certificates_factory',
|
||||
'support/js/enrollment_factory',
|
||||
'js/bookmarks/bookmarks_factory',
|
||||
'js/learner_dashboard/program_list_factory'
|
||||
'js/learner_dashboard/program_list_factory',
|
||||
'js/api_admin/catalog_preview_factory'
|
||||
]),
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
#api-access-wrapper {
|
||||
|
||||
#api-access-request-header {
|
||||
h1 {
|
||||
@extend %t-title4;
|
||||
margin-bottom: 0;
|
||||
padding: $baseline;
|
||||
@include text-align(left);
|
||||
}
|
||||
|
||||
.api-access-request-subheading {
|
||||
h2 {
|
||||
@extend %t-title5;
|
||||
margin: $baseline;
|
||||
@include text-align(left);
|
||||
}
|
||||
|
||||
.api-tos-body {
|
||||
p {
|
||||
@extend %t-copy-sub1;
|
||||
margin: $baseline;
|
||||
}
|
||||
@@ -40,64 +40,95 @@
|
||||
@extend %t-copy-base;
|
||||
}
|
||||
|
||||
.api-management-form {
|
||||
.catalog-body {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
padding: 0 $baseline $baseline $baseline;
|
||||
.api-form-container {
|
||||
@include float(left);
|
||||
width: 50%;
|
||||
|
||||
p {
|
||||
margin: 1.5*$baseline 0;
|
||||
.api-form {
|
||||
|
||||
.helptext {
|
||||
@extend %t-copy-sub1;
|
||||
display: block;
|
||||
padding: 0 $baseline $baseline $baseline;
|
||||
|
||||
p {
|
||||
margin: 1.5*$baseline 0;
|
||||
|
||||
.helptext {
|
||||
@extend %t-copy-sub1;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
@extend %t-copy-base;
|
||||
display: block;
|
||||
font-style: normal;
|
||||
|
||||
&.tos-checkbox-label {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
@extend %t-copy-base;
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
width: 300px;
|
||||
|
||||
&[type=checkbox] {
|
||||
display: inline-block;
|
||||
width: initial;
|
||||
@include margin-right(0.5*$baseline);
|
||||
}
|
||||
}
|
||||
|
||||
.errorlist {
|
||||
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
label {
|
||||
@extend %t-copy-base;
|
||||
margin: 0;
|
||||
color: $red;
|
||||
display: block;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
input[type=checkbox] + label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
@extend %t-copy-base;
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
width: 300px;
|
||||
|
||||
&[type=checkbox] {
|
||||
display: inline-block;
|
||||
width: initial;
|
||||
@include margin-right(0.5*$baseline);
|
||||
}
|
||||
|
||||
&[type=submit] {
|
||||
@extend %t-copy-base;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
background-color: $blue;
|
||||
box-shadow: none;
|
||||
background-image: none;
|
||||
text-shadow: none;
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.errorlist {
|
||||
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
@extend %t-copy-base;
|
||||
margin: 0;
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
#api-access-submit, .preview-query {
|
||||
@extend %t-copy-base;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
background-color: $blue;
|
||||
box-shadow: none;
|
||||
background-image: none;
|
||||
text-shadow: none;
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#api-access-submit {
|
||||
@extend %t-copy-base;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
background-color: $blue;
|
||||
box-shadow: none;
|
||||
background-image: none;
|
||||
text-shadow: none;
|
||||
text-transform: none;
|
||||
}
|
||||
.preview-results {
|
||||
@include float(right);
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.preview-query {
|
||||
display: block;
|
||||
margin-top: $baseline/2;
|
||||
}
|
||||
|
||||
.application-info {
|
||||
|
||||
50
lms/templates/admin/api_admin/catalog/change_form.html
Normal file
50
lms/templates/admin/api_admin/catalog/change_form.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load admin_modify adminmedia %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
{{ media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/forms.css" />{% endblock %}
|
||||
|
||||
{% block coltype %}colMS{% endblock %}
|
||||
|
||||
{% block bodyclass %} change-form{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="../../../">Home</a> ›
|
||||
<a href="../../">Api_Admin</a> ›
|
||||
<a href="../">Catalogs</a> ›
|
||||
{% if change %}
|
||||
{{form.name}}
|
||||
{% else %}
|
||||
Add Catalog
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content-main">
|
||||
{% block object-tools %}
|
||||
{% endblock %}
|
||||
<form action="." method="post" enctype="multipart/form-data">
|
||||
|
||||
<fieldset class="module aligned {{ fieldset.classes }}">
|
||||
{% for field in form.visible_fields %}
|
||||
<div class="form-row">
|
||||
{{ field.errors }}
|
||||
{{ field.label_tag }}{{ field }}
|
||||
{% if field.field.help_text %}<p class="help">{{ field.field.help_text|safe }}</p>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
|
||||
</fieldset>
|
||||
<input type="submit" value="Save" />
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
27
lms/templates/admin/api_admin/catalog/change_list.html
Normal file
27
lms/templates/admin/api_admin/catalog/change_list.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
{% block innercontent %}
|
||||
<ul class="object-tools">
|
||||
<li>
|
||||
<a class="addlink" href="add/">Add Catalog</a>
|
||||
</li>
|
||||
</ul>
|
||||
<table cellspacing="0" style="margin-top: 20px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for catalog in catalogs %}
|
||||
<tr class="{% cycle 'row1' 'row2' %}">
|
||||
<td> </td>
|
||||
<td>
|
||||
<a href="{{catalog.id}}">{{catalog.name}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -8,13 +8,17 @@
|
||||
<%block name="pagetitle">${_("API Access Request")}</%block>
|
||||
|
||||
<div id="api-access-wrapper" class="container">
|
||||
<h1 id="api-access-request-header">
|
||||
<h1 id="api-header">
|
||||
${_("{platform_name} API Access Request").format(platform_name=settings.PLATFORM_NAME)}
|
||||
</h1>
|
||||
|
||||
<form action="" method="post" class="api-management-form">
|
||||
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
|
||||
${form.as_p() | n}
|
||||
<input id="api-access-submit" type="submit" value="${_('Request API Access')}"/>
|
||||
</form>
|
||||
<div class="catalog-body">
|
||||
<div class="api-form-container">
|
||||
<form action="" method="post" class="api-form">
|
||||
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
|
||||
${form.as_p() | n}
|
||||
<input id="api-access-submit" type="submit" value="${_('Request API Access')}"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
3
lms/templates/api_admin/catalog-error.underscore
Normal file
3
lms/templates/api_admin/catalog-error.underscore
Normal file
@@ -0,0 +1,3 @@
|
||||
<p class="api-copy-body">
|
||||
<%- gettext('There was an error retrieving preview results for this catalog. Please check that your query is correct and try again.') %>
|
||||
</p>
|
||||
6
lms/templates/api_admin/catalog-results.underscore
Normal file
6
lms/templates/api_admin/catalog-results.underscore
Normal file
@@ -0,0 +1,6 @@
|
||||
<h2 class="api-subheading"><%- gettext("This catalog's courses:") %></h2>
|
||||
<ul class="preview-results-list">
|
||||
<% _.each(courses, function (course) { %>
|
||||
<li><a href="<%- catalogApiUrl + encodeURIComponent(course.key) %>"><%- course.title %></a></li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
49
lms/templates/api_admin/catalog_changeform.html
Normal file
49
lms/templates/api_admin/catalog_changeform.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load admin_modify staticfiles %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
{{ media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static 'css/forms.css' %}" />{% endblock %}
|
||||
|
||||
{% block coltype %}colMS{% endblock %}
|
||||
|
||||
{% block bodyclass %} change-form{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="../../../">Home</a> ›
|
||||
<a href="../../">Api_Admin</a> ›
|
||||
<a href="../">Catalogs</a> ›
|
||||
{% if change %}
|
||||
{{form.name}}
|
||||
{% else %}
|
||||
Add Catalog
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content-main">
|
||||
{% block object-tools %}
|
||||
{% endblock %}
|
||||
<form action="." method="post" enctype="multipart/form-data">{% csrf_token %}
|
||||
<fieldset class="module aligned {{ fieldset.classes }}">
|
||||
{% for field in form.visible_fields %}
|
||||
<div class="form-row">
|
||||
{{ field.errors }}
|
||||
{{ field.label_tag }}{{ field }}
|
||||
{% if field.field.help_text %}<p class="help">{{ field.field.help_text|safe }}</p>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
|
||||
</fieldset>
|
||||
<input type="submit" value="Save" />
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
60
lms/templates/api_admin/catalog_changelist.html
Normal file
60
lms/templates/api_admin/catalog_changelist.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n admin_urls static admin_list %}
|
||||
|
||||
{% block extrastyle %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "admin/css/changelists.css" %}" />
|
||||
{% if cl.formset %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
|
||||
{% endif %}
|
||||
{% if cl.formset or action_form %}
|
||||
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
|
||||
{% endif %}
|
||||
{{ media.css }}
|
||||
{% if not actions_on_top and not actions_on_bottom %}
|
||||
<style>
|
||||
#changelist table thead th:first-child {width: inherit}
|
||||
</style>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
{{ media.js }}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="../../../">Home</a> ›
|
||||
<a href="../../">Api_Admin</a> ›
|
||||
Catalogs
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block coltype %}flex{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block innercontent %}
|
||||
<ul class="object-tools">
|
||||
<li>
|
||||
<a class="addlink" href="add/">Add Catalog</a>
|
||||
</li>
|
||||
</ul>
|
||||
<table cellspacing="0" style="margin-top: 20px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for catalog in catalogs %}
|
||||
<tr class="{% cycle 'row1' 'row2' %}">
|
||||
<td> </td>
|
||||
<td>
|
||||
<a href="{{catalog.id}}">{{catalog.name}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
18
lms/templates/api_admin/catalogs/detail.html
Normal file
18
lms/templates/api_admin/catalogs/detail.html
Normal file
@@ -0,0 +1,18 @@
|
||||
## mako
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="../../main.html"/>
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
|
||||
<%block name="pagetitle">${catalog.name}</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div id="api-access-wrapper">
|
||||
<h1 id="api-header">${catalog.name}</h1>
|
||||
<p class="api-copy-body">${catalog.query}</p>
|
||||
<p class="api-copy-body"><a href="${edit_link}">${_("Edit or delete this catalog.")}</a></p>
|
||||
<p class="api-copy-body"><a href="${preview_link}">${_("See a preview of this catalog's contents.")}</a></p>
|
||||
</div>
|
||||
</%block>
|
||||
41
lms/templates/api_admin/catalogs/edit.html
Normal file
41
lms/templates/api_admin/catalogs/edit.html
Normal file
@@ -0,0 +1,41 @@
|
||||
## mako
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="../../main.html"/>
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
<%block name="pagetitle">${_("Edit {catalog_name}").format(catalog_name=catalog.name)}</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<%static:require_module module_name="js/api_admin/catalog_preview_factory" class_name="CatalogPreviewFactory">
|
||||
CatalogPreviewFactory({
|
||||
previewUrl: "${preview_url}",
|
||||
catalogApiUrl: "${catalog_api_url}",
|
||||
});
|
||||
</%static:require_module>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div id="api-access-wrapper">
|
||||
<h1 id="api-header">${catalog.name}</h1>
|
||||
|
||||
<div class="catalog-body">
|
||||
<div class="api-form-container">
|
||||
<form class="api-form" id="catalog-update" action="${reverse('api_admin:catalog-edit', args=(catalog.id,))}" method="post">
|
||||
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
|
||||
<p>
|
||||
<input type="checkbox" id="delete-catalog" name="delete-catalog" />
|
||||
<label class="api-checkbox-label" for="delete-catalog">${_("Delete this catalog")}</label>
|
||||
</p>
|
||||
${form.as_p() | n}
|
||||
<input id="catalog-create-submit" type="submit" value="${_('Update Catalog')}"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="preview-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
45
lms/templates/api_admin/catalogs/list.html
Normal file
45
lms/templates/api_admin/catalogs/list.html
Normal file
@@ -0,0 +1,45 @@
|
||||
## mako
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="../../main.html"/>
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
<%block name="pagetitle">${_("Catalogs for {username}").format(username=username)}</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<%static:require_module module_name="js/api_admin/catalog_preview_factory" class_name="CatalogPreviewFactory">
|
||||
CatalogPreviewFactory({
|
||||
previewUrl: "${preview_url}",
|
||||
catalogApiUrl: "${catalog_api_url}",
|
||||
});
|
||||
</%static:require_module>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div id="api-access-wrapper">
|
||||
<h1 id="api-header">${_("Catalogs for {username}").format(username=username)}</h1>
|
||||
<ul>
|
||||
% for catalog in catalogs:
|
||||
<li>
|
||||
<a href="${reverse('api_admin:catalog-edit', args=(catalog.id,))}">${catalog.name}</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
|
||||
<div class="catalog-body">
|
||||
<h2 class="api-subheading">${_("Create new catalog:")}</h2>
|
||||
<div class="api-form-container">
|
||||
<form class="api-form" id="catalog-create" action="${reverse('api_admin:catalog-list', args={'username': username})}" method="post">
|
||||
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
|
||||
${form.as_p() | n}
|
||||
<input id="catalog-create-submit" type="submit" value="${_('Create Catalog')}"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="preview-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
28
lms/templates/api_admin/catalogs/search.html
Normal file
28
lms/templates/api_admin/catalogs/search.html
Normal file
@@ -0,0 +1,28 @@
|
||||
## mako
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="../../main.html"/>
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
|
||||
<%block name="pagetitle">${_("Catalog search")}</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div id="api-access-wrapper">
|
||||
<h1 id="api-header">${_("Catalog Search")}</h1>
|
||||
|
||||
<div class="catalog-body">
|
||||
<h2 class="api-subheading">${_("Enter a username to view catalogs belonging to that user.")}</h2>
|
||||
<div class="api-form-container">
|
||||
<form class="api-form" method="post" action="">
|
||||
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
|
||||
<p>
|
||||
<input name="username" type="text" maxlength="30" placeholder="${_('Username')}" />
|
||||
</p>
|
||||
<input id="catalog-search-submit" type="submit" value="${_('Search')}"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -9,7 +9,7 @@ from openedx.core.djangolib.markup import Text, HTML
|
||||
%>
|
||||
|
||||
<div id="api-access-wrapper">
|
||||
<h1 id="api-access-request-header">${_("{platform_name} API Access Request").format(platform_name=settings.PLATFORM_NAME)}</h1>
|
||||
<h1 id="api-header">${_("{platform_name} API Access Request").format(platform_name=settings.PLATFORM_NAME)}</h1>
|
||||
<div class="request-status request-${status}">
|
||||
<p id="api-access-status">
|
||||
% if status == ApiAccessRequest.PENDING:
|
||||
@@ -41,11 +41,15 @@ from openedx.core.djangolib.markup import Text, HTML
|
||||
<p>${_('If you would like to regenerate your API client information, please use the form below.')}</p>
|
||||
|
||||
% endif
|
||||
<form id="api-form-fields" method="post" class="api-management-form">
|
||||
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
|
||||
${form.as_p() | n}
|
||||
<input id="api-access-submit" type="submit" value="${_('Generate API client credentials')}"/>
|
||||
</form>
|
||||
<div class="catalog-body">
|
||||
<div class="api-form-container">
|
||||
<form id="api-form-fields" method="post" class="api-form">
|
||||
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
|
||||
${form.as_p() | n}
|
||||
<input id="api-access-submit" type="submit" value="${_('Generate API client credentials')}"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</p>
|
||||
|
||||
|
||||
@@ -7,99 +7,99 @@ from django.utils.translation import ugettext as _
|
||||
%>
|
||||
|
||||
<div id="api-access-wrapper">
|
||||
<h1 id="api-access-request-header">${_("Terms of Service for {platform_name} APIs").format(platform_name=settings.PLATFORM_NAME)}</h1>
|
||||
<h2 class="api-access-request-subheading">${_("Effective Date: April 12th, 2016")}</h2>
|
||||
<h1 id="api-header">${_("Terms of Service for {platform_name} APIs").format(platform_name=settings.PLATFORM_NAME)}</h1>
|
||||
<h2 class="api-subheading">${_("Effective Date: April 12th, 2016")}</h2>
|
||||
|
||||
<p class="api-tos-body">${_("Welcome to {platform_name}. Thank you for using {platform_name}'s Course Discovery API and any additional APIs that we may offer from time to time (collectively, the \"APIs\"). Please read these Terms of Service prior to accessing or using the APIs. These Terms of Service, any additional terms within accompanying API documentation, and any applicable policies and guidelines that {platform_name} makes available and/or updates from time to time are agreements (collectively, the \"Terms\") between you and {platform_name}. By accessing or using the APIs, you accept and agree to be legally bound by the Terms, whether or not you are a registered user. If you do not understand or do not wish to be bound by the Terms, you should not use the APIs.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
<p class="api-copy-body">${_("Welcome to {platform_name}. Thank you for using {platform_name}'s Course Discovery API and any additional APIs that we may offer from time to time (collectively, the \"APIs\"). Please read these Terms of Service prior to accessing or using the APIs. These Terms of Service, any additional terms within accompanying API documentation, and any applicable policies and guidelines that {platform_name} makes available and/or updates from time to time are agreements (collectively, the \"Terms\") between you and {platform_name}. By accessing or using the APIs, you accept and agree to be legally bound by the Terms, whether or not you are a registered user. If you do not understand or do not wish to be bound by the Terms, you should not use the APIs.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
|
||||
<h2 class="api-access-request-subheading">${_("API Access")}</h2>
|
||||
<h2 class="api-subheading">${_("API Access")}</h2>
|
||||
|
||||
<p class="api-tos-body">${_("To access the APIs, you will need to create an {platform_name} user account for your application (not for personal use). This account will provide you with access to our API request page at {request_url}. On that page, you must complete the API request form including a description of your proposed uses for the APIs. Any account and registration information that you provide to {platform_name} must be accurate and up to date, and you agree to inform us promptly of any changes. {platform_name} will review your API request form and, upon approval in {platform_name}'s sole discretion, will provide you with instructions for obtaining your API shared secret and client ID.").format(platform_name=settings.PLATFORM_NAME, request_url=reverse('api_admin:api-request'))}</p>
|
||||
<p class="api-copy-body">${_("To access the APIs, you will need to create an {platform_name} user account for your application (not for personal use). This account will provide you with access to our API request page at {request_url}. On that page, you must complete the API request form including a description of your proposed uses for the APIs. Any account and registration information that you provide to {platform_name} must be accurate and up to date, and you agree to inform us promptly of any changes. {platform_name} will review your API request form and, upon approval in {platform_name}'s sole discretion, will provide you with instructions for obtaining your API shared secret and client ID.").format(platform_name=settings.PLATFORM_NAME, request_url=reverse('api_admin:api-request'))}</p>
|
||||
|
||||
<h2 class="api-access-request-subheading">${_("Permissible Use")}</h2>
|
||||
<h2 class="api-subheading">${_("Permissible Use")}</h2>
|
||||
|
||||
<p class="api-tos-body">${_("You agree to use the APIs solely for the purpose of delivering content that is accessed through the APIs (the \"API Content\") to your own website, mobile site, app, blog, email distribution list, or social media property or for another commercial use that you described in your request for access and that {platform_name} has approved on a case-by-case basis. {platform_name} may monitor your use of the APIs for compliance with the Terms and may deny your access or shut down your integration if you try to go around or exceed the requirements and limitations set by {platform_name}. Your Application or other approved use of the API or the API Content must not prompt your end users to provide their {platform_name} username, password or other {platform_name} user credentials anywhere other than the {platform_name} website at {platform_url}.").format(platform_name=settings.PLATFORM_NAME, platform_url='TODO')}</p>
|
||||
<p class="api-copy-body">${_("You agree to use the APIs solely for the purpose of delivering content that is accessed through the APIs (the \"API Content\") to your own website, mobile site, app, blog, email distribution list, or social media property or for another commercial use that you described in your request for access and that {platform_name} has approved on a case-by-case basis. {platform_name} may monitor your use of the APIs for compliance with the Terms and may deny your access or shut down your integration if you try to go around or exceed the requirements and limitations set by {platform_name}. Your Application or other approved use of the API or the API Content must not prompt your end users to provide their {platform_name} username, password or other {platform_name} user credentials anywhere other than the {platform_name} website at {platform_url}.").format(platform_name=settings.PLATFORM_NAME, platform_url='TODO')}</p>
|
||||
|
||||
<h2 class="api-access-request-subheading">${_("Prohibited Uses and Activities")}</h2>
|
||||
<h2 class="api-subheading">${_("Prohibited Uses and Activities")}</h2>
|
||||
|
||||
<p class="api-tos-body">${_("{platform_name} shall have the sole right to determine whether or not any given use of the APIs is acceptable, and {platform_name} reserves the right to revoke API access for any use that {platform_name} determines at any time, in its sole discretion, does not benefit or serve the best interests of {platform_name}, its users and its partners.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
<p class="api-copy-body">${_("{platform_name} shall have the sole right to determine whether or not any given use of the APIs is acceptable, and {platform_name} reserves the right to revoke API access for any use that {platform_name} determines at any time, in its sole discretion, does not benefit or serve the best interests of {platform_name}, its users and its partners.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
|
||||
<p class="api-tos-body">${_("The following activities are not acceptable when using the APIs (this is not an exhaustive list):")}</p>
|
||||
<p class="api-copy-body">${_("The following activities are not acceptable when using the APIs (this is not an exhaustive list):")}</p>
|
||||
|
||||
<ul>
|
||||
<li class="api-tos-body">${_("collecting or storing the names, passwords, or other credentials of {platform_name} users;").format(platform_name=settings.PLATFORM_NAME)}</li>
|
||||
<li class="api-tos-body">${_("scraping or similar techniques to aggregate or otherwise create permanent copies of API Content;")}</li>
|
||||
<li class="api-tos-body">${_("violating, misappropriating or infringing any copyright, trademark rights, rights of privacy or publicity, confidential information or any other right of any third party;")}</li>
|
||||
<li class="api-tos-body">${_("altering or removing any trademark, copyright or other proprietary or legal notices contained in, or appearing on, the APIs or any API Content;")}</li>
|
||||
<li class="api-tos-body">${_("altering or editing any content or graphics in the API Content")}</li>
|
||||
<li class="api-tos-body">${_("sublicensing, re-distributing, renting, selling or leasing access to the APIs or your client secret to any third party;")}</li>
|
||||
<li class="api-tos-body">${_("distributing any virus, Trojan horse, spyware, adware, malware, bot, time bomb, worm, or other harmful or malicious component; or")}</li>
|
||||
<li class="api-tos-body">${_("using the APIs for any purpose which or might overburden, impair or disrupt the {platform_name} platform, servers or networks.").format(platform_name=settings.PLATFORM_NAME)}</li>
|
||||
<li class="api-copy-body">${_("collecting or storing the names, passwords, or other credentials of {platform_name} users;").format(platform_name=settings.PLATFORM_NAME)}</li>
|
||||
<li class="api-copy-body">${_("scraping or similar techniques to aggregate or otherwise create permanent copies of API Content;")}</li>
|
||||
<li class="api-copy-body">${_("violating, misappropriating or infringing any copyright, trademark rights, rights of privacy or publicity, confidential information or any other right of any third party;")}</li>
|
||||
<li class="api-copy-body">${_("altering or removing any trademark, copyright or other proprietary or legal notices contained in, or appearing on, the APIs or any API Content;")}</li>
|
||||
<li class="api-copy-body">${_("altering or editing any content or graphics in the API Content")}</li>
|
||||
<li class="api-copy-body">${_("sublicensing, re-distributing, renting, selling or leasing access to the APIs or your client secret to any third party;")}</li>
|
||||
<li class="api-copy-body">${_("distributing any virus, Trojan horse, spyware, adware, malware, bot, time bomb, worm, or other harmful or malicious component; or")}</li>
|
||||
<li class="api-copy-body">${_("using the APIs for any purpose which or might overburden, impair or disrupt the {platform_name} platform, servers or networks.").format(platform_name=settings.PLATFORM_NAME)}</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="api-access-request-subheading">${_("Usage and Quotas")}</h2>
|
||||
<h2 class="api-subheading">${_("Usage and Quotas")}</h2>
|
||||
|
||||
<p class="api-tos-body">${_("{platform_name} reserves the right, in its discretion, to impose restrictions and limitations on the number and frequency of calls made by you or your Application to the APIs. You must not attempt to circumvent any restrictions or limitations that we impose.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
<p class="api-copy-body">${_("{platform_name} reserves the right, in its discretion, to impose restrictions and limitations on the number and frequency of calls made by you or your Application to the APIs. You must not attempt to circumvent any restrictions or limitations that we impose.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
|
||||
<h2 class="api-access-request-subheading">${_("Compliance")}</h2>
|
||||
<h2 class="api-subheading">${_("Compliance")}</h2>
|
||||
|
||||
<p class="api-tos-body">${_("You agree to comply with all applicable law, regulation, and third party rights (including without limitation laws regarding the import or export of data or software, privacy, copyright, and local laws). You will not use the APIs to encourage or promote illegal activity or violation of third party rights. You will not violate any other terms of service with {platform_name}. You will only access (or attempt to access) an API by the means described in the documentation of that API. You will not misrepresent or mask either your identity or yourApplication's identity when using the APIs.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
<p class="api-copy-body">${_("You agree to comply with all applicable law, regulation, and third party rights (including without limitation laws regarding the import or export of data or software, privacy, copyright, and local laws). You will not use the APIs to encourage or promote illegal activity or violation of third party rights. You will not violate any other terms of service with {platform_name}. You will only access (or attempt to access) an API by the means described in the documentation of that API. You will not misrepresent or mask either your identity or yourApplication's identity when using the APIs.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
|
||||
<h2 class="api-access-request-subheading">${_("Ownership")}</h2>
|
||||
<h2 class="api-subheading">${_("Ownership")}</h2>
|
||||
|
||||
<p class="api-tos-body">${_("You acknowledge and agree that the APIs and all API Content contain valuable intellectual property of {platform_name} and its partners. The APIs and all API Content are protected by United States and foreign copyright, trademark, and other laws. All rights in the APIs and the API Content, if not expressly granted, are reserved. By using the APIs or any API Content, you do not acquire ownership of any rights in the APIs or API Content. You must not claim or attempt to claim ownership in the APIs or any API Content or misrepresent yourself or your company or your Application as being the source of any API Content. You may not modify, create derivative works of, or attempt to use, license, or in any way exploit any API Content in whole or in part on your own behalf or on behalf of any third party. You may not distribute or modify the APIs or any API Content (including adaptation, editing, excerpting, or creating derivative works).")}</p>
|
||||
<p class="api-copy-body">${_("You acknowledge and agree that the APIs and all API Content contain valuable intellectual property of {platform_name} and its partners. The APIs and all API Content are protected by United States and foreign copyright, trademark, and other laws. All rights in the APIs and the API Content, if not expressly granted, are reserved. By using the APIs or any API Content, you do not acquire ownership of any rights in the APIs or API Content. You must not claim or attempt to claim ownership in the APIs or any API Content or misrepresent yourself or your company or your Application as being the source of any API Content. You may not modify, create derivative works of, or attempt to use, license, or in any way exploit any API Content in whole or in part on your own behalf or on behalf of any third party. You may not distribute or modify the APIs or any API Content (including adaptation, editing, excerpting, or creating derivative works).")}</p>
|
||||
|
||||
<p class="api-tos-body">${_("All names, logos and seals (\"Trademarks\") that appear in the APIs, API Content, or on or through the services made available on or through the APIs, if any, are the property of their respective owners. You may not remove, alter, or obscure any copyright, Trademark, or other proprietary rightrs notices incorporated in or accompanying the API Content. If any third party revokes access to API Content owned or controlled by that third party, including without limitation any Trademarks, you must ensure that all API Content pertaining to that third party is deleted from your app, networks, systems and servers as soon as reasonably possible. If you stop using the APIs altogether or if your API access is revoked, you must delete all API Content in the same way.")}</p>
|
||||
<p class="api-copy-body">${_("All names, logos and seals (\"Trademarks\") that appear in the APIs, API Content, or on or through the services made available on or through the APIs, if any, are the property of their respective owners. You may not remove, alter, or obscure any copyright, Trademark, or other proprietary rightrs notices incorporated in or accompanying the API Content. If any third party revokes access to API Content owned or controlled by that third party, including without limitation any Trademarks, you must ensure that all API Content pertaining to that third party is deleted from your app, networks, systems and servers as soon as reasonably possible. If you stop using the APIs altogether or if your API access is revoked, you must delete all API Content in the same way.")}</p>
|
||||
|
||||
<p class="api-tos-body">${_("To the extent that you submit any content to {platform_name} in connection with your use of the APIs or any API Content, you hereby grant to {platform_name} a worldwide, non-exclusive, transferable, assignable, sub licensable, fully paid-up, royalty-free, perpetual, irrevocable right and license to host, transfer, display, perform, reproduce, modify, distribute, re-distribute, relicense and otherwise use, make available and exploit such content, in whole or in part, in any form and in any media formats and through any media channels (now known or hereafter developed).").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
<p class="api-copy-body">${_("To the extent that you submit any content to {platform_name} in connection with your use of the APIs or any API Content, you hereby grant to {platform_name} a worldwide, non-exclusive, transferable, assignable, sub licensable, fully paid-up, royalty-free, perpetual, irrevocable right and license to host, transfer, display, perform, reproduce, modify, distribute, re-distribute, relicense and otherwise use, make available and exploit such content, in whole or in part, in any form and in any media formats and through any media channels (now known or hereafter developed).").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
|
||||
<h2 class="api-access-request-subheading">${_("Privacy")}</h2>
|
||||
<h2 class="api-subheading">${_("Privacy")}</h2>
|
||||
|
||||
<p class="api-tos-body">${_("You agree to comply with all applicable privacy laws and regulations and to be transparent with respect to any collection and use of end user data. You will provide and adhere to a privacy policy for your Application that clearly and accurately describes to your end users what user information you collect and how you may use and share such information (including for advertising) with {platform_name} and other third parties.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
<p class="api-copy-body">${_("You agree to comply with all applicable privacy laws and regulations and to be transparent with respect to any collection and use of end user data. You will provide and adhere to a privacy policy for your Application that clearly and accurately describes to your end users what user information you collect and how you may use and share such information (including for advertising) with {platform_name} and other third parties.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
|
||||
<h2 class="api-access-request-subheading">${_("Right to Charge")}</h2>
|
||||
<h2 class="api-subheading">${_("Right to Charge")}</h2>
|
||||
|
||||
<p class="api-tos-body">${_("{platform_name} reserves the right to modify the Terms at any time without advance notice. Any changes to the Terms will be effective immediately upon posting on this page, with an updated effective date. By accessing or using the APIs after any changes have been made, you signify your agreement on a prospective basis to the modified Terms and all of the changes. Be sure to return to this page periodically to ensure familiarity with the most current version of the Terms.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
<p class="api-copy-body">${_("{platform_name} reserves the right to modify the Terms at any time without advance notice. Any changes to the Terms will be effective immediately upon posting on this page, with an updated effective date. By accessing or using the APIs after any changes have been made, you signify your agreement on a prospective basis to the modified Terms and all of the changes. Be sure to return to this page periodically to ensure familiarity with the most current version of the Terms.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
|
||||
<p class="api-tos-body">${_("{platform_name} may also update or modify the APIs from time to time without advance notice. These changes may affect your use of the APIs or the way your integration interacts with the API. If we make a change that is unacceptable to you, you should stop using the APIs. Continued use of the APIs means you accept the change.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
<p class="api-copy-body">${_("{platform_name} may also update or modify the APIs from time to time without advance notice. These changes may affect your use of the APIs or the way your integration interacts with the API. If we make a change that is unacceptable to you, you should stop using the APIs. Continued use of the APIs means you accept the change.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
|
||||
<h2 class="api-access-request-subheading">${_("Confidentiality")}</h2>
|
||||
<h2 class="api-subheading">${_("Confidentiality")}</h2>
|
||||
|
||||
<p class="api-tos-body">${_("Your credentials (such as client secret and IDs) are intended to be used solely by you. You will keep your credentials confidential and discourage others from using your credentials. Your credentials may not be embedded in open source projects.")}</p>
|
||||
<p class="api-copy-body">${_("Your credentials (such as client secret and IDs) are intended to be used solely by you. You will keep your credentials confidential and discourage others from using your credentials. Your credentials may not be embedded in open source projects.")}</p>
|
||||
|
||||
<p class="api-tos-body">${_("In the event that {platform_name} provides you with access to information specific to {platform_name} and/or the APIs that is either marked as \"Confidential\" or which a reasonable person would assume to be confidential or proprietary given the terms of its disclosure (\"Confidential Information\"), you agree to use this information only to use and build with the APIs. You may not disclose the Confidential Information to anyone without {platform_name}'s prior written consent, and you agree to protect the Confidential Information from unauthorized use and disclosure in the same way that you would protect your own confidential information. Confidential information does not include information that you independently developed, that was rightfully given to you by a third party without confidentiality obligation, or that becomes public through no fault of your own. You may disclose Confidential Information when compelled to do so by law if you provide {platform_name} with reasonable prior notice, unless a court orders that {platform_name} not receive notice.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
<p class="api-copy-body">${_("In the event that {platform_name} provides you with access to information specific to {platform_name} and/or the APIs that is either marked as \"Confidential\" or which a reasonable person would assume to be confidential or proprietary given the terms of its disclosure (\"Confidential Information\"), you agree to use this information only to use and build with the APIs. You may not disclose the Confidential Information to anyone without {platform_name}'s prior written consent, and you agree to protect the Confidential Information from unauthorized use and disclosure in the same way that you would protect your own confidential information. Confidential information does not include information that you independently developed, that was rightfully given to you by a third party without confidentiality obligation, or that becomes public through no fault of your own. You may disclose Confidential Information when compelled to do so by law if you provide {platform_name} with reasonable prior notice, unless a court orders that {platform_name} not receive notice.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
|
||||
<h2 class="api-access-request-subheading">${_("Disclaimer of Warranty / Limitation of Liabilities")}</h2>
|
||||
<h2 class="api-subheading">${_("Disclaimer of Warranty / Limitation of Liabilities")}</h2>
|
||||
|
||||
<p class="api-tos-body">${_("THE APIS AND ANY INFORMATION, API CONTENT OR SERVICES MADE AVAILABLE ON OR THROUGH THE APIS ARE PROVIDED \"AS IS\" AND \"AS AVAILABLE\" WITHOUT WARRANTY OF ANY KIND (EXPRESS, IMPLIED OR OTHERWISE), INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT, EXCEPT INSOFAR AS ANY SUCH IMPLIED WARRANTIES MAY NOT BE DISCLAIMED UNDER APPLICABLE LAW.")}</p>
|
||||
<p class="api-copy-body">${_("THE APIS AND ANY INFORMATION, API CONTENT OR SERVICES MADE AVAILABLE ON OR THROUGH THE APIS ARE PROVIDED \"AS IS\" AND \"AS AVAILABLE\" WITHOUT WARRANTY OF ANY KIND (EXPRESS, IMPLIED OR OTHERWISE), INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT, EXCEPT INSOFAR AS ANY SUCH IMPLIED WARRANTIES MAY NOT BE DISCLAIMED UNDER APPLICABLE LAW.")}</p>
|
||||
|
||||
<p class="api-tos-body">${_("{platform_name} AND THE {platform_name} PARTICIPANTS (AS HERINAFTER DEFINED) DO NOT WARRANT THAT THE APIS WILL OPERATE IN AN UNINTERRUPTED OR ERROR-FREE MANNER, THAT THE APIS ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS, OR THAT THE APIS OR API CONTENT PROVIDED WILL MEET YOUR NEEDS OR EXPECTATIONS. {platform_name} AND THE {platform_name} PARTICIPANTS ALSO MAKE NO WARRANTY ABOUT THE ACCURACY, COMPLETENESS, TIMELINESS, OR QUALITY OF THE APIS OR ANY API CONTENT, OR THAT ANY PARTICULAR API CONTENT WILL CONTINUE TO BE MADE AVAILABLE. \"{platform_name} PARTICIPANTS\" MEANS MIT, HARVARD, THE OTHER MEMBERS, THE ENTITIES PROVIDING INFORMATION, API CONTENT OR SERVICES FOR THE APIS, THE COURSE INSTRUCTORS AND THEIR STAFFS.").format(platform_name=settings.PLATFORM_NAME.upper())}</p>
|
||||
<p class="api-copy-body">${_("{platform_name} AND THE {platform_name} PARTICIPANTS (AS HERINAFTER DEFINED) DO NOT WARRANT THAT THE APIS WILL OPERATE IN AN UNINTERRUPTED OR ERROR-FREE MANNER, THAT THE APIS ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS, OR THAT THE APIS OR API CONTENT PROVIDED WILL MEET YOUR NEEDS OR EXPECTATIONS. {platform_name} AND THE {platform_name} PARTICIPANTS ALSO MAKE NO WARRANTY ABOUT THE ACCURACY, COMPLETENESS, TIMELINESS, OR QUALITY OF THE APIS OR ANY API CONTENT, OR THAT ANY PARTICULAR API CONTENT WILL CONTINUE TO BE MADE AVAILABLE. \"{platform_name} PARTICIPANTS\" MEANS MIT, HARVARD, THE OTHER MEMBERS, THE ENTITIES PROVIDING INFORMATION, API CONTENT OR SERVICES FOR THE APIS, THE COURSE INSTRUCTORS AND THEIR STAFFS.").format(platform_name=settings.PLATFORM_NAME.upper())}</p>
|
||||
|
||||
<p class="api-tos-body">${_("USE OF THE APIS, AND THE API CONTENT AND ANY SERVICES OBTAINED FROM OR THROUGH THE APIS, IS AT YOUR OWN RISK. YOUR ACCESS TO OR DOWNLOAD OF INFORMATION, MATERIALS OR DATA THROUGH THE APIS IS AT YOUR OWN DISCRETION AND RISK, AND YOU WILL BE SOLELY RESPONSIBLE FOR ANY DAMAGE TO YOUR PROPERTY (INCLUDING YOUR COMPUTER SYSTEM) OR LOSS OF DATA THAT RESULTS FROM THE DOWNLOAD OR USE OF SUCH MATERIAL OR DATA, UNLESS OTHERWISE EXPRESSLY PROVIDED FOR IN THE {platform_name} PRIVACY POLICY.").format(platform_name=settings.PLATFORM_NAME.upper())}</p>
|
||||
<p class="api-copy-body">${_("USE OF THE APIS, AND THE API CONTENT AND ANY SERVICES OBTAINED FROM OR THROUGH THE APIS, IS AT YOUR OWN RISK. YOUR ACCESS TO OR DOWNLOAD OF INFORMATION, MATERIALS OR DATA THROUGH THE APIS IS AT YOUR OWN DISCRETION AND RISK, AND YOU WILL BE SOLELY RESPONSIBLE FOR ANY DAMAGE TO YOUR PROPERTY (INCLUDING YOUR COMPUTER SYSTEM) OR LOSS OF DATA THAT RESULTS FROM THE DOWNLOAD OR USE OF SUCH MATERIAL OR DATA, UNLESS OTHERWISE EXPRESSLY PROVIDED FOR IN THE {platform_name} PRIVACY POLICY.").format(platform_name=settings.PLATFORM_NAME.upper())}</p>
|
||||
|
||||
<p class="api-tos-body">${_("TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, YOU AGREE THAT NEITHER {platform_name} NOR ANY OF THE {platform_name} PARTICIPANTS WILL BE LIABLE TO YOU FOR ANY LOSS OR DAMAGES, EITHER ACTUAL OR CONSEQUENTIAL, ARISING OUT OF OR RELATING TO THESE TERMS, OR YOUR (OR ANY THIRD PARTY'S) USE OF OR INABILITY TO USE THE APIS OR ANY API CONTENT, OR YOUR RELIANCE UPON INFORMATION OBTAINED FROM OR THROUGH THE APIS, WHETHER YOUR CLAIM IS BASED IN CONTRACT, TORT, STATUTORY OR OTHER LAW.").format(platform_name=settings.PLATFORM_NAME.upper())}</p>
|
||||
<p class="api-copy-body">${_("TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, YOU AGREE THAT NEITHER {platform_name} NOR ANY OF THE {platform_name} PARTICIPANTS WILL BE LIABLE TO YOU FOR ANY LOSS OR DAMAGES, EITHER ACTUAL OR CONSEQUENTIAL, ARISING OUT OF OR RELATING TO THESE TERMS, OR YOUR (OR ANY THIRD PARTY'S) USE OF OR INABILITY TO USE THE APIS OR ANY API CONTENT, OR YOUR RELIANCE UPON INFORMATION OBTAINED FROM OR THROUGH THE APIS, WHETHER YOUR CLAIM IS BASED IN CONTRACT, TORT, STATUTORY OR OTHER LAW.").format(platform_name=settings.PLATFORM_NAME.upper())}</p>
|
||||
|
||||
<p class="api-tos-body">${_("IN PARTICULAR, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER {platform_name} NOR ANY OF THE {platform_name} PARTICIPANTS WILL HAVE ANY LIABILITY FOR ANY CONSEQUENTIAL, INDIRECT, PUNITIVE, SPECIAL, EXEMPLARY OR INCIDENTAL DAMAGES, WHETHER FORESEEABLE OR UNFORESEEABLE AND WHETHER OR NOT {platform_name} OR ANY OF THE {platform_name} PARTICIPANTS HAS BEEN NEGLIGENT OR OTHERWISE AT FAULT (INCLUDING, BUT NOT LIMITED TO, CLAIMS FOR DEFAMATION, ERRORS, LOSS OF PROFITS, LOSS OF DATA OR INTERRUPTION IN AVAILABILITY OF DATA).").format(platform_name=settings.PLATFORM_NAME.upper())}</p>
|
||||
<p class="api-copy-body">${_("IN PARTICULAR, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER {platform_name} NOR ANY OF THE {platform_name} PARTICIPANTS WILL HAVE ANY LIABILITY FOR ANY CONSEQUENTIAL, INDIRECT, PUNITIVE, SPECIAL, EXEMPLARY OR INCIDENTAL DAMAGES, WHETHER FORESEEABLE OR UNFORESEEABLE AND WHETHER OR NOT {platform_name} OR ANY OF THE {platform_name} PARTICIPANTS HAS BEEN NEGLIGENT OR OTHERWISE AT FAULT (INCLUDING, BUT NOT LIMITED TO, CLAIMS FOR DEFAMATION, ERRORS, LOSS OF PROFITS, LOSS OF DATA OR INTERRUPTION IN AVAILABILITY OF DATA).").format(platform_name=settings.PLATFORM_NAME.upper())}</p>
|
||||
|
||||
<p class="api-tos-body">${_("CERTAIN STATE LAWS DO NOT ALLOW LIMITATIONS ON IMPLIED WARRANTIES OR THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES. IF THESE LAWS APPLY TO YOU, SOME OR ALL OF THE ABOVE DISCLAIMERS, EXCLUSIONS, OR LIMITATIONS MAY NOT APPLY TO YOU, AND YOU MIGHT HAVE ADDITIONAL RIGHTS.")}</p>
|
||||
<p class="api-copy-body">${_("CERTAIN STATE LAWS DO NOT ALLOW LIMITATIONS ON IMPLIED WARRANTIES OR THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES. IF THESE LAWS APPLY TO YOU, SOME OR ALL OF THE ABOVE DISCLAIMERS, EXCLUSIONS, OR LIMITATIONS MAY NOT APPLY TO YOU, AND YOU MIGHT HAVE ADDITIONAL RIGHTS.")}</p>
|
||||
|
||||
<p class="api-tos-body">${_("The APIs and API Content may include hyperlinks to sites maintained or controlled by others. {platform_name} and the {platform_name} Participants are not responsible for and do not routinely screen, approve, review or endorse the contents of or use of any of the products or services that may be offered at these sites. If you decide to access linked third-party websites, you do so at your own risk.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
<p class="api-copy-body">${_("The APIs and API Content may include hyperlinks to sites maintained or controlled by others. {platform_name} and the {platform_name} Participants are not responsible for and do not routinely screen, approve, review or endorse the contents of or use of any of the products or services that may be offered at these sites. If you decide to access linked third-party websites, you do so at your own risk.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
|
||||
<h2 class="api-access-request-subheading">Indemnification</h2>
|
||||
<h2 class="api-subheading">Indemnification</h2>
|
||||
|
||||
<p class="api-tos-body">${_("To the maximum extent permitted by applicable law, you agree to defend, hold harmless and indemnify {platform_name} and the {platform_name} Participants, and their respective subsidiaries, affiliates, officers, faculty, students, fellows, governing board members, agents and employees from and against any third-party claims, actions or demands arising out of, resulting from or in any way related to your use of the APIs and any API Content, including any liability or expense arising from any and all claims, losses, damages (actual and consequential), suits, judgments, litigation costs and attorneys' fees, of every kind and nature. In such a case, {platform_name} or one of the {platform_name} Participants will provide you with written notice of such claim, suit or action.").format(platform_name=settings.PLATFORM_NAME)}/</p>
|
||||
<p class="api-copy-body">${_("To the maximum extent permitted by applicable law, you agree to defend, hold harmless and indemnify {platform_name} and the {platform_name} Participants, and their respective subsidiaries, affiliates, officers, faculty, students, fellows, governing board members, agents and employees from and against any third-party claims, actions or demands arising out of, resulting from or in any way related to your use of the APIs and any API Content, including any liability or expense arising from any and all claims, losses, damages (actual and consequential), suits, judgments, litigation costs and attorneys' fees, of every kind and nature. In such a case, {platform_name} or one of the {platform_name} Participants will provide you with written notice of such claim, suit or action.").format(platform_name=settings.PLATFORM_NAME)}/</p>
|
||||
|
||||
<h2 class="api-access-request-subheading">${_("General Legal Terms")}</h2>
|
||||
<h2 class="api-subheading">${_("General Legal Terms")}</h2>
|
||||
|
||||
<p class="api-tos-body">${_("The Terms constitute the entire agreement between you and {platform_name} with respect to your use of the APIs and API Content, superseding any prior agreements between you and {platform_name} regarding your use of the APIs and API Content. The failure of {platform_name} to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by a court of competent jurisdiction to be invalid, the parties nevertheless agree that the court should endeavor to give effect to the parties' intentions as reflected in the provision and the other provisions of the Terms shall remain in full force and effect. The Terms do not create any third party beneficiary rights or any agency, partnership, or joint venture. For any notice provided to you by {platform_name} under these Terms, {platform_name} may notify you via the email address associated with your {platform_name} account.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
<p class="api-copy-body">${_("The Terms constitute the entire agreement between you and {platform_name} with respect to your use of the APIs and API Content, superseding any prior agreements between you and {platform_name} regarding your use of the APIs and API Content. The failure of {platform_name} to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by a court of competent jurisdiction to be invalid, the parties nevertheless agree that the court should endeavor to give effect to the parties' intentions as reflected in the provision and the other provisions of the Terms shall remain in full force and effect. The Terms do not create any third party beneficiary rights or any agency, partnership, or joint venture. For any notice provided to you by {platform_name} under these Terms, {platform_name} may notify you via the email address associated with your {platform_name} account.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
|
||||
<p class="api-tos-body">${_("You agree that the Terms, the APIs, and any claim or dispute arising out of or relating to the Terms or the APIs will be governed by the laws of the Commonwealth of Massachusetts, excluding its conflicts of law provisions. You agree that all such claims and disputes will be heard and resolved exclusively in the federal or state courts located in and serving Cambridge, Massachusetts, U.S.A. You consent to the personal jurisdiction of those courts over you for this purpose, and you waive and agree not to assert any objection to such proceedings in those courts (including any defense or objection of lack of proper jurisdiction or venue or inconvenience of forum). Notwithstanding the foregoing, you agree that {platform_name} shall still be allowed to apply to injunctive remedies (or an equivalent type of urgent legal relief) in any jursdiction.")}</p>
|
||||
<p class="api-copy-body">${_("You agree that the Terms, the APIs, and any claim or dispute arising out of or relating to the Terms or the APIs will be governed by the laws of the Commonwealth of Massachusetts, excluding its conflicts of law provisions. You agree that all such claims and disputes will be heard and resolved exclusively in the federal or state courts located in and serving Cambridge, Massachusetts, U.S.A. You consent to the personal jurisdiction of those courts over you for this purpose, and you waive and agree not to assert any objection to such proceedings in those courts (including any defense or objection of lack of proper jurisdiction or venue or inconvenience of forum). Notwithstanding the foregoing, you agree that {platform_name} shall still be allowed to apply to injunctive remedies (or an equivalent type of urgent legal relief) in any jursdiction.")}</p>
|
||||
|
||||
<h2 class="api-access-request-subheading">${_("Termination")}</h2>
|
||||
<h2 class="api-subheading">${_("Termination")}</h2>
|
||||
|
||||
<p class="api-tos-body">${_("You may stop using the APIs at any time. You agree that {platform_name}, in its sole discretion and at any time, may terminate your use of the APIs or any API Content for any reason or no reason, without prior notice or liabiliy.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
<p class="api-copy-body">${_("You may stop using the APIs at any time. You agree that {platform_name}, in its sole discretion and at any time, may terminate your use of the APIs or any API Content for any reason or no reason, without prior notice or liabiliy.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
|
||||
<p class="api-tos-body">${_("{platform_name} and the {platform_name} Participants reserve the right at any time in their sole discretion to cancel, delay, reschedule or alter the format of any API or API Content offered through {platform_name}, or to cease providing any part or all of the APIs or API Content or related services, and you agree that neither {platform_name} nor any of the {platform_name} Participants will have any liability to you for such an action.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
<p class="api-copy-body">${_("{platform_name} and the {platform_name} Participants reserve the right at any time in their sole discretion to cancel, delay, reschedule or alter the format of any API or API Content offered through {platform_name}, or to cease providing any part or all of the APIs or API Content or related services, and you agree that neither {platform_name} nor any of the {platform_name} Participants will have any liability to you for such an action.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
|
||||
<p class="api-tos-body">${_("Upon any termination of the Terms or discontinuation of your access to an API for any reason, your right to use any API and API Content will immediately cease. You will immediately stop using the APIs and delete any cached or stored API Content. All provisions of the Terms that by their nature should survive termination shall survive termination, including, without limitation, ownership provisions, warranty disclaimers, and limitation of liability. Termination of your access to and use of the APIs and API Content shall not relieve you of any obligations arising or accrusing prior to such termination or limit any liability that you otherwise may have to {platform_name}, including without limitation any indemnification obligations contained herein.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
<p class="api-copy-body">${_("Upon any termination of the Terms or discontinuation of your access to an API for any reason, your right to use any API and API Content will immediately cease. You will immediately stop using the APIs and delete any cached or stored API Content. All provisions of the Terms that by their nature should survive termination shall survive termination, including, without limitation, ownership provisions, warranty disclaimers, and limitation of liability. Termination of your access to and use of the APIs and API Content shall not relieve you of any obligations arising or accrusing prior to such termination or limit any liability that you otherwise may have to {platform_name}, including without limitation any indemnification obligations contained herein.").format(platform_name=settings.PLATFORM_NAME)}</p>
|
||||
</div>
|
||||
|
||||
@@ -15,5 +15,4 @@ class ApiAccessRequestAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ('user', 'website', 'reason', 'company_name', 'company_address', 'contacted', )
|
||||
exclude = ('site',)
|
||||
|
||||
|
||||
admin.site.register(ApiAccessConfig, ConfigurationModelAdmin)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Forms for API management."""
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
|
||||
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest, Catalog
|
||||
from openedx.core.djangoapps.api_admin.widgets import TermsOfServiceCheckboxInput
|
||||
|
||||
|
||||
@@ -32,3 +33,50 @@ class ApiAccessRequestForm(forms.ModelForm):
|
||||
# Get rid of the colons at the end of the field labels.
|
||||
kwargs.setdefault('label_suffix', '')
|
||||
super(ApiAccessRequestForm, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class ViewersWidget(forms.widgets.TextInput):
|
||||
"""Form widget to display a comma-separated list of usernames."""
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
return super(ViewersWidget, self).render(name, ', '.join(value), attrs)
|
||||
|
||||
|
||||
class ViewersField(forms.Field):
|
||||
"""Custom form field for a comma-separated list of usernames."""
|
||||
|
||||
widget = ViewersWidget
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': 'Enter a comma-separated list of usernames.',
|
||||
}
|
||||
|
||||
def to_python(self, value):
|
||||
"""Parse out a comma-separated list of usernames."""
|
||||
return [username.strip() for username in value.split(',')]
|
||||
|
||||
def validate(self, value):
|
||||
super(ViewersField, self).validate(value)
|
||||
nonexistent_users = []
|
||||
for username in value:
|
||||
try:
|
||||
User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
nonexistent_users.append(username)
|
||||
if nonexistent_users:
|
||||
raise forms.ValidationError(
|
||||
_('The following users do not exist: {usernames}.').format(usernames=nonexistent_users)
|
||||
)
|
||||
|
||||
|
||||
class CatalogForm(forms.ModelForm):
|
||||
"""Form to create a catalog."""
|
||||
|
||||
viewers = ViewersField()
|
||||
|
||||
class Meta(object):
|
||||
model = Catalog
|
||||
fields = ('name', 'query', 'viewers')
|
||||
help_texts = {
|
||||
'viewers': _('Comma-separated list of usernames which will be able to view this catalog.'),
|
||||
}
|
||||
|
||||
26
openedx/core/djangoapps/api_admin/migrations/0006_catalog.py
Normal file
26
openedx/core/djangoapps/api_admin/migrations/0006_catalog.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api_admin', '0005_auto_20160414_1232'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Catalog',
|
||||
fields=[
|
||||
('id', models.IntegerField(serialize=False, primary_key=True)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('query', models.TextField()),
|
||||
('viewers', models.TextField()),
|
||||
],
|
||||
options={
|
||||
'managed': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -179,3 +179,45 @@ def _send_decision_email(instance):
|
||||
instance.contacted = True
|
||||
except SMTPException:
|
||||
log.exception('Error sending API user notification email for request [%s].', instance.id)
|
||||
|
||||
|
||||
class Catalog(models.Model):
|
||||
"""A (non-Django-managed) model for Catalogs in the course discovery service."""
|
||||
|
||||
id = models.IntegerField(primary_key=True) # pylint: disable=invalid-name
|
||||
name = models.CharField(max_length=255, null=False, blank=False)
|
||||
query = models.TextField(null=False, blank=False)
|
||||
viewers = models.TextField()
|
||||
|
||||
class Meta(object):
|
||||
# Catalogs live in course discovery, so we do not create any
|
||||
# tables in LMS. Instead we override the save method to not
|
||||
# touch the database, and use our API client to communicate
|
||||
# with discovery.
|
||||
managed = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
attributes = kwargs.get('attributes')
|
||||
if attributes:
|
||||
self.id = attributes['id'] # pylint: disable=invalid-name
|
||||
self.name = attributes['name']
|
||||
self.query = attributes['query']
|
||||
self.viewers = attributes['viewers']
|
||||
else:
|
||||
super(Catalog, self).__init__(*args, **kwargs)
|
||||
|
||||
def save(self, **kwargs): # pylint: disable=unused-argument
|
||||
return None
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
"""Return a dictionary representation of this catalog."""
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'query': self.query,
|
||||
'viewers': self.viewers,
|
||||
}
|
||||
|
||||
def __unicode__(self):
|
||||
return u'Catalog {name} [{query}]'.format(name=self.name, query=self.query)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""Factories for API management."""
|
||||
import factory
|
||||
from factory.fuzzy import FuzzyInteger, FuzzyText
|
||||
from factory.django import DjangoModelFactory
|
||||
from oauth2_provider.models import get_application_model
|
||||
|
||||
from microsite_configuration.tests.factories import SiteFactory
|
||||
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
|
||||
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest, Catalog
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
@@ -27,3 +28,14 @@ class ApplicationFactory(DjangoModelFactory):
|
||||
|
||||
authorization_grant_type = Application.GRANT_CLIENT_CREDENTIALS
|
||||
client_type = Application.CLIENT_CONFIDENTIAL
|
||||
|
||||
|
||||
class CatalogFactory(DjangoModelFactory):
|
||||
"""Factory for Catalog objects."""
|
||||
|
||||
class Meta(object):
|
||||
model = Catalog
|
||||
|
||||
id = FuzzyInteger(0, 999) # pylint: disable=invalid-name
|
||||
query = '*'
|
||||
name = FuzzyText(prefix='test-catalog')
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
#pylint: disable=missing-docstring
|
||||
import unittest
|
||||
import json
|
||||
from urlparse import urljoin
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from edx_oauth2_provider.tests.factories import ClientFactory
|
||||
import httpretty
|
||||
from oauth2_provider.models import get_application_model
|
||||
|
||||
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest, ApiAccessConfig
|
||||
from openedx.core.djangoapps.api_admin.tests.factories import ApiAccessRequestFactory, ApplicationFactory
|
||||
from openedx.core.djangoapps.api_admin.tests.factories import (
|
||||
ApiAccessRequestFactory, ApplicationFactory, CatalogFactory
|
||||
)
|
||||
from openedx.core.djangoapps.api_admin.tests.utils import VALID_DATA
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
Application = get_application_model() # pylint: disable=invalid-name
|
||||
|
||||
MOCK_CATALOG_API_URL_ROOT = 'https://api.example.com/'
|
||||
|
||||
|
||||
class ApiAdminTest(TestCase):
|
||||
|
||||
@@ -206,3 +214,169 @@ class ApiTosViewTest(ApiAdminTest):
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('Terms of Service', response.content)
|
||||
|
||||
|
||||
class CatalogTest(ApiAdminTest):
|
||||
|
||||
def setUp(self):
|
||||
super(CatalogTest, self).setUp()
|
||||
password = 'abc123'
|
||||
self.user = UserFactory(password=password, is_staff=True)
|
||||
self.client.login(username=self.user.username, password=password)
|
||||
ClientFactory(user=self.user, name='course-discovery', url=MOCK_CATALOG_API_URL_ROOT)
|
||||
|
||||
def mock_catalog_api(self, url, data, method=httpretty.GET, status_code=200):
|
||||
self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Catalog API calls.')
|
||||
httpretty.reset()
|
||||
httpretty.register_uri(
|
||||
method,
|
||||
urljoin(MOCK_CATALOG_API_URL_ROOT, url),
|
||||
body=json.dumps(data),
|
||||
content_type='application/json',
|
||||
status=status_code
|
||||
)
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class CatalogSearchViewTest(CatalogTest):
|
||||
|
||||
def setUp(self):
|
||||
super(CatalogSearchViewTest, self).setUp()
|
||||
self.url = reverse('api_admin:catalog-search')
|
||||
|
||||
def test_get(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@httpretty.activate
|
||||
def test_post(self):
|
||||
catalog_user = UserFactory()
|
||||
self.mock_catalog_api('api/v1/catalogs/', {'results': []})
|
||||
response = self.client.post(self.url, {'username': catalog_user.username})
|
||||
self.assertRedirects(response, reverse('api_admin:catalog-list', kwargs={'username': catalog_user.username}))
|
||||
|
||||
def test_post_without_username(self):
|
||||
response = self.client.post(self.url, {'username': ''})
|
||||
self.assertRedirects(response, reverse('api_admin:catalog-search'))
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class CatalogListViewTest(CatalogTest):
|
||||
|
||||
def setUp(self):
|
||||
super(CatalogListViewTest, self).setUp()
|
||||
self.catalog_user = UserFactory()
|
||||
self.url = reverse('api_admin:catalog-list', kwargs={'username': self.catalog_user.username})
|
||||
|
||||
@httpretty.activate
|
||||
def test_get(self):
|
||||
catalog = CatalogFactory(viewers=[self.catalog_user.username])
|
||||
self.mock_catalog_api('api/v1/catalogs/', {
|
||||
'results': [catalog.attributes]
|
||||
})
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(catalog.name, response.content.decode('utf-8'))
|
||||
|
||||
@httpretty.activate
|
||||
def test_post(self):
|
||||
catalog_data = {
|
||||
'name': 'test-catalog',
|
||||
'query': '*',
|
||||
'viewers': [self.catalog_user.username]
|
||||
}
|
||||
catalog_id = 123
|
||||
self.mock_catalog_api('api/v1/catalogs/', dict(catalog_data, id=catalog_id), method=httpretty.POST)
|
||||
response = self.client.post(self.url, catalog_data)
|
||||
self.assertEqual(httpretty.last_request().method, 'POST')
|
||||
self.mock_catalog_api('api/v1/catalogs/{}/'.format(catalog_id), CatalogFactory().attributes)
|
||||
self.assertRedirects(response, reverse('api_admin:catalog-edit', kwargs={'catalog_id': catalog_id}))
|
||||
|
||||
@httpretty.activate
|
||||
def test_post_invalid(self):
|
||||
catalog = CatalogFactory(viewers=[self.catalog_user.username])
|
||||
self.mock_catalog_api('api/v1/catalogs/', {
|
||||
'results': [catalog.attributes]
|
||||
})
|
||||
response = self.client.post(self.url, {
|
||||
'name': '',
|
||||
'query': '*',
|
||||
'viewers': [self.catalog_user.username]
|
||||
})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
# Assert that no POST was made to the catalog API
|
||||
self.assertEqual(len([r for r in httpretty.httpretty.latest_requests if r.method == 'POST']), 0)
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class CatalogEditViewTest(CatalogTest):
|
||||
|
||||
def setUp(self):
|
||||
super(CatalogEditViewTest, self).setUp()
|
||||
self.catalog_user = UserFactory()
|
||||
self.catalog = CatalogFactory(viewers=[self.catalog_user.username])
|
||||
self.url = reverse('api_admin:catalog-edit', kwargs={'catalog_id': self.catalog.id})
|
||||
|
||||
@httpretty.activate
|
||||
def test_get(self):
|
||||
self.mock_catalog_api('api/v1/catalogs/{}/'.format(self.catalog.id), self.catalog.attributes)
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(self.catalog.name, response.content.decode('utf-8'))
|
||||
|
||||
@httpretty.activate
|
||||
def test_delete(self):
|
||||
self.mock_catalog_api(
|
||||
'api/v1/catalogs/{}/'.format(self.catalog.id),
|
||||
self.catalog.attributes,
|
||||
method=httpretty.DELETE
|
||||
)
|
||||
response = self.client.post(self.url, {'delete-catalog': 'on'})
|
||||
self.assertRedirects(response, reverse('api_admin:catalog-search'))
|
||||
self.assertEqual(httpretty.last_request().method, 'DELETE')
|
||||
self.assertEqual(
|
||||
httpretty.last_request().path,
|
||||
'/api/v1/catalogs/{}/'.format(self.catalog.id)
|
||||
)
|
||||
self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
|
||||
|
||||
@httpretty.activate
|
||||
def test_edit(self):
|
||||
self.mock_catalog_api(
|
||||
'api/v1/catalogs/{}/'.format(self.catalog.id),
|
||||
self.catalog.attributes, method=httpretty.PATCH
|
||||
)
|
||||
new_attributes = dict(self.catalog.attributes, **{'delete-catalog': 'off', 'name': 'changed'})
|
||||
response = self.client.post(self.url, new_attributes)
|
||||
self.mock_catalog_api('api/v1/catalogs/{}/'.format(self.catalog.id), new_attributes)
|
||||
self.assertRedirects(response, reverse('api_admin:catalog-edit', kwargs={'catalog_id': self.catalog.id}))
|
||||
|
||||
@httpretty.activate
|
||||
def test_edit_invalid(self):
|
||||
self.mock_catalog_api('api/v1/catalogs/{}/'.format(self.catalog.id), self.catalog.attributes)
|
||||
new_attributes = dict(self.catalog.attributes, **{'delete-catalog': 'off', 'name': ''})
|
||||
response = self.client.post(self.url, new_attributes)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
# Assert that no PATCH was made to the Catalog API
|
||||
self.assertEqual(len([r for r in httpretty.httpretty.latest_requests if r.method == 'PATCH']), 0)
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class CatalogPreviewViewTest(CatalogTest):
|
||||
|
||||
def setUp(self):
|
||||
super(CatalogPreviewViewTest, self).setUp()
|
||||
self.url = reverse('api_admin:catalog-preview')
|
||||
|
||||
@httpretty.activate
|
||||
def test_get(self):
|
||||
data = {'count': 1, 'results': ['test data'], 'next': None, 'prev': None}
|
||||
self.mock_catalog_api('api/v1/courses/', data)
|
||||
response = self.client.get(self.url, {'q': '*'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(json.loads(response.content), data)
|
||||
|
||||
def test_get_without_query(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(json.loads(response.content), {'count': 0, 'results': [], 'next': None, 'prev': None})
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
"""URLs for API access management."""
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from openedx.core.djangoapps.api_admin.decorators import api_access_enabled_or_404
|
||||
from openedx.core.djangoapps.api_admin.views import ApiRequestView, ApiRequestStatusView, ApiTosView
|
||||
from openedx.core.djangoapps.api_admin.views import (
|
||||
ApiRequestView, ApiRequestStatusView, ApiTosView, CatalogListView, CatalogEditView,
|
||||
CatalogPreviewView, CatalogSearchView
|
||||
)
|
||||
|
||||
urlpatterns = (
|
||||
url(
|
||||
@@ -17,6 +21,42 @@ urlpatterns = (
|
||||
api_access_enabled_or_404(ApiTosView.as_view()),
|
||||
name="api-tos"
|
||||
),
|
||||
url(
|
||||
r'^catalogs/preview/$',
|
||||
staff_member_required(
|
||||
api_access_enabled_or_404(CatalogPreviewView.as_view()),
|
||||
login_url='dashboard',
|
||||
redirect_field_name=None
|
||||
),
|
||||
name='catalog-preview',
|
||||
),
|
||||
url(
|
||||
r'^catalogs/user/(?P<username>[\w.@+-]+)/$',
|
||||
staff_member_required(
|
||||
api_access_enabled_or_404(CatalogListView.as_view()),
|
||||
login_url='dashboard',
|
||||
redirect_field_name=None
|
||||
),
|
||||
name='catalog-list',
|
||||
),
|
||||
url(
|
||||
r'^catalogs/(?P<catalog_id>\d+)/$',
|
||||
staff_member_required(
|
||||
api_access_enabled_or_404(CatalogEditView.as_view()),
|
||||
login_url='dashboard',
|
||||
redirect_field_name=None
|
||||
),
|
||||
name='catalog-edit',
|
||||
),
|
||||
url(
|
||||
r'^catalogs/$',
|
||||
staff_member_required(
|
||||
api_access_enabled_or_404(CatalogSearchView.as_view()),
|
||||
login_url='dashboard',
|
||||
redirect_field_name=None
|
||||
),
|
||||
name='catalog-search',
|
||||
),
|
||||
url(
|
||||
r'^$',
|
||||
api_access_enabled_or_404(login_required(ApiRequestView.as_view())),
|
||||
|
||||
15
openedx/core/djangoapps/api_admin/utils.py
Normal file
15
openedx/core/djangoapps/api_admin/utils.py
Normal file
@@ -0,0 +1,15 @@
|
||||
""" Course Discovery API Service. """
|
||||
from edx_rest_api_client.client import EdxRestApiClient
|
||||
from openedx.core.lib.token_utils import get_id_token
|
||||
from provider.oauth2.models import Client
|
||||
|
||||
CLIENT_NAME = 'course-discovery'
|
||||
|
||||
|
||||
def course_discovery_api_client(user):
|
||||
""" Returns a Course Discovery API client setup with authentication for the specified user. """
|
||||
course_discovery_client = Client.objects.get(name=CLIENT_NAME)
|
||||
return EdxRestApiClient(
|
||||
course_discovery_client.url,
|
||||
jwt=get_id_token(user, CLIENT_NAME)
|
||||
)
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.core.urlresolvers import reverse_lazy, reverse
|
||||
from django.http.response import JsonResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import View
|
||||
@@ -15,8 +16,9 @@ from oauth2_provider.views import ApplicationRegistration
|
||||
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from openedx.core.djangoapps.api_admin.decorators import require_api_access
|
||||
from openedx.core.djangoapps.api_admin.forms import ApiAccessRequestForm
|
||||
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
|
||||
from openedx.core.djangoapps.api_admin.forms import ApiAccessRequestForm, CatalogForm
|
||||
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest, Catalog
|
||||
from openedx.core.djangoapps.api_admin.utils import course_discovery_api_client
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -115,3 +117,112 @@ class ApiTosView(TemplateView):
|
||||
"""View to show the API Terms of Service."""
|
||||
|
||||
template_name = 'api_admin/terms_of_service.html'
|
||||
|
||||
|
||||
class CatalogSearchView(View):
|
||||
"""View to search for catalogs belonging to a user."""
|
||||
|
||||
def get(self, request):
|
||||
"""Display a form to search for catalogs belonging to a user."""
|
||||
return render_to_response('api_admin/catalogs/search.html')
|
||||
|
||||
def post(self, request):
|
||||
"""Redirect to the list view for the given user."""
|
||||
username = request.POST.get('username')
|
||||
# If no username is provided, bounce back to this page.
|
||||
if not username:
|
||||
return redirect(reverse('api_admin:catalog-search'))
|
||||
return redirect(reverse('api_admin:catalog-list', kwargs={'username': username}))
|
||||
|
||||
|
||||
class CatalogListView(View):
|
||||
"""View to list existing catalogs and create new ones."""
|
||||
|
||||
template = 'api_admin/catalogs/list.html'
|
||||
|
||||
def get(self, request, username):
|
||||
"""Display a list of a user's catalogs."""
|
||||
client = course_discovery_api_client(request.user)
|
||||
response = client.api.v1.catalogs.get(username=username)
|
||||
catalogs = [Catalog(attributes=catalog) for catalog in response['results']]
|
||||
return render_to_response(self.template, {
|
||||
'username': username,
|
||||
'catalogs': catalogs,
|
||||
'form': CatalogForm(initial={'viewers': [username]}),
|
||||
'preview_url': reverse('api_admin:catalog-preview'),
|
||||
'catalog_api_url': client.api.v1.courses.url(),
|
||||
})
|
||||
|
||||
def post(self, request, username):
|
||||
"""Create a new catalog for a user."""
|
||||
form = CatalogForm(request.POST)
|
||||
client = course_discovery_api_client(request.user)
|
||||
|
||||
if not form.is_valid():
|
||||
response = client.api.v1.catalogs.get(username=username)
|
||||
catalogs = [Catalog(attributes=catalog) for catalog in response['results']]
|
||||
return render_to_response(self.template, {
|
||||
'form': form,
|
||||
'catalogs': catalogs,
|
||||
'username': username,
|
||||
'preview_url': reverse('api_admin:catalog-preview'),
|
||||
'catalog_api_url': client.api.v1.courses.url(),
|
||||
}, status=400)
|
||||
|
||||
attrs = form.instance.attributes
|
||||
catalog = client.api.v1.catalogs.post(attrs)
|
||||
return redirect(reverse('api_admin:catalog-edit', kwargs={'catalog_id': catalog['id']}))
|
||||
|
||||
|
||||
class CatalogEditView(View):
|
||||
"""View to edit an individual catalog."""
|
||||
|
||||
def get(self, request, catalog_id):
|
||||
"""Display a form to edit this catalog."""
|
||||
client = course_discovery_api_client(request.user)
|
||||
response = client.api.v1.catalogs(catalog_id).get()
|
||||
catalog = Catalog(attributes=response)
|
||||
form = CatalogForm(instance=catalog)
|
||||
return render_to_response('api_admin/catalogs/edit.html', {
|
||||
'catalog': catalog,
|
||||
'form': form,
|
||||
'preview_url': reverse('api_admin:catalog-preview'),
|
||||
'catalog_api_url': client.api.v1.courses.url(),
|
||||
})
|
||||
|
||||
def post(self, request, catalog_id):
|
||||
"""Update or delete this catalog."""
|
||||
client = course_discovery_api_client(request.user)
|
||||
if request.POST.get('delete-catalog') == 'on':
|
||||
client.api.v1.catalogs(catalog_id).delete()
|
||||
return redirect(reverse('api_admin:catalog-search'))
|
||||
form = CatalogForm(request.POST)
|
||||
if not form.is_valid():
|
||||
response = client.api.v1.catalogs(catalog_id).get()
|
||||
catalog = Catalog(attributes=response)
|
||||
return render_to_response('api_admin/catalogs/edit.html', {
|
||||
'catalog': catalog,
|
||||
'form': form,
|
||||
'preview_url': reverse('api_admin:catalog-preview'),
|
||||
'catalog_api_url': client.api.v1.courses.url(),
|
||||
}, status=400)
|
||||
catalog = client.api.v1.catalogs(catalog_id).patch(form.instance.attributes)
|
||||
return redirect(reverse('api_admin:catalog-edit', kwargs={'catalog_id': catalog['id']}))
|
||||
|
||||
|
||||
class CatalogPreviewView(View):
|
||||
"""Endpoint to preview courses for a query."""
|
||||
|
||||
def get(self, request):
|
||||
"""
|
||||
Return the results of a query against the course catalog API. If no
|
||||
query parameter is given, returns an empty result set.
|
||||
"""
|
||||
client = course_discovery_api_client(request.user)
|
||||
# Just pass along the request params including limit/offset pagination
|
||||
if 'q' in request.GET:
|
||||
results = client.api.v1.courses.get(**request.GET)
|
||||
# Ensure that we don't just return all the courses if no query is given
|
||||
else:
|
||||
results = {'count': 0, 'results': [], 'next': None, 'prev': None}
|
||||
return JsonResponse(results)
|
||||
|
||||
21
openedx/core/lib/rsa_key_utils.py
Normal file
21
openedx/core/lib/rsa_key_utils.py
Normal file
@@ -0,0 +1,21 @@
|
||||
""" Utils for RSA keys"""
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives.serialization import(
|
||||
Encoding, PublicFormat, PrivateFormat, NoEncryption
|
||||
)
|
||||
|
||||
|
||||
def generate_rsa_key_pair(key_size=2048):
|
||||
""" Generates a public and private RSA PEM encoded key pair"""
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=key_size,
|
||||
backend=default_backend()
|
||||
)
|
||||
private_key_str = private_key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
|
||||
public_key_str = private_key.public_key().public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
|
||||
|
||||
# Not intented for programmatic use, so we print the keys out
|
||||
print public_key_str
|
||||
print private_key_str
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Utilities for working with ID tokens."""
|
||||
import datetime
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
import jwt
|
||||
@@ -63,3 +65,51 @@ def get_id_token(user, client_name):
|
||||
}
|
||||
|
||||
return jwt.encode(payload, client.client_secret)
|
||||
|
||||
|
||||
def get_asymmetric_token(user, client_id):
|
||||
"""Construct a JWT signed with this app's private key.
|
||||
|
||||
The JWT includes the following claims:
|
||||
|
||||
preferred_username (str): The user's username. The claim name is borrowed from edx-oauth2-provider.
|
||||
name (str): The user's full name.
|
||||
email (str): The user's email address.
|
||||
administrator (Boolean): Whether the user has staff permissions.
|
||||
iss (str): Registered claim. Identifies the principal that issued the JWT.
|
||||
exp (int): Registered claim. Identifies the expiration time on or after which
|
||||
the JWT must NOT be accepted for processing.
|
||||
iat (int): Registered claim. Identifies the time at which the JWT was issued.
|
||||
sub (int): Registered claim. Identifies the user. This implementation uses the raw user id.
|
||||
|
||||
Arguments:
|
||||
user (User): User for which to generate the JWT.
|
||||
|
||||
Returns:
|
||||
str: the JWT
|
||||
|
||||
"""
|
||||
private_key = load_pem_private_key(settings.PRIVATE_RSA_KEY, None, default_backend())
|
||||
|
||||
try:
|
||||
# Service users may not have user profiles.
|
||||
full_name = UserProfile.objects.get(user=user).name
|
||||
except UserProfile.DoesNotExist:
|
||||
full_name = None
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
expires_in = getattr(settings, 'OAUTH_ID_TOKEN_EXPIRATION', 30)
|
||||
|
||||
payload = {
|
||||
'preferred_username': user.username,
|
||||
'name': full_name,
|
||||
'email': user.email,
|
||||
'administrator': user.is_staff,
|
||||
'iss': settings.OAUTH_OIDC_ISSUER,
|
||||
'exp': now + datetime.timedelta(seconds=expires_in),
|
||||
'iat': now,
|
||||
'aud': client_id,
|
||||
'sub': anonymous_id_for_user(user, None),
|
||||
}
|
||||
|
||||
return jwt.encode(payload, private_key, algorithm='RS512')
|
||||
|
||||
@@ -10,6 +10,7 @@ bleach==1.4
|
||||
html5lib==0.999
|
||||
boto==2.39.0
|
||||
celery==3.1.18
|
||||
cryptography==1.3.1
|
||||
cssselect==0.9.1
|
||||
dealer==2.0.4
|
||||
defusedxml==0.4.1
|
||||
|
||||
Reference in New Issue
Block a user