Commit e0c1f170 by Clinton Blackburn Committed by Clinton Blackburn

Initial course app creation and list view migration

- Created Backbone app/router
- Updated list view to operate via new router

XCOM-309, XCOM-337
parent eec2bfbc
...@@ -23,6 +23,8 @@ ...@@ -23,6 +23,8 @@
"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"
} }
} }
...@@ -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. """
......
...@@ -3,10 +3,11 @@ from django.conf.urls import patterns, url ...@@ -3,10 +3,11 @@ from django.conf.urls import patterns, url
from ecommerce.core.constants import COURSE_ID_PATTERN 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'), 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,23 +6,24 @@ from django.contrib.auth.decorators import login_required ...@@ -6,23 +6,24 @@ 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 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 CourseAppView(StaffOnlyMixin, TemplateView):
template_name = 'courses/course_app.html'
class CourseDetailView(TemplateView): class CourseDetailView(TemplateView):
......
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});
}
};
$(function () {
var $app = $('#app');
// Let's start the show!
courseApp = new CourseRouter({$el: $app});
courseApp.start();
// Handle navbar clicks.
$('a.navbar-brand').on('click', 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 () {
}
);
...@@ -3,6 +3,8 @@ require.config({ ...@@ -3,6 +3,8 @@ 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.route-filter': 'bower_components/backbone-route-filter/backbone-route-filter',
'backbone.super': 'bower_components/backbone-super/backbone-super/backbone-super',
'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 +14,9 @@ require.config({ ...@@ -12,7 +14,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',
......
...@@ -4,7 +4,10 @@ define([ ...@@ -4,7 +4,10 @@ define([
'collections/product_collection', 'collections/product_collection',
'models/course_seat_model' 'models/course_seat_model'
], ],
function (Backbone, _, ProductCollection, CourseSeatModel) { function (Backbone,
_,
ProductCollection,
CourseSeatModel) {
'use strict'; 'use strict';
return Backbone.Model.extend({ return Backbone.Model.extend({
......
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.super',
'pages/course_list_page'
],
function (Backbone,
BackboneSuper,
CourseListPage) {
'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',
'*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) {
// This is where views will be rendered
this.$el = options.$el;
},
/**
* 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);
}
});
}
);
...@@ -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);
} }
} }
......
...@@ -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([ 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
} }
] ]
}); });
......
...@@ -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,10 @@ ...@@ -24,8 +24,10 @@
// -------------------- // --------------------
@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_admin';
@import '../views/course_detail'; @import '../views/course_detail';
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,18 @@ $border-radius-large: 3px; ...@@ -20,10 +20,18 @@ $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;
#app {
padding-bottom: spacing-vertical(x-small);
.container {
padding-bottom: spacing-vertical(small);
}
#courseTable {
.course-name {
font-weight: bold;
}
}
}
<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">
<button class="btn btn-primary btn-small"><%- gettext('Add New Course') %></button>
</div>
</h1> </h1>
</div> </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 %}
{% 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