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 @@
"text": "~2.0.14",
"backbone.paginator": "~2.0.2",
"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 @@
name: 'js/config'
},
{
name: 'js/pages/course_list_page',
name: 'js/apps/course_admin_app',
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):
self.assertEqual(response.status_code, 200)
class CourseListViewTests(UserMixin, TestCase):
path = reverse('courses:list')
class CourseAppViewTests(UserMixin, TestCase):
path = reverse('courses:app', args=[''])
def test_login_required(self):
""" Users are required to login before accessing the view. """
......
from django.conf.urls import patterns, url
from ecommerce.core.constants import COURSE_ID_PATTERN
from ecommerce.courses import views
urlpatterns = patterns(
'',
url(r'^$', views.CourseListView.as_view(), name='list'),
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
from django.core.management import call_command
from django.http import Http404, HttpResponse
from django.utils.decorators import method_decorator
from django.views.generic import View, ListView, TemplateView
from ecommerce.courses.models import Course
from django.views.generic import View, TemplateView
logger = logging.getLogger(__name__)
class CourseListView(ListView):
model = Course
context_object_name = 'courses'
class StaffOnlyMixin(object):
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise Http404
return super(CourseListView, self).dispatch(request, *args, **kwargs)
return super(StaffOnlyMixin, self).dispatch(request, *args, **kwargs)
class CourseDetailView(TemplateView):
template_name = 'courses/course_detail.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 CourseAppView(StaffOnlyMixin, TemplateView):
template_name = 'courses/course_app.html'
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 = (
'oscar.apps.checkout.context_processors.checkout',
'oscar.apps.customer.notifications.context_processors.notifications',
'oscar.core.context_processors.metadata',
'ecommerce.core.context_processors.core',
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders
......@@ -398,7 +399,7 @@ REST_FRAMEWORK = {
'ecommerce.extensions.api.authentication.BearerAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'DEFAULT_PAGINATION_CLASS': 'ecommerce.extensions.api.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
'DEFAULT_THROTTLE_CLASSES': (
'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([
'underscore',
'collections/drf_pageable_collection',
'models/course_model'
], function (_, DrfPageableCollection, CourseModel) {
'underscore',
'collections/drf_pageable_collection',
'models/course_model'
],
function (_,
DrfPageableCollection,
CourseModel) {
'use strict';
return DrfPageableCollection.extend({
model: CourseModel,
url: '/api/v2/courses/',
});
});
return DrfPageableCollection.extend({
model: CourseModel,
url: '/api/v2/courses/'
});
}
);
......@@ -7,8 +7,13 @@ define([
'use strict';
return Backbone.PageableCollection.extend({
queryParams: {
pageSize: 'page_size'
},
state: {
pageSize: 20
// TODO Replace this collection with something that works properly with our API.
pageSize: 10000
},
parseRecords: function (resp, options) {
......
require([
'jquery',
'backbone',
'bootstrap',
'bootstrap_accessibility',
'underscore'
], function () {
});
'jquery',
'backbone',
'bootstrap',
'bootstrap_accessibility',
'underscore'
],
function () {
$(function () {
// Activate all pre-rendered tooltips.
$('[data-toggle="tooltip"]').tooltip();
})
}
);
......@@ -3,6 +3,11 @@ require.config({
paths: {
'backbone': 'bower_components/backbone/backbone',
'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_accessibility': 'bower_components/bootstrapaccessibilityplugin/plugins/js/bootstrap-accessibility',
'collections': 'js/collections',
......@@ -12,7 +17,9 @@ require.config({
'jquery-cookie': 'bower_components/jquery-cookie/jquery.cookie',
'models': 'js/models',
'moment': 'bower_components/moment/moment',
'pages': 'js/pages',
'requirejs': 'bower_components/requirejs/require',
'routers': 'js/routers',
'templates': 'templates',
'text': 'bower_components/text/text',
'underscore': 'bower_components/underscore/underscore',
......
define([
'backbone',
'backbone.relational',
'backbone.validation',
'underscore',
'collections/product_collection',
'models/course_seat_model'
],
function (Backbone, _, ProductCollection, CourseSeatModel) {
function (Backbone,
BackboneRelational,
BackboneValidation,
_,
ProductCollection,
CourseSeatModel) {
'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/',
defaults: {
name: ''
id: null,
name: null,
type: null,
verification_deadline: null
},
getProducts: function () {
if (_.isUndefined(this._products)) {
this._products = new ProductCollection();
this._products.url = this.get('products_url');
return this._products.getFirstPage({fetch: true});
validation: {
id: {
required: true,
pattern: 'courseId'
},
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 () {
// Returns the seat products
return this.getProducts().filter(function (product) {
// Filter out parent products since there is no need to display or modify.
return (product instanceof CourseSeatModel) && product.get('structure') !== 'parent';
});
var seats = this.get('products').filter(function (product) {
// 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([
'backbone.super',
'models/product_model'
],
function (ProductModel) {
function (BackboneSuper,
ProductModel) {
'use strict';
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 () {
switch (this.get('certificate_type')) {
case 'verified':
......
define([
'backbone'
'backbone',
'backbone.relational',
'backbone.validation',
'moment',
'underscore'
],
function (Backbone) {
function (Backbone,
BackboneRelational,
BackboneValidation,
moment,
_) {
'use strict';
return Backbone.Model.extend({
_.extend(Backbone.Model.prototype, Backbone.Validation.mixin);
return Backbone.RelationalModel.extend({
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 () {
// 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);
return response;
},
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);
// 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([
'views/course_detail_view'
define([
'models/course_model',
'views/course_detail_view',
'pages/page'
],
function (CourseDetailView) {
function (Course,
CourseDetailView,
Page) {
'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',
'views/course_list_view'
'views/course_list_view',
'pages/page'
],
function (CourseCollection, CourseListView) {
function (CourseCollection,
CourseListView,
Page) {
'use strict';
return new CourseListView({
collection: new CourseCollection()
});
return Page.extend({
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) {
// you can automatically get the test files using karma's configs
for (var file in window.__karma__.files) {
if (/spec\.js$/.test(file)) {
if (/js\/test\/specs\/.*spec\.js$/.test(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([
'jquery',
'views/course_list_view',
'collections/course_collection'
],
function ($, CourseListView, CourseCollection) {
describe('course list view', function () {
var view,
collection,
defaultCourses,
renderInterval;
beforeEach(function (done) {
defaultCourses = {
"id": "edX/DemoX.1/2014",
"name": "DemoX",
"last_edited": "2015-06-16T19:14:34Z"
],
function ($,
CourseListView,
CourseCollection) {
'use strict';
describe('course list view', function () {
var view,
collection,
courses = [
{
id: 'edX/DemoX.1/2014',
name: 'DemoX',
last_edited: '2015-06-16T19:14:34Z',
type: 'honor'
},
{
"id": "edX/victor101/Victor_s_Test_Course",
"name": "Victor's Test Course",
"last_edited": "2015-06-16T19:42:55Z"
};
collection = new CourseCollection();
spyOn(collection, 'fetch').and.callFake(function () {
collection.set(defaultCourses);
});
// Set up the environment
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 () {
id: 'edX/victor101/Victor_s_Test_Course',
name: 'Victor\'s Test Course',
last_edited: '2015-06-16T19:42:55Z',
type: 'professional'
}
];
beforeEach(function () {
collection = new CourseCollection();
collection.set(courses);
view = new CourseListView({collection: collection}).render();
});
var table = $('#courseTable').DataTable();
tableData = table.data();
it('should change the default filter placeholder to a custom string', function () {
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([
'underscore',
'underscore.string',
'moment',
'models/course_model',
'text!templates/course_detail.html',
'text!templates/_course_seat.html'
],
......@@ -13,27 +12,15 @@ define([
_,
_s,
moment,
CourseModel,
CourseDetailTemplate,
CourseSeatTemplate) {
'use strict';
return Backbone.View.extend({
el: '.course-detail-view',
className: 'course-detail-view',
initialize: function () {
var self = this,
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();
});
}
});
this.listenTo(this.model, 'change', this.render);
},
getSeats: function () {
......@@ -43,7 +30,8 @@ define([
'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')]
});
......@@ -51,27 +39,33 @@ define([
},
render: function () {
var html, templateData;
document.title = this.course.get('name') + ' - ' + gettext('View Course');
var html,
verifcationDeadline = this.model.get('verification_deadline'),
templateData;
templateData = {
course: this.course.attributes,
courseType: _s.capitalize(this.course.get('type'))
course: this.model.attributes,
courseType: _s.capitalize(this.model.get('type')),
verificationDeadline: verifcationDeadline ? moment.utc(verifcationDeadline).format('lll z') : null
};
html = _.template(CourseDetailTemplate)(templateData);
this.$el.html(html)
this.$el.html(html);
this.renderSeats();
return this;
},
renderSeats: function () {
var html = '',
$seatHolder = $('.course-seats', this.$el);
this.getSeats().forEach(function (seat) {
_.each(this.getSeats(), function (seat) {
html += _.template(CourseSeatTemplate)({seat: seat, moment: moment});
});
$seatHolder.append(html);
$seatHolder.html(html);
}
});
}
......
define([
'jquery',
'backbone',
'underscore',
'underscore.string',
'backbone',
'moment',
'text!templates/course_list.html',
'dataTablesBootstrap'
],
function ($, _, _s, Backbone, moment, courseListViewTemplate) {
],
function ($,
Backbone,
_,
_s,
moment,
courseListViewTemplate) {
'use strict';
return Backbone.View.extend({
el: '#course-list-view',
className: 'course-list-view',
template: _.template(courseListViewTemplate),
initialize: function (options) {
initialize: function () {
this.listenTo(this.collection, 'add remove change', this.render);
this.collection.fetch();
},
renderCourseTable: function () {
var tableData = [],
filterPlaceholder = gettext('Filter by org or course ID'),
filterPlaceholder = gettext('Search...'),
$emptyLabel = '<label class="sr">' + filterPlaceholder + '</label>';
this.collection.each(function (value) {
tableData.push(
{
id: value.get('id'),
type: value.get('type'),
name: value.get('name'),
last_edited: moment(value.get('last_edited')).format('MMMM DD, YYYY, h:mm A')
}
......@@ -43,26 +47,48 @@ define([
this.$el.find('#courseTable').DataTable({
autoWidth: false,
data: tableData,
info: false,
paging: false,
info: true,
paging: true,
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: ''
},
order: [[0, 'asc']],
columns: [
{
title: gettext('ID'),
data: 'id',
title: gettext('Course'),
data: 'name',
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'),
data: 'name'
title: gettext('Course Type'),
data: 'type',
fnCreatedCell: function (nTd, sData, oData, iRow, iCol) {
$(nTd).html(_s.capitalize(oData.type));
}
},
{
title: gettext('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 @@
// --------------------
html {
font-size:16px;
font-size: 16px;
}
a {
&:hover,
&:focus {
text-decoration: none;
}
&:hover,
&:focus {
text-decoration: none;
}
}
.container {
background-color: white;
background-color: $container-bg;
}
.sr {
@extend .sr-only;
@extend .sr-only;
}
.page-header {
.hd-1 {
margin: 0;
}
}
.breadcrumb {
> li {
+ li:before {
content: "#{$breadcrumb-separator} ";
}
}
}
......@@ -24,8 +24,9 @@
// --------------------
@import '../components/buttons';
@import '../components/navbar';
@import '../components/footer';
// views
// --------------------
@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 @@
// --------------------------------------------------
.nav {
.nav-link {
&:hover,
&:focus {
outline:inherit;
border-bottom-color:transparent;
}
.nav-link {
&:hover,
&:focus {
outline: inherit;
border-bottom-color: transparent;
}
}
}
.navbar {
margin-bottom: 0;
// Remove default Bootstrap navbar border styling
border: none;
border-radius: 0;
......@@ -20,8 +22,24 @@
border-bottom: $navbar-border-bottom-width solid palette(primary, accent);
// Vertically center the logo
.navbar-brand .navbar-brand-logo {
@include center-vertically;
.navbar-brand {
&: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 @@
}
.dropdown-menu {
.nav-link {
// Disables default link behavior of pattern library on menu items
transition:none;
&:hover,
&:focus {
border-bottom-color:transparent;
}
.nav-link {
// Disables default link behavior of pattern library on menu items
transition: none;
&:hover,
&:focus {
border-bottom-color: transparent;
}
}
}
......@@ -21,4 +21,5 @@
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
vertical-align: middle;
}
......@@ -20,10 +20,19 @@ $border-radius-large: 3px;
$border-radius-small: 3px;
// 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;
// navbar
$navbar-default-bg: white;
$navbar-height: 74px;
$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="col-md-4">
<div class="col-sm-12">
<div class="seat-type"><%= seat.getSeatType() %></div>
<% if (seat.get('price')) { %>
<div class="seat-price"><%= gettext('Price:') + ' $' + seat.get('price') %></div>
<% } %>
</div>
<div class="col-md-4 seat-certificate-type">
<i class="fa fa-check-square-o"></i> <%= seat.getCertificateDisplayName() %>
</div>
<div class="col-md-4 seat-additional-info">
<div class="col-sm-4">
<div class="seat-price"><%= gettext('Price:') + ' $' + Number(seat.get('price')).toLocaleString() %></div>
<% var expires = seat.get('expires');
if(expires) {
print(gettext('Verification Close:') + ' ' + moment(expires).format('LLL Z'));
print(gettext('Upgrade Deadline:') + ' ' + moment.utc(expires).format('lll z'));
}
%>
</div>
<div class="col-sm-4 seat-certificate-type">
<%= seat.getCertificateDisplayName() %>
</div>
<div class="col-sm-4 seat-additional-info"></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">
<ol class="breadcrumb">
<li><a href="/courses/"><%= gettext('Courses') %></a></li>
<li class="active"><%= course['name'] %></li>
</ol>
<div class="page-header">
<h1 class="hd-1 emphasized">
<%= gettext('View Course') %>
<span class="course-name"><%= course['name'] %></span>
<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>
</h1>
</div>
<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="course-id"><%= course['id'] %></div>
<div class="info-item">
<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>
<div class="course-type"><%= courseType %></div>
<% if(verificationDeadline) { %>
<div class="info-item">
<div class="heading"><%= gettext('Verification Deadline') %></div>
<div class="course-verification-deadline"><%= verificationDeadline %></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>
<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">
<h1 class="hd-1 emphasized">
<%- gettext('Courses') %>
<button class="btn btn-primary btn-small"><%- gettext('Add New Course') %></button>
<%- gettext('Courses') %>
<div class="pull-right">
<a href="/courses/new/" class="btn btn-primary btn-small"><%- gettext('Add New Course') %></a>
</div>
</h1>
</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 i18n %}
<li class="{{ additional_class }}"><a class="nav-link" href="{% settings_value 'LMS_DASHBOARD_URL' %}">{% trans "Student Dashboard" %}</a></li>
<li class="{{ additional_class }}"><a class="nav-link" href="{% url 'courses:list' %}">{% 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="{{ additional_class }}">
<a class="nav-link" href="{% settings_value 'LMS_DASHBOARD_URL' %}">{% trans "Student Dashboard" %}</a>
</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="{{ additional_class }}"><a class="nav-link" href="{% url 'logout' %}">{% trans "Sign Out" %}</a></li>
......@@ -79,6 +79,9 @@
{% block content %}
{% endblock content %}
{% block footer %}
{% endblock footer %}
{# Translation support for JavaScript strings. #}
<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