Commit dc318a57 by Clinton Blackburn

Merge pull request #249 from edx/clintonb/course-edit-view

Added course edit view
parents 28ffaa5a 596685ce
...@@ -23,6 +23,11 @@ ...@@ -23,6 +23,11 @@
"text": "~2.0.14", "text": "~2.0.14",
"backbone.paginator": "~2.0.2", "backbone.paginator": "~2.0.2",
"moment": "~2.10.3", "moment": "~2.10.3",
"underscore.string": "~3.1.1" "underscore.string": "~3.1.1",
"backbone-super": "~1.0.4",
"backbone-route-filter": "~0.1.2",
"backbone-relational": "~0.9.0",
"backbone-validation": "~0.11.5",
"backbone.stickit": "~0.9.2"
} }
} }
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
name: 'js/config' name: 'js/config'
}, },
{ {
name: 'js/pages/course_list_page', name: 'js/apps/course_admin_app',
exclude: ['js/common'] exclude: ['js/common']
}, },
{ {
......
from django.conf import settings
def core(_request):
return {
'platform_name': settings.PLATFORM_NAME
}
from django.test import TestCase, override_settings, RequestFactory
from ecommerce.core.context_processors import core
PLATFORM_NAME = 'Test Platform'
class CoreContextProcessorTests(TestCase):
@override_settings(PLATFORM_NAME=PLATFORM_NAME)
def test_core(self):
request = RequestFactory().get('/')
self.assertDictEqual(core(request), {'platform_name': PLATFORM_NAME})
...@@ -37,8 +37,8 @@ class CourseMigrationViewTests(UserMixin, TestCase): ...@@ -37,8 +37,8 @@ class CourseMigrationViewTests(UserMixin, TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
class CourseListViewTests(UserMixin, TestCase): class CourseAppViewTests(UserMixin, TestCase):
path = reverse('courses:list') path = reverse('courses:app', args=[''])
def test_login_required(self): def test_login_required(self):
""" Users are required to login before accessing the view. """ """ Users are required to login before accessing the view. """
......
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from ecommerce.core.constants import COURSE_ID_PATTERN
from ecommerce.courses import views from ecommerce.courses import views
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^$', views.CourseListView.as_view(), name='list'),
url(r'^migrate/$', views.CourseMigrationView.as_view(), name='migrate'), url(r'^migrate/$', views.CourseMigrationView.as_view(), name='migrate'),
url(r'^{}/$'.format(COURSE_ID_PATTERN), views.CourseDetailView.as_view(), name='detail'),
# Declare all paths above this line to avoid dropping into the Course Admin Tool (which does its own routing)
url(r'^(.*)$', views.CourseAppView.as_view(), name='app'),
) )
...@@ -6,39 +6,22 @@ from django.contrib.auth.decorators import login_required ...@@ -6,39 +6,22 @@ from django.contrib.auth.decorators import login_required
from django.core.management import call_command from django.core.management import call_command
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.generic import View, ListView, TemplateView from django.views.generic import View, TemplateView
from ecommerce.courses.models import Course
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CourseListView(ListView): class StaffOnlyMixin(object):
model = Course
context_object_name = 'courses'
@method_decorator(login_required) @method_decorator(login_required)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff: if not request.user.is_staff:
raise Http404 raise Http404
return super(CourseListView, self).dispatch(request, *args, **kwargs) return super(StaffOnlyMixin, self).dispatch(request, *args, **kwargs)
class CourseDetailView(TemplateView): class CourseAppView(StaffOnlyMixin, TemplateView):
template_name = 'courses/course_detail.html' template_name = 'courses/course_app.html'
def get(self, request, *args, **kwargs):
if not Course.objects.filter(id=kwargs['course_id']).exists():
raise Http404
return super(CourseDetailView, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(CourseDetailView, self).get_context_data()
context.update({
'course_id': kwargs['course_id']
})
return context
class CourseMigrationView(View): class CourseMigrationView(View):
......
from rest_framework import pagination
class PageNumberPagination(pagination.PageNumberPagination):
page_size_query_param = 'page_size'
# NOTE (CCB): This is a hack, necessary until the frontend
# can properly follow our paginated lists.
max_page_size = 10000
...@@ -164,6 +164,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ...@@ -164,6 +164,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'oscar.apps.checkout.context_processors.checkout', 'oscar.apps.checkout.context_processors.checkout',
'oscar.apps.customer.notifications.context_processors.notifications', 'oscar.apps.customer.notifications.context_processors.notifications',
'oscar.core.context_processors.metadata', 'oscar.core.context_processors.metadata',
'ecommerce.core.context_processors.core',
) )
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders
...@@ -398,7 +399,7 @@ REST_FRAMEWORK = { ...@@ -398,7 +399,7 @@ REST_FRAMEWORK = {
'ecommerce.extensions.api.authentication.BearerAuthentication', 'ecommerce.extensions.api.authentication.BearerAuthentication',
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
), ),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'DEFAULT_PAGINATION_CLASS': 'ecommerce.extensions.api.pagination.PageNumberPagination',
'PAGE_SIZE': 20, 'PAGE_SIZE': 20,
'DEFAULT_THROTTLE_CLASSES': ( 'DEFAULT_THROTTLE_CLASSES': (
'rest_framework.throttling.UserRateThrottle', 'rest_framework.throttling.UserRateThrottle',
......
require([
'backbone',
'routers/course_router'
],
function (Backbone,
CourseRouter) {
'use strict';
var navigate,
courseApp;
/**
* Navigate to a new page within the app.
*
* Attempts to open the link in a new tab/window behave as the user expects, however the app
* and data will be reloaded in the new tab/window.
*
* @param {Event} event - Event being handled.
* @returns {boolean} - Indicates if event handling succeeded (always true).
*/
navigate = function (event) {
var url = $(this).attr('href').replace(courseApp.root, '');
// Handle the cases where the user wants to open the link in a new tab/window.
if (event.ctrlKey || event.shiftKey || event.metaKey || event.which == 2) {
return true;
}
// We'll take it from here...
event.preventDefault();
// Process the navigation in the app/router.
if (url === Backbone.history.getFragment() && url === '') {
// Note: We must call the index directly since Backbone does not support routing to the same route.
courseApp.index();
} else {
courseApp.navigate(url, {trigger: true});
}
};
/**
* Navigate to a new page within the Course App.
*
* This extends Backbone.View, allowing pages to navigate to
* any path within the app, without requiring a reference to the
* app instance.
*
* @param {String} fragment
*/
Backbone.View.prototype.goTo = function (fragment) {
courseApp.navigate(fragment, {trigger: true});
};
$(function () {
var $app = $('#app');
courseApp = new CourseRouter({$el: $app});
courseApp.start();
// Handle navbar clicks.
$('a.navbar-brand').on('click', navigate);
// Handle internal clicks
$app.on('click', 'a', navigate);
});
}
);
define([ define([
'underscore', 'underscore',
'collections/drf_pageable_collection', 'collections/drf_pageable_collection',
'models/course_model' 'models/course_model'
], function (_, DrfPageableCollection, CourseModel) { ],
function (_,
DrfPageableCollection,
CourseModel) {
'use strict';
return DrfPageableCollection.extend({ return DrfPageableCollection.extend({
model: CourseModel, model: CourseModel,
url: '/api/v2/courses/', url: '/api/v2/courses/'
}); });
}
}); );
...@@ -7,8 +7,13 @@ define([ ...@@ -7,8 +7,13 @@ define([
'use strict'; 'use strict';
return Backbone.PageableCollection.extend({ return Backbone.PageableCollection.extend({
queryParams: {
pageSize: 'page_size'
},
state: { state: {
pageSize: 20 // TODO Replace this collection with something that works properly with our API.
pageSize: 10000
}, },
parseRecords: function (resp, options) { parseRecords: function (resp, options) {
......
require([ require([
'jquery', 'jquery',
'backbone', 'backbone',
'bootstrap', 'bootstrap',
'bootstrap_accessibility', 'bootstrap_accessibility',
'underscore' 'underscore'
], function () { ],
}); function () {
$(function () {
// Activate all pre-rendered tooltips.
$('[data-toggle="tooltip"]').tooltip();
})
}
);
...@@ -3,6 +3,11 @@ require.config({ ...@@ -3,6 +3,11 @@ require.config({
paths: { paths: {
'backbone': 'bower_components/backbone/backbone', 'backbone': 'bower_components/backbone/backbone',
'backbone.paginator': 'bower_components/backbone.paginator/lib/backbone.paginator', 'backbone.paginator': 'bower_components/backbone.paginator/lib/backbone.paginator',
'backbone.relational': 'bower_components/backbone-relational/backbone-relational',
'backbone.route-filter': 'bower_components/backbone-route-filter/backbone-route-filter',
'backbone.stickit': 'bower_components/backbone.stickit/backbone.stickit',
'backbone.super': 'bower_components/backbone-super/backbone-super/backbone-super',
'backbone.validation': 'bower_components/backbone-validation/dist/backbone-validation-amd',
'bootstrap': 'bower_components/bootstrap-sass/assets/javascripts/bootstrap', 'bootstrap': 'bower_components/bootstrap-sass/assets/javascripts/bootstrap',
'bootstrap_accessibility': 'bower_components/bootstrapaccessibilityplugin/plugins/js/bootstrap-accessibility', 'bootstrap_accessibility': 'bower_components/bootstrapaccessibilityplugin/plugins/js/bootstrap-accessibility',
'collections': 'js/collections', 'collections': 'js/collections',
...@@ -12,7 +17,9 @@ require.config({ ...@@ -12,7 +17,9 @@ require.config({
'jquery-cookie': 'bower_components/jquery-cookie/jquery.cookie', 'jquery-cookie': 'bower_components/jquery-cookie/jquery.cookie',
'models': 'js/models', 'models': 'js/models',
'moment': 'bower_components/moment/moment', 'moment': 'bower_components/moment/moment',
'pages': 'js/pages',
'requirejs': 'bower_components/requirejs/require', 'requirejs': 'bower_components/requirejs/require',
'routers': 'js/routers',
'templates': 'templates', 'templates': 'templates',
'text': 'bower_components/text/text', 'text': 'bower_components/text/text',
'underscore': 'bower_components/underscore/underscore', 'underscore': 'bower_components/underscore/underscore',
......
define([ define([
'backbone', 'backbone',
'backbone.relational',
'backbone.validation',
'underscore', 'underscore',
'collections/product_collection', 'collections/product_collection',
'models/course_seat_model' 'models/course_seat_model'
], ],
function (Backbone, _, ProductCollection, CourseSeatModel) { function (Backbone,
BackboneRelational,
BackboneValidation,
_,
ProductCollection,
CourseSeatModel) {
'use strict'; 'use strict';
return Backbone.Model.extend({ Backbone.Validation.configure({
labelFormatter: 'label'
});
_.extend(Backbone.Model.prototype, Backbone.Validation.mixin);
_.extend(Backbone.Validation.patterns, {
courseId: /[^/+]+(\/|\+)[^/+]+(\/|\+)[^/]+/
});
_.extend(Backbone.Validation.messages, {
courseId: gettext('The course ID is invalid.')
});
return Backbone.RelationalModel.extend({
urlRoot: '/api/v2/courses/', urlRoot: '/api/v2/courses/',
defaults: { defaults: {
name: '' id: null,
name: null,
type: null,
verification_deadline: null
}, },
getProducts: function () { validation: {
if (_.isUndefined(this._products)) { id: {
this._products = new ProductCollection(); required: true,
this._products.url = this.get('products_url'); pattern: 'courseId'
return this._products.getFirstPage({fetch: true}); },
name: {
required: true
},
type: {
required: true,
msg: gettext('You must select a course type.')
},
verification_deadline: {
msg: gettext('Verification deadline is required for course types with verified modes.'),
required: function(value, attr, computedState) {
// TODO Return true if one of the products requires ID verification.
return false;
}
} }
},
return this._products; labels: {
id: gettext('Course ID'),
name: gettext('Course Name'),
type: gettext('Course Type'),
verification_deadline: gettext('Verification Deadline')
}, },
relations: [{
type: Backbone.HasMany,
key: 'products',
relatedModel: CourseSeatModel,
includeInJSON: false,
parse: true
}],
getSeats: function () { getSeats: function () {
// Returns the seat products // Returns the seat products
return this.getProducts().filter(function (product) {
// Filter out parent products since there is no need to display or modify. var seats = this.get('products').filter(function (product) {
return (product instanceof CourseSeatModel) && product.get('structure') !== 'parent'; // Filter out parent products since there is no need to display or modify.
}); return (product instanceof CourseSeatModel) && product.get('structure') !== 'parent';
}),
seatTypes = _.map(seats, function (seat) {
return seat.get('certificate_type');
});
return _.object(seatTypes, seats);
} }
}); });
} }
......
define([ define([
'backbone.super',
'models/product_model' 'models/product_model'
], ],
function (ProductModel) { function (BackboneSuper,
ProductModel) {
'use strict'; 'use strict';
return ProductModel.extend({ return ProductModel.extend({
defaults: {
certificate_type: null,
expires: null,
id_verification_required: null,
price: null,
product_class: 'Seat'
},
validation: {
certificate_type: {
required: true
},
price: {
required: true
},
product_class: {
oneOf: ['Seat']
}
},
getSeatType: function () { getSeatType: function () {
switch (this.get('certificate_type')) { switch (this.get('certificate_type')) {
case 'verified': case 'verified':
......
define([ define([
'backbone' 'backbone',
'backbone.relational',
'backbone.validation',
'moment',
'underscore'
], ],
function (Backbone) { function (Backbone,
BackboneRelational,
BackboneValidation,
moment,
_) {
'use strict'; 'use strict';
return Backbone.Model.extend({ _.extend(Backbone.Model.prototype, Backbone.Validation.mixin);
return Backbone.RelationalModel.extend({
urlRoot: '/api/v2/products/', urlRoot: '/api/v2/products/',
nestedAttributes: ['certificate_type', 'id_verification_required', 'course_key'],
parse: function (response) {
// Un-nest the attributes
_.each(response.attribute_values, function (data) {
this.nestedAttributes.push(data.name);
response[data.name] = data.value;
}, this);
delete response.attribute_values;
// The view displaying the expires value assumes times are in the user's local timezone. We want all
// times to be displayed in UTC to avoid confusion. Strip the timezone data to workaround the UI
// deficiencies. We will restore the UTC timezone in toJSON().
if (response.expires) {
response.expires = moment.utc(response.expires).format('YYYY-MM-DDTHH:mm:ss');
}
initialize: function () { return response;
// Expose the nested attribute values as top-level attributes on the model },
this.get('attribute_values').forEach(function (av) {
this.set(av.name, av.value); toJSON: function () {
var data = _.clone(this.attributes);
data.attribute_values = [];
// Re-nest the attributes
_.each(_.uniq(this.nestedAttributes), function (attribute) {
if (this.has(attribute)) {
data.attribute_values.push({
name: attribute,
value: this.get(attribute)
});
delete data[attribute];
}
}, this); }, this);
// Restore the timezone component, and output the ISO 8601 format expected by the server.
if (data.expires) {
data.expires = moment.utc(data.expires + 'Z').format();
}
return data;
} }
}); });
} }
......
define([
'models/course_model',
'views/course_create_edit_view',
'pages/page'
],
function (Course,
CourseCreateEditView,
Page) {
'use strict';
return Page.extend({
title: 'Create New Course',
initialize: function () {
this.model = new Course({});
this.view = new CourseCreateEditView({model: this.model});
this.render();
}
});
}
);
require([ define([
'views/course_detail_view' 'models/course_model',
'views/course_detail_view',
'pages/page'
], ],
function (CourseDetailView) { function (Course,
CourseDetailView,
Page) {
'use strict'; 'use strict';
new CourseDetailView(); return Page.extend({
title: function () {
return this.model.get('name') + ' - ' + gettext('View Course');
},
initialize: function (options) {
this.model = Course.findOrCreate({id: options.id});
this.view = new CourseDetailView({model: this.model});
this.listenTo(this.model, 'change sync', this.render);
this.model.fetch({data: {include_products: true}});
}
});
} }
); );
define([
'models/course_model',
'pages/page',
'views/course_create_edit_view'
],
function (Course,
Page,
CourseCreateEditView) {
'use strict';
return Page.extend({
title: function () {
return this.model.get('name') + ' - ' + gettext('Edit Course');
},
initialize: function (options) {
this.model = Course.findOrCreate({id: options.id});
this.view = new CourseCreateEditView({
editing: true,
model: this.model
});
this.listenTo(this.model, 'change sync', this.render);
this.model.fetch({
data: {include_products: true}
});
}
});
}
);
require([ define([
'collections/course_collection', 'collections/course_collection',
'views/course_list_view' 'views/course_list_view',
'pages/page'
], ],
function (CourseCollection, CourseListView) { function (CourseCollection,
CourseListView,
Page) {
'use strict';
return new CourseListView({ return Page.extend({
collection: new CourseCollection() title: 'Courses',
});
initialize: function () {
this.collection = new CourseCollection();
this.view = new CourseListView({collection: this.collection});
this.listenTo(this.collection, 'reset', this.render);
this.collection.fetch({reset: true});
}
});
} }
); );
define(['backbone',
'backbone.super'],
function (Backbone,
BackboneSuper) {
'use strict';
/***
* Base Page class.
*/
var Page = Backbone.View.extend({
/**
* Document title set during rendering.
*
* This can either be a string or a function that accepts this
* instance and returns a string.
*/
title: null,
/**
* Initializes this view and any models, collections, and/or nested views.
*
* Inheriting classes MUST override this method.
*/
initialize: function () {
},
/**
* Removes the nested view before removing this view.
*/
remove: function () {
if (this.view) {
this.view.remove();
this.view = null;
}
return this._super();
},
/**
* Updates the browser window's title.
*/
renderTitle: function () {
var title = _.result(this, 'title');
if (title) {
document.title = title;
}
},
/**
* Renders the nested view.
*/
renderNestedView: function () {
this.view.render();
this.$el.html(this.view.el);
},
/**
* Renders this Page, specifically the title and nested view.
* @returns {Page} current instance
*/
render: function () {
this.renderTitle();
this.renderNestedView();
return this;
}
});
return Page;
}
);
define([
'backbone',
'backbone.route-filter',
'backbone.super',
'pages/course_list_page',
'pages/course_detail_page',
'pages/course_create_page',
'pages/course_edit_page'
],
function (Backbone,
BackboneRouteFilter,
BackboneSuper,
CourseListPage,
CourseDetailPage,
CourseCreatePage,
CourseEditPage) {
'use strict';
return Backbone.Router.extend({
// Keeps track of the page/view currently on display
currentView: null,
// Base/root path of the app
root: '/courses/',
routes: {
'(/)': 'index',
'new(/)': 'new',
'*path': 'notFound'
},
// Filter(s) called before routes are executed. If the filters return a truthy value
// the route will be executed; otherwise, the route will not be executed.
before: {
'*any': 'clearView'
},
/**
* Setup special routes.
*
* @param {Object} options - Data used to initialize the router. This should include a key, $el, that
* refers to a jQuery Element where the pages will be rendered.
*/
initialize: function (options) {
var courseIdRegex = /([^/+]+(\/|\+)[^/+]+(\/|\+)[^/]+)/;
// This is where views will be rendered
this.$el = options.$el;
// Custom routes, requiring RegExp or other complex placeholders, should be defined here
this.route(new RegExp('^' + courseIdRegex.source + '(\/)?$'), 'show');
this.route(new RegExp('^' + courseIdRegex.source + '/edit(\/)?$'), 'edit');
},
/**
* Starts the router.
*/
start: function () {
Backbone.history.start({pushState: true, root: this.root});
return this;
},
/**
* Removes the current view.
*/
clearView: function () {
if (this.currentView) {
this.currentView.remove();
this.currentView = null;
}
return this;
},
/**
* 404 page
* @param {String} path - Invalid path.
*/
notFound: function (path) {
// TODO Render something!
alert(path + ' is invalid.');
},
/**
* Display a list of all courses in the system.
*/
index: function () {
var page = new CourseListPage();
this.currentView = page;
this.$el.html(page.el);
},
/**
* Display details for a single course.
* @param {String} id - ID of the course to display.
*/
show: function (id) {
var page = new CourseDetailPage({id: id});
this.currentView = page;
this.$el.html(page.el);
},
/**
* Display a form for creating a new course.
*/
new: function () {
var page = new CourseCreatePage();
this.currentView = page;
this.$el.html(page.el);
},
edit: function (id) {
var page = new CourseEditPage({id: id});
this.currentView = page;
$('#app').html(page.el);
}
});
}
);
...@@ -26,7 +26,7 @@ if (isBrowser) { ...@@ -26,7 +26,7 @@ if (isBrowser) {
// you can automatically get the test files using karma's configs // you can automatically get the test files using karma's configs
for (var file in window.__karma__.files) { for (var file in window.__karma__.files) {
if (/spec\.js$/.test(file)) { if (/js\/test\/specs\/.*spec\.js$/.test(file)) {
specs.push(file); specs.push(file);
} }
} }
......
define([
'jquery',
'underscore.string',
'views/course_detail_view',
'models/course_model'
],
function ($,
_s,
CourseDetailView,
Course) {
'use strict';
describe('course detail view', function () {
var view,
model,
data = {
id: 'edX/DemoX/Demo_Course',
url: 'http://ecommerce.local:8002/api/v2/courses/edX/DemoX/Demo_Course/',
name: 'edX Demonstration Course',
verification_deadline: null,
type: 'verified',
products_url: 'http://ecommerce.local:8002/api/v2/courses/edX/DemoX/Demo_Course/products/',
last_edited: '2015-07-27T00:27:23Z',
products: [
{
id: 9,
url: 'http://ecommerce.local:8002/api/v2/products/9/',
structure: 'child',
product_class: 'Seat',
title: 'Seat in edX Demonstration Course with honor certificate',
price: '0.00',
expires: null,
attribute_values: [
{
name: 'certificate_type',
value: 'honor'
},
{
name: 'course_key',
value: 'edX/DemoX/Demo_Course'
},
{
name: 'id_verification_required',
value: false
}
],
is_available_to_buy: true
},
{
id: 8,
url: 'http://ecommerce.local:8002/api/v2/products/8/',
structure: 'child',
product_class: 'Seat',
title: 'Seat in edX Demonstration Course with verified certificate (and ID verification)',
price: '15.00',
expires: null,
attribute_values: [
{
name: 'certificate_type',
value: 'verified'
},
{
name: 'course_key',
value: 'edX/DemoX/Demo_Course'
},
{
name: 'id_verification_required',
value: true
}
],
is_available_to_buy: true
},
{
id: 7,
url: 'http://ecommerce.local:8002/api/v2/products/7/',
structure: 'parent',
product_class: 'Seat',
title: 'Seat in edX Demonstration Course',
price: null,
expires: null,
attribute_values: [
{
name: 'course_key',
value: 'edX/DemoX/Demo_Course'
}
],
is_available_to_buy: false
}
]
};
beforeEach(function () {
model = Course.findOrCreate(data, {parse: true});
view = new CourseDetailView({model: model}).render();
});
it('should display course details', function () {
expect(view.$el.find('.course-name').text()).toEqual(model.get('name'));
expect(view.$el.find('.course-id').text()).toEqual(model.get('id'));
expect(view.$el.find('.course-type').text()).toEqual(_s.capitalize(model.get('type')));
expect(view.$el.find('.course-verification-deadline').length).toEqual(0);
});
it('should list the course seats', function () {
var $seats = view.$el.find('.course-seat'),
products = _.filter(data.products, function (product) {
return product.product_class === 'Seat' && product.structure === 'child';
});
expect($seats.length).toEqual(products.length);
// TODO Verify the rendered info matches the data
});
});
}
);
...@@ -2,74 +2,53 @@ define([ ...@@ -2,74 +2,53 @@ define([
'jquery', 'jquery',
'views/course_list_view', 'views/course_list_view',
'collections/course_collection' 'collections/course_collection'
], ],
function ($, CourseListView, CourseCollection) { function ($,
CourseListView,
describe('course list view', function () { CourseCollection) {
'use strict';
var view,
collection, describe('course list view', function () {
defaultCourses, var view,
renderInterval; collection,
courses = [
beforeEach(function (done) { {
id: 'edX/DemoX.1/2014',
defaultCourses = { name: 'DemoX',
"id": "edX/DemoX.1/2014", last_edited: '2015-06-16T19:14:34Z',
"name": "DemoX", type: 'honor'
"last_edited": "2015-06-16T19:14:34Z"
}, },
{ {
"id": "edX/victor101/Victor_s_Test_Course", id: 'edX/victor101/Victor_s_Test_Course',
"name": "Victor's Test Course", name: 'Victor\'s Test Course',
"last_edited": "2015-06-16T19:42:55Z" last_edited: '2015-06-16T19:42:55Z',
}; type: 'professional'
}
collection = new CourseCollection(); ];
spyOn(collection, 'fetch').and.callFake(function () { beforeEach(function () {
collection.set(defaultCourses); collection = new CourseCollection();
}); collection.set(courses);
// Set up the environment view = new CourseListView({collection: collection}).render();
setFixtures('<div id="course-list-view"></div>'); });
view = new CourseListView({
collection: collection
});
// Wait till the DOM is rendered before continuing
renderInterval = setInterval(function () {
if (view.$el.html()) {
clearInterval(renderInterval);
done();
}
}, 100);
});
it('should change the default filter placeholder to a custom string', function () {
expect(view.$el.find('#courseTable_filter input').attr('placeholder')).toBe('Filter by org or course ID');
});
it('should adjust the style of the filter textbox', function () {
var $tableInput = view.$el.find('#courseTable_filter input');
expect($tableInput.hasClass('field-input input-text')).toBeTruthy();
expect($tableInput.hasClass('form-control input-sm')).toBeFalsy();
});
it('should populate the table based on the course collection', function () {
var table = $('#courseTable').DataTable(); it('should change the default filter placeholder to a custom string', function () {
tableData = table.data(); expect(view.$el.find('#courseTable_filter input[type=search]').attr('placeholder')).toBe('Search...');
});
expect(tableData.data().length).toBe(collection.length); it('should adjust the style of the filter textbox', function () {
var $tableInput = view.$el.find('#courseTable_filter input');
}); expect($tableInput.hasClass('field-input input-text')).toBeTruthy();
expect($tableInput.hasClass('form-control input-sm')).toBeFalsy();
});
it('should populate the table based on the course collection', function () {
var tableData = view.$el.find('#courseTable').DataTable().data();
expect(tableData.data().length).toBe(collection.length);
}); });
});
} }
); );
define([
'jquery',
'backbone',
'underscore'
],
function ($, Backbone, _) {
'use strict';
return Backbone.View.extend({
className: 'alert',
template: _.template('<strong><%= title %></strong> <%= message %>'),
initialize: function (options) {
this.level = options.level || 'info';
this.title = options.title || '';
this.message = options.message || '';
},
render: function () {
var body = this.template({title: this.title, message: this.message});
this.$el.addClass('alert-' + this.level).attr('role', 'alert').html(body);
return this;
}
});
}
);
define([
'jquery',
'backbone',
'backbone.super',
'underscore',
'views/course_form_view',
'text!templates/course_create_edit.html'
],
function ($,
Backbone,
BackboneSuper,
_,
CourseFormView,
CourseCreateEditTemplate) {
'use strict';
return Backbone.View.extend({
template: _.template(CourseCreateEditTemplate),
className: 'course-create-edit-view',
initialize: function (options) {
// This indicates if we are editing or creating a course.
this.editing = options.editing;
},
remove: function(){
if(this.formView) {
this.formView.remove();
this.formView = null;
}
this._super();
},
render: function () {
var $html,
data = this.model.attributes;
// The form should be instantiated only once.
this.formView = this.formView || new CourseFormView({editing: this.editing, model: this.model});
// Render the basic page layout
data.editing = this.editing;
$html = $(this.template(data));
// Render the form
this.formView.render();
$html.find('.course-form-outer').html(this.formView.el);
// Render the complete view
this.$el.html($html);
// Activate the tooltips
this.$el.find('[data-toggle="tooltip"]').tooltip();
return this;
}
});
}
);
...@@ -4,7 +4,6 @@ define([ ...@@ -4,7 +4,6 @@ define([
'underscore', 'underscore',
'underscore.string', 'underscore.string',
'moment', 'moment',
'models/course_model',
'text!templates/course_detail.html', 'text!templates/course_detail.html',
'text!templates/_course_seat.html' 'text!templates/_course_seat.html'
], ],
...@@ -13,27 +12,15 @@ define([ ...@@ -13,27 +12,15 @@ define([
_, _,
_s, _s,
moment, moment,
CourseModel,
CourseDetailTemplate, CourseDetailTemplate,
CourseSeatTemplate) { CourseSeatTemplate) {
'use strict'; 'use strict';
return Backbone.View.extend({ return Backbone.View.extend({
el: '.course-detail-view', className: 'course-detail-view',
initialize: function () { initialize: function () {
var self = this, this.listenTo(this.model, 'change', this.render);
course_id = self.$el.data('course-id');
this.course = new CourseModel({id: course_id});
this.course.fetch({
success: function (course) {
self.render();
course.getProducts().done(function () {
self.renderSeats();
});
}
});
}, },
getSeats: function () { getSeats: function () {
...@@ -43,7 +30,8 @@ define([ ...@@ -43,7 +30,8 @@ define([
'honor', 'verified', 'no-id-professional', 'professional', 'credit' 'honor', 'verified', 'no-id-professional', 'professional', 'credit'
]))); ])));
seats = _.sortBy(this.course.getSeats(), function (seat) { seats = _.values(this.model.getSeats());
seats = _.sortBy(seats, function (seat) {
return sortObj[seat.get('certificate_type')] return sortObj[seat.get('certificate_type')]
}); });
...@@ -51,27 +39,33 @@ define([ ...@@ -51,27 +39,33 @@ define([
}, },
render: function () { render: function () {
var html, templateData; var html,
document.title = this.course.get('name') + ' - ' + gettext('View Course'); verifcationDeadline = this.model.get('verification_deadline'),
templateData;
templateData = { templateData = {
course: this.course.attributes, course: this.model.attributes,
courseType: _s.capitalize(this.course.get('type')) courseType: _s.capitalize(this.model.get('type')),
verificationDeadline: verifcationDeadline ? moment.utc(verifcationDeadline).format('lll z') : null
}; };
html = _.template(CourseDetailTemplate)(templateData); html = _.template(CourseDetailTemplate)(templateData);
this.$el.html(html) this.$el.html(html);
this.renderSeats();
return this;
}, },
renderSeats: function () { renderSeats: function () {
var html = '', var html = '',
$seatHolder = $('.course-seats', this.$el); $seatHolder = $('.course-seats', this.$el);
this.getSeats().forEach(function (seat) { _.each(this.getSeats(), function (seat) {
html += _.template(CourseSeatTemplate)({seat: seat, moment: moment}); html += _.template(CourseSeatTemplate)({seat: seat, moment: moment});
}); });
$seatHolder.append(html); $seatHolder.html(html);
} }
}); });
} }
......
define([ define([
'jquery', 'jquery',
'backbone',
'underscore', 'underscore',
'underscore.string', 'underscore.string',
'backbone',
'moment', 'moment',
'text!templates/course_list.html', 'text!templates/course_list.html',
'dataTablesBootstrap' 'dataTablesBootstrap'
], ],
function ($, _, _s, Backbone, moment, courseListViewTemplate) { function ($,
Backbone,
_,
_s,
moment,
courseListViewTemplate) {
'use strict'; 'use strict';
return Backbone.View.extend({ return Backbone.View.extend({
className: 'course-list-view',
el: '#course-list-view',
template: _.template(courseListViewTemplate), template: _.template(courseListViewTemplate),
initialize: function (options) { initialize: function () {
this.listenTo(this.collection, 'add remove change', this.render); this.listenTo(this.collection, 'add remove change', this.render);
this.collection.fetch();
}, },
renderCourseTable: function () { renderCourseTable: function () {
var tableData = [], var tableData = [],
filterPlaceholder = gettext('Filter by org or course ID'), filterPlaceholder = gettext('Search...'),
$emptyLabel = '<label class="sr">' + filterPlaceholder + '</label>'; $emptyLabel = '<label class="sr">' + filterPlaceholder + '</label>';
this.collection.each(function (value) { this.collection.each(function (value) {
tableData.push( tableData.push(
{ {
id: value.get('id'), id: value.get('id'),
type: value.get('type'),
name: value.get('name'), name: value.get('name'),
last_edited: moment(value.get('last_edited')).format('MMMM DD, YYYY, h:mm A') last_edited: moment(value.get('last_edited')).format('MMMM DD, YYYY, h:mm A')
} }
...@@ -43,26 +47,48 @@ define([ ...@@ -43,26 +47,48 @@ define([
this.$el.find('#courseTable').DataTable({ this.$el.find('#courseTable').DataTable({
autoWidth: false, autoWidth: false,
data: tableData, data: tableData,
info: false, info: true,
paging: false, paging: true,
oLanguage: { oLanguage: {
oPaginate: {
sNext: gettext('Next'),
sPrevious: gettext('Previous')
},
// Translators: _START_, _END_, and _TOTAL_ are placeholders. Do NOT translate them.
sInfo: gettext("Displaying _START_ to _END_ of _TOTAL_ courses"),
// Translators: _MAX_ is a placeholder. Do NOT translate it.
sInfoFiltered: gettext('(filtered from _MAX_ total courses)'),
// Translators: _MENU_ is a placeholder. Do NOT translate it.
sLengthMenu: gettext('Display _MENU_ courses'),
sSearch: '' sSearch: ''
}, },
order: [[0, 'asc']],
columns: [ columns: [
{ {
title: gettext('ID'), title: gettext('Course'),
data: 'id', data: 'name',
fnCreatedCell: function (nTd, sData, oData, iRow, iCol) { fnCreatedCell: function (nTd, sData, oData, iRow, iCol) {
$(nTd).html(_s.sprintf('<a href=\'/courses/%s\'>%s</a>', oData.id, oData.id)); $(nTd).html(_s.sprintf('<a href="/courses/%s/" class="course-name">%s</a><div class="course-id">%s</div>', oData.id, oData.name, oData.id));
} }
}, },
{ {
title: gettext('Name'), title: gettext('Course Type'),
data: 'name' data: 'type',
fnCreatedCell: function (nTd, sData, oData, iRow, iCol) {
$(nTd).html(_s.capitalize(oData.type));
}
}, },
{ {
title: gettext('Last Edited'), title: gettext('Last Edited'),
data: 'last_edited' data: 'last_edited'
},
{
data: 'id',
visible: false,
searchable: true
} }
] ]
}); });
......
define([
'jquery',
'backbone',
'backbone.validation',
'backbone.stickit',
'underscore',
'underscore.string'
],
function ($,
Backbone,
BackboneValidation,
BackboneStickit,
_,
_s) {
'use strict';
return Backbone.View.extend({
idVerificationRequired: false,
seatType: null,
template: null,
bindings: {
'input[name=certificate_type]': 'certificate_type',
'input[name=price]': {
observe: 'price',
setOptions: {
validate: true
}
},
'input[name=expires]': 'expires',
'input[name=id_verification_required]': {
observe: 'id_verification_required',
onSet: 'cleanIdVerificationRequired'
}
},
className: function () {
return 'row course-seat ' + this.seatType;
},
initialize: function () {
Backbone.Validation.bind(this, {
valid: function (view, attr, selector) {
var $el = view.$('[name=' + attr + ']'),
$group = $el.closest('.form-group');
$group.removeClass('has-error');
$group.find('.help-block:first').html('').addClass('hidden');
},
invalid: function (view, attr, error, selector) {
var $el = view.$('[name=' + attr + ']'),
$group = $el.closest('.form-group');
$group.addClass('has-error');
$group.find('.help-block:first').html(error).removeClass('hidden');
}
});
},
render: function () {
this.$el.html(this.template(this.model.attributes));
this.stickit();
return this;
},
cleanIdVerificationRequired: function(val){
return _s.toBoolean(val);
},
getFieldValue: function (name) {
return this.$(_s.sprintf('input[name=%s]', name)).val();
},
// TODO Validate the input: http://thedersen.com/projects/backbone-validation/.
/***
* Return the input data from the form fields.
*/
getData: function () {
var data = {},
fields = ['certificate_type', 'id_verification_required', 'price', 'expires'];
_.each(fields, function (field) {
data[field] = this.getFieldValue(field);
}, this);
return data;
},
updateModel: function () {
this.model.set(this.getData());
}
});
}
);
define([
'views/course_seat_form_fields/course_seat_form_field_view',
'text!templates/honor_course_seat_form_field.html'
],
function (CourseSeatFormFieldView,
FieldTemplate) {
'use strict';
return CourseSeatFormFieldView.extend({
certificateType: 'honor',
idVerificationRequired: false,
seatType: 'honor',
template: _.template(FieldTemplate)
});
}
);
define([
'underscore.string',
'views/course_seat_form_fields/verified_course_seat_form_field_view',
'text!templates/professional_course_seat_form_field.html'
],
function (_s,
VerifiedCourseSeatFormFieldView,
FieldTemplate) {
'use strict';
return VerifiedCourseSeatFormFieldView.extend({
certificateType: 'professional',
idVerificationRequired: false,
seatType: 'professional',
template: _.template(FieldTemplate),
getFieldValue: function (name) {
var value;
if (name === 'id_verification_required') {
value = this.$('input[name=id_verification_required]:checked').val();
value = _s.toBoolean(value);
} else {
value = this._super(name);
}
return value;
}
});
}
);
define([
'views/course_seat_form_fields/course_seat_form_field_view',
'text!templates/verified_course_seat_form_field.html'
],
function (CourseSeatFormFieldView,
FieldTemplate) {
'use strict';
return CourseSeatFormFieldView.extend({
certificateType: 'verified',
idVerificationRequired: true,
seatType: 'verified',
template: _.template(FieldTemplate)
});
}
);
...@@ -2,20 +2,34 @@ ...@@ -2,20 +2,34 @@
// -------------------- // --------------------
html { html {
font-size:16px; font-size: 16px;
} }
a { a {
&:hover, &:hover,
&:focus { &:focus {
text-decoration: none; text-decoration: none;
} }
} }
.container { .container {
background-color: white; background-color: $container-bg;
} }
.sr { .sr {
@extend .sr-only; @extend .sr-only;
}
.page-header {
.hd-1 {
margin: 0;
}
}
.breadcrumb {
> li {
+ li:before {
content: "#{$breadcrumb-separator} ";
}
}
} }
...@@ -24,8 +24,9 @@ ...@@ -24,8 +24,9 @@
// -------------------- // --------------------
@import '../components/buttons'; @import '../components/buttons';
@import '../components/navbar'; @import '../components/navbar';
@import '../components/footer';
// views // views
// -------------------- // --------------------
@import '../views/credit'; @import '../views/credit';
@import '../views/course_detail'; @import '../views/course_admin';
html,
body {
height: 100%;
/* The html and body elements cannot have any padding or margin. */
}
/* Set the fixed height of the footer here */
footer.footer {
height: $footer-height;
margin-top: $footer-margin;
border-top: $navbar-border-bottom-width solid palette(secondary, base);
padding-top: 8px;
background-color: $footer-bg;
.container{
background-color: transparent;
}
}
...@@ -2,16 +2,18 @@ ...@@ -2,16 +2,18 @@
// -------------------------------------------------- // --------------------------------------------------
.nav { .nav {
.nav-link { .nav-link {
&:hover, &:hover,
&:focus { &:focus {
outline:inherit; outline: inherit;
border-bottom-color:transparent; border-bottom-color: transparent;
}
} }
}
} }
.navbar { .navbar {
margin-bottom: 0;
// Remove default Bootstrap navbar border styling // Remove default Bootstrap navbar border styling
border: none; border: none;
border-radius: 0; border-radius: 0;
...@@ -20,8 +22,24 @@ ...@@ -20,8 +22,24 @@
border-bottom: $navbar-border-bottom-width solid palette(primary, accent); border-bottom: $navbar-border-bottom-width solid palette(primary, accent);
// Vertically center the logo // Vertically center the logo
.navbar-brand .navbar-brand-logo { .navbar-brand {
@include center-vertically; &:active,
&:focus,
&:hover {
border: none;
}
.navbar-brand-logo {
@include center-vertically;
}
.navbar-brand-app {
@include center-vertically;
top: 0;
display: inline-block;
color: palette(primary, accent);
font-weight: 600;
}
} }
} }
...@@ -65,13 +83,13 @@ ...@@ -65,13 +83,13 @@
} }
.dropdown-menu { .dropdown-menu {
.nav-link { .nav-link {
// Disables default link behavior of pattern library on menu items // Disables default link behavior of pattern library on menu items
transition:none; transition: none;
&:hover, &:hover,
&:focus { &:focus {
border-bottom-color:transparent; border-bottom-color: transparent;
}
} }
}
} }
...@@ -21,4 +21,5 @@ ...@@ -21,4 +21,5 @@
-webkit-transform: translateY(-50%); -webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%); -ms-transform: translateY(-50%);
transform: translateY(-50%); transform: translateY(-50%);
vertical-align: middle;
} }
...@@ -20,10 +20,19 @@ $border-radius-large: 3px; ...@@ -20,10 +20,19 @@ $border-radius-large: 3px;
$border-radius-small: 3px; $border-radius-small: 3px;
// typography // typography
$font-family-sans-serif: 'Open Sans','Helvetica Neue', Helvetica, Arial, sans-serif; $font-family-sans-serif: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
$font-family-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace; $font-family-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace;
// navbar // navbar
$navbar-default-bg: white; $navbar-default-bg: white;
$navbar-height: 74px; $navbar-height: 74px;
$navbar-border-bottom-width: 4px; $navbar-border-bottom-width: 4px;
// Footer
$footer-bg: $body-bg;
$footer-height: 37px;
$footer-margin: $footer-height;
// Miscellaneous
$container-bg: white;
$breadcrumb-bg: white;
#app {
padding-bottom: spacing-vertical(x-small);
.container {
padding-bottom: spacing-vertical(small);
}
#courseTable {
.course-name {
font-weight: bold;
}
}
.breadcrumb {
background-color: $breadcrumb-bg;
padding-left: 0;
margin-top: spacing-vertical(small);
}
.course-detail-view,
.course-create-edit-view {
.page-header {
margin-top: 0;
}
.course-information {
margin-bottom: spacing-vertical(small);
.info-item {
margin-bottom: 10px;
.heading {
font-weight: bold;
}
}
}
.course-seats {
.course-seat {
margin-bottom: spacing-vertical(mid-small);
.seat-type {
margin-bottom: 5px;
border-bottom: 1px solid $page-header-border-color;
padding-bottom: 3px;
font-weight: bold;
}
}
}
}
.course-create-edit-view {
.fields {
width: 50%;
}
}
}
.course-detail-view {
.course-information {
margin-bottom: spacing-vertical(small);
.heading {
font-weight: bold;
}
.course-id {
margin-bottom: spacing-vertical(x-small);
}
}
.course-seats {
.course-seat {
margin-bottom: spacing-vertical(small);
.seat-type {
font-weight: bold;
}
.seat-certificate-type {
}
}
}
}
<div class="row course-seat"> <div class="row course-seat">
<div class="col-md-4"> <div class="col-sm-12">
<div class="seat-type"><%= seat.getSeatType() %></div> <div class="seat-type"><%= seat.getSeatType() %></div>
<% if (seat.get('price')) { %>
<div class="seat-price"><%= gettext('Price:') + ' $' + seat.get('price') %></div>
<% } %>
</div> </div>
<div class="col-md-4 seat-certificate-type"> <div class="col-sm-4">
<i class="fa fa-check-square-o"></i> <%= seat.getCertificateDisplayName() %> <div class="seat-price"><%= gettext('Price:') + ' $' + Number(seat.get('price')).toLocaleString() %></div>
</div>
<div class="col-md-4 seat-additional-info">
<% var expires = seat.get('expires'); <% var expires = seat.get('expires');
if(expires) { if(expires) {
print(gettext('Verification Close:') + ' ' + moment(expires).format('LLL Z')); print(gettext('Upgrade Deadline:') + ' ' + moment.utc(expires).format('lll z'));
} }
%> %>
</div> </div>
<div class="col-sm-4 seat-certificate-type">
<%= seat.getCertificateDisplayName() %>
</div>
<div class="col-sm-4 seat-additional-info"></div>
</div> </div>
<div class="radio <% if(disabled) { %> disabled <% } %>" <% if(disabled) { %>aria-disabled="true" <% } %> >
<label>
<input type="radio" name="type" id="courseType<%= type %>" value="<%= type %>"
aria-describedby="courseType<%= type %>HelpBlock"
<% if(disabled) { print('disabled="disabled"'); } %>
<% if(checked) { print('checked'); } %>>
<%= displayName %>
<span id="courseType<%= type %>HelpBlock" class="help-block">
<%= helpText %>
</span>
</label>
</div>
<div class="container">
<ol class="breadcrumb">
<li><a href="/courses/"><%= gettext('Courses') %></a></li>
<% if(editing){ %>
<li><a href="/courses/<%= id %>/"><%= name %></a></li>
<li class="active"><%= gettext('Edit') %></li>
<% } else { %>
<li class="active"><%= gettext('Create') %></li>
<% } %>
</ol>
<div class="page-header">
<h1 class="hd-1 emphasized">
<%= gettext(editing ? 'Edit Course' : 'Create New Course') %>
</h1>
</div>
<div class="course-form-outer"></div>
</div>
<div class="container"> <div class="container">
<ol class="breadcrumb">
<li><a href="/courses/"><%= gettext('Courses') %></a></li>
<li class="active"><%= course['name'] %></li>
</ol>
<div class="page-header"> <div class="page-header">
<h1 class="hd-1 emphasized"> <h1 class="hd-1 emphasized">
<%= gettext('View Course') %> <span class="course-name"><%= course['name'] %></span>
<div class="pull-right"> <div class="pull-right">
<button class="btn btn-primary btn-small"><%= gettext('Edit Course') %></button> <a class="btn btn-primary btn-small" href="/courses/<%= course['id'] %>/edit/">
<%= gettext('Edit Course') %>
</a>
</div> </div>
</h1> </h1>
</div> </div>
<div class="course-information"> <div class="course-information">
<h3 class="hd-3"><%= gettext('Course Information') %></h3> <h3 class="hd-3 de-emphasized"><%= gettext('Course Information') %></h3>
<div class="heading course-name"><%= course['name'] %></div> <div class="info-item">
<div class="course-id"><%= course['id'] %></div> <div class="heading"><%= gettext('Course ID') %></div>
<div class="course-id"><%= course['id'] %></div>
</div>
<div class="info-item">
<div class="heading"><%= gettext('Course Type') %></div>
<div class="course-type"><%= courseType %></div>
</div>
<div class="heading"><%= gettext('Course Type') %></div> <% if(verificationDeadline) { %>
<div class="course-type"><%= courseType %></div> <div class="info-item">
<div class="heading"><%= gettext('Verification Deadline') %></div>
<div class="course-verification-deadline"><%= verificationDeadline %></div>
</div>
<% } %>
</div> </div>
<h3 class="hd-3"><%= gettext('Course Seats') %></h3> <h3 class="hd-3 de-emphasized"><%= gettext('Course Seats') %></h3>
<div class="course-seats"></div> <div class="course-seats"></div>
</div> </div>
<div class="alerts"></div>
<div class="fields">
<div class="form-group">
<label class="hd-4" for="id"><%= gettext('Course ID') %></label>
<% if(editing){ %>
<input type="hidden" name="id" value="<%= id %>">
<div class="course-id"><%= id %></div>
<% } else { %>
<input type="text" class="form-control" name="id" placeholder="e.g. edX/DemoX/Demo_Course">
<p class="help-block"></p>
<% } %>
</div>
<!-- TODO: Hide this field and get the data from an API. -->
<div class="form-group">
<label class="hd-4" for="name"><%= gettext('Course Name') %></label>
<input type="text" class="form-control" name="name" placeholder="e.g. edX Demonstration Course">
<p class="help-block"></p>
</div>
<div class="form-group">
<label class="hd-4" for="courseType"><%= gettext('Course Type') %></label>
<p class="help-block"></p>
<!-- Radio fields will be appended here. -->
<div class="course-types"></div>
</div>
<div class="form-group verification-deadline hidden">
<label class="hd-4" for="verification_deadline"><%= gettext('Verification Deadline') %></label>
<div class="input-group">
<div class="input-group-addon"><i class="fa fa-calendar" aria-hidden="true"></i></div>
<input type="datetime-local" id="verificationDeadline" name="verification_deadline" class="form-control"
aria-describedby="verificationDeadlineHelpBlock">
</div>
<!-- NOTE: This help-block is here for validation messages. -->
<span class="help-block"></span>
<span id="verificationDeadlineHelpBlock" class="help-block">
<%= gettext('After this date/time, students can no longer submit photos for ID verification.') %>
</span>
<span class="help-block">
<em>(<%= gettext('This is only required for course modes that require ID verification.') %>)</em>
</span>
</div>
</div>
<div class="form-group course-seats">
<label class="hd-4"><%= gettext('Course Seats') %></label>
<div class="course-seat empty"><em><%= gettext('Select a course type.') %></em></div>
<div class="editable-seats"></div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary"><%= gettext(editing ? 'Save Changes' : 'Create Course') %></button>
<a class="btn btn-default" href="/courses/<% if(id){ print(id + '/'); } %>"><%= gettext('Cancel') %></a>
</div>
<div class="page-header"> <div class="page-header">
<h1 class="hd-1 emphasized"> <h1 class="hd-1 emphasized">
<%- gettext('Courses') %> <%- gettext('Courses') %>
<button class="btn btn-primary btn-small"><%- gettext('Add New Course') %></button> <div class="pull-right">
<a href="/courses/new/" class="btn btn-primary btn-small"><%- gettext('Add New Course') %></a>
</div>
</h1> </h1>
</div> </div>
......
<div class="col-sm-12">
<div class="seat-type"><%= gettext('Free (Honor)') %></div>
</div>
<div class="col-sm-4">
<label class="price-label"><%= gettext('Price (in USD)') %>:</label> <span class="seat-price">$0.00</span>
<input type="hidden" name="price" value="0">
<input type="hidden" name="certificate_type" value="honor">
<input type="hidden" name="id_verification_required" value="false">
</div>
<div class="col-sm-4 seat-certificate-type">
<%= gettext('Honor Certificate') %>
</div>
<div class="col-sm-4 seat-additional-info"></div>
<div class="col-sm-12">
<div class="seat-type"><%= gettext('Professional Education') %></div>
</div>
<div class="col-sm-4">
<input type="hidden" name="certificate_type" value="professional">
<div class="seat-price">
<div class="form-group">
<label for="price"><%= gettext('Price (in USD)') %></label>
<div class="input-group">
<div class="input-group-addon">$</div>
<input type="number" class="form-control" name="price" id="price" min="5" step="1" pattern="\d+"
placeholder="100" value="<%= price %>">
</div>
</div>
</div>
<div class="form-group expires">
<label for="expires"><%= gettext('Upgrade Deadline') %></label>
<div class="input-group" data-toggle="tooltip" title="<%= gettext('Professional education courses have no upgrade deadline.') %>">
<div class="input-group-addon"><i class="fa fa-calendar" aria-hidden="true"></i></div>
<input type="datetime-local" id="expires" name="expires" class="form-control" value=""
aria-describedby="expiresHelpBlock" disabled="disabled">
</div>
<span id="expiresHelpBlock" class="help-block">
<%= gettext('After this date/time, students can no longer enroll in this track.') %>
</span>
</div>
</div>
<div class="col-sm-4 seat-certificate-type">
<%= gettext('Professional Education Certificate') %>
</div>
<div class="col-sm-4 seat-additional-info">
<div class="form-group">
<label for="id_verification_required"><%= gettext('ID Verification Required?') %></label>
<div class="input-group">
<label class="radio-inline">
<input type="radio" name="id_verification_required" value="true"> Yes
</label>
<label class="radio-inline">
<input type="radio" name="id_verification_required" value="false"> No
</label>
</div>
</div>
</div>
<div class="col-sm-12">
<div class="seat-type"><%= gettext('Verified') %></div>
</div>
<div class="col-sm-4">
<input type="hidden" name="certificate_type" value="verified">
<input type="hidden" name="id_verification_required" value="true">
<div class="seat-price">
<div class="form-group">
<label for="price"><%= gettext('Price (in USD)') %></label>
<div class="input-group">
<div class="input-group-addon">$</div>
<input type="number" class="form-control" name="price" id="price" min="5" step="1" pattern="\d+"
placeholder="100" value="<%= price %>">
</div>
</div>
</div>
<div class="form-group expires">
<label for="expires"><%= gettext('Upgrade Deadline') %></label>
<div class="input-group">
<div class="input-group-addon"><i class="fa fa-calendar" aria-hidden="true"></i></div>
<input type="datetime-local" id="expires" name="expires" class="form-control" value=""
aria-describedby="expiresHelpBlock">
</div>
<span id="expiresHelpBlock" class="help-block">
<%= gettext('After this date/time, students can no longer enroll in this track.') %>
</span>
</div>
</div>
<div class="col-sm-4 seat-certificate-type">
<%= gettext('Verified Certificate') %>
</div>
<div class="col-sm-4 seat-additional-info"></div>
{% extends 'edx/base.html' %}
{% load core_extras %}
{% load i18n %}
{% load staticfiles %}
{% block title %}{% trans "Courses" %}{% endblock %}
{% block navbar %}
<nav class="navbar navbar-default" aria-label="Account">
<div class="container">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#main-navbar-collapse" aria-expanded="false">
<span class="sr-only">{% trans "Toggle navigation" %}</span>
<span aria-hidden="true" class="icon-bar"></span>
<span aria-hidden="true" class="icon-bar"></span>
<span aria-hidden="true" class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/courses/">
<div class="navbar-brand-logo" alt="{% settings_value 'PLATFORM_NAME' %}"></div>
<div class="navbar-brand-app">{% trans "Course Administration" %}</div>
</a>
</div>
<div class="collapse navbar-collapse" id="main-navbar-collapse">
<ul class="nav navbar-nav navbar-right">
{% if user.is_authenticated %}
<li class="btn-group user-menu">
<button type="button" class="btn btn-default hidden-xs main-btn nav-button"
onclick="window.open('{% settings_value 'LMS_DASHBOARD_URL' %}');">
<i class="icon fa fa-home" aria-hidden="true"></i>
<span class="sr-only">{% trans "Dashboard for:" %}</span>
{{ user.username }}
</button>
<button type="button" class="btn btn-default dropdown-toggle hidden-xs nav-button"
data-toggle="dropdown"
aria-haspopup="true">
<span class="caret"></span>
<span class="sr-only">{% trans "Toggle Dropdown" %}</span>
</button>
<ul class="dropdown-menu" aria-expanded="false">
{% include "courses/menu_options.html" %}
</ul>
{% include "courses/menu_options.html" with additional_class="visible-xs" %}
</li>
{% else %}
<a class="btn btn-primary navbar-btn hidden-xs" href="{% url 'login' %}">{% trans "Login" %}</a>
<li class="visible-xs"><a class="nav-link" href="{% url 'login' %}">{% trans "Login" %}</a></li>
</a>
{% endif %}
</ul>
</div>
</div>
</nav>
{% endblock navbar %}
{% block content %}
<div id="app" class="container"></div>
{% endblock %}
{% block footer %}
<footer class="footer">
<div class="container">
<div class="row">
<div class="col-xs-12 text-right">
<em>{% blocktrans %}{{ platform_name }} Course Administration Tool{% endblocktrans %}</em>
</div>
</div>
</div>
</footer>
{% endblock footer %}
{% block javascript %}
<script src="{% static 'js/apps/course_admin_app.js' %}"></script>
{% endblock %}
{% extends 'edx/base.html' %}
{% load staticfiles %}
{% block content %}
<div class="course-detail-view" data-course-id="{{ course_id }}"></div>
{% endblock %}
{% block javascript %}
<script src="{% static 'js/pages/course_detail_page.js' %}"></script>
{% endblock %}
{% extends 'edx/base.html' %}
{% load staticfiles %}
{% load i18n %}
{% block title %}{% trans "Courses" %}{% endblock %}
{% block content %}
<div class="container" id="course-list-view"></div>
{% endblock %}
{% block javascript %}
<script src="{% static 'js/pages/course_list_page.js' %}"></script>
{% endblock %}
{% load core_extras %} {% load core_extras %}
{% load i18n %} {% load i18n %}
<li class="{{ additional_class }}"><a class="nav-link" href="{% settings_value 'LMS_DASHBOARD_URL' %}">{% trans "Student Dashboard" %}</a></li> <li class="{{ additional_class }}">
<li class="{{ additional_class }}"><a class="nav-link" href="{% url 'courses:list' %}">{% trans "Course Admin Tool" %}</a></li> <a class="nav-link" href="{% settings_value 'LMS_DASHBOARD_URL' %}">{% trans "Student Dashboard" %}</a>
<li class="{{ additional_class }}"><a class="nav-link" href="{% url 'dashboard:index' %}">{% trans "E-Commerce Dashboard" %}</a></li> </li>
<li class="{{ additional_class }}">
<a class="nav-link" href="{% url 'courses:app' '' %}">{% trans "Course Admin Tool" %}</a>
</li>
<li class="{{ additional_class }}"
><a class="nav-link" href="{% url 'dashboard:index' %}">{% trans "E-Commerce Dashboard" %}</a>
</li>
<li class="divider {{ additional_class }}"></li> <li class="divider {{ additional_class }}"></li>
<li class="{{ additional_class }}"><a class="nav-link" href="{% url 'logout' %}">{% trans "Sign Out" %}</a></li> <li class="{{ additional_class }}"><a class="nav-link" href="{% url 'logout' %}">{% trans "Sign Out" %}</a></li>
...@@ -79,6 +79,9 @@ ...@@ -79,6 +79,9 @@
{% block content %} {% block content %}
{% endblock content %} {% endblock content %}
{% block footer %}
{% endblock footer %}
{# Translation support for JavaScript strings. #} {# Translation support for JavaScript strings. #}
<script type="text/javascript" src="{% url 'django.views.i18n.javascript_catalog' %}"></script> <script type="text/javascript" src="{% url 'django.views.i18n.javascript_catalog' %}"></script>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment