Commit 53a8b55b by Clinton Blackburn

Added course create view

XCOM-520
parent 68e66025
...@@ -26,6 +26,8 @@ ...@@ -26,6 +26,8 @@
"underscore.string": "~3.1.1", "underscore.string": "~3.1.1",
"backbone-super": "~1.0.4", "backbone-super": "~1.0.4",
"backbone-route-filter": "~0.1.2", "backbone-route-filter": "~0.1.2",
"backbone-relational": "~0.9.0" "backbone-relational": "~0.9.0",
"backbone-validation": "~0.11.5",
"backbone.stickit": "~0.9.2"
} }
} }
...@@ -38,6 +38,20 @@ require([ ...@@ -38,6 +38,20 @@ require([
} }
}; };
/**
* 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 () { $(function () {
var $app = $('#app'); var $app = $('#app');
......
...@@ -6,5 +6,9 @@ require([ ...@@ -6,5 +6,9 @@ require([
'underscore' 'underscore'
], ],
function () { function () {
$(function () {
// Activate all pre-rendered tooltips.
$('[data-toggle="tooltip"]').tooltip();
})
} }
); );
...@@ -5,7 +5,9 @@ require.config({ ...@@ -5,7 +5,9 @@ require.config({
'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.relational': 'bower_components/backbone-relational/backbone-relational',
'backbone.route-filter': 'bower_components/backbone-route-filter/backbone-route-filter', '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.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',
......
define([ define([
'backbone', 'backbone',
'backbone.relational', 'backbone.relational',
'backbone.validation',
'underscore', 'underscore',
'collections/product_collection', 'collections/product_collection',
'models/course_seat_model' 'models/course_seat_model'
], ],
function (Backbone, function (Backbone,
BackboneRelational, BackboneRelational,
BackboneValidation,
_, _,
ProductCollection, ProductCollection,
CourseSeatModel) { CourseSeatModel) {
'use strict'; 'use strict';
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({ return Backbone.RelationalModel.extend({
urlRoot: '/api/v2/courses/', urlRoot: '/api/v2/courses/',
defaults: { defaults: {
id: null, id: null,
name: null, name: null,
type: null type: null,
verification_deadline: null
},
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;
}
}
},
labels: {
id: gettext('Course ID'),
name: gettext('Course Name'),
type: gettext('Course Type'),
verification_deadline: gettext('Verification Deadline')
}, },
relations: [{ relations: [{
......
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({
...@@ -13,6 +15,18 @@ define([ ...@@ -13,6 +15,18 @@ define([
product_class: 'Seat' 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.relational',
'backbone.validation',
'moment', 'moment',
'underscore' 'underscore'
], ],
function (Backbone, function (Backbone,
BackboneRelational, BackboneRelational,
BackboneValidation,
moment, moment,
_) { _) {
'use strict'; 'use strict';
_.extend(Backbone.Model.prototype, Backbone.Validation.mixin);
return Backbone.RelationalModel.extend({ return Backbone.RelationalModel.extend({
urlRoot: '/api/v2/products/', urlRoot: '/api/v2/products/',
nestedAttributes: ['certificate_type', 'id_verification_required', 'course_key'], nestedAttributes: ['certificate_type', 'id_verification_required', 'course_key'],
......
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();
}
});
}
);
...@@ -3,13 +3,15 @@ define([ ...@@ -3,13 +3,15 @@ define([
'backbone.route-filter', 'backbone.route-filter',
'backbone.super', 'backbone.super',
'pages/course_list_page', 'pages/course_list_page',
'pages/course_detail_page' 'pages/course_detail_page',
'pages/course_create_page'
], ],
function (Backbone, function (Backbone,
BackboneRouteFilter, BackboneRouteFilter,
BackboneSuper, BackboneSuper,
CourseListPage, CourseListPage,
CourseDetailPage) { CourseDetailPage,
CourseCreatePage) {
'use strict'; 'use strict';
return Backbone.Router.extend({ return Backbone.Router.extend({
...@@ -21,6 +23,7 @@ define([ ...@@ -21,6 +23,7 @@ define([
routes: { routes: {
'(/)': 'index', '(/)': 'index',
'new(/)': 'new',
'*path': 'notFound' '*path': 'notFound'
}, },
...@@ -93,6 +96,15 @@ define([ ...@@ -93,6 +96,15 @@ define([
var page = new CourseDetailPage({id: id}); var page = new CourseDetailPage({id: id});
this.currentView = page; this.currentView = page;
this.$el.html(page.el); 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);
} }
}); });
} }
......
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);
return this;
}
});
}
);
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)
});
}
);
...@@ -17,7 +17,8 @@ ...@@ -17,7 +17,8 @@
margin-top: spacing-vertical(small); margin-top: spacing-vertical(small);
} }
.course-detail-view { .course-detail-view,
.course-create-edit-view {
.page-header { .page-header {
margin-top: 0; margin-top: 0;
} }
...@@ -47,4 +48,10 @@ ...@@ -47,4 +48,10 @@
} }
} }
} }
.course-create-edit-view {
.fields {
width: 50%;
}
}
} }
<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="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>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<h1 class="hd-1 emphasized"> <h1 class="hd-1 emphasized">
<%- gettext('Courses') %> <%- gettext('Courses') %>
<div class="pull-right"> <div class="pull-right">
<button class="btn btn-primary btn-small"><%- gettext('Add New Course') %></button> <a href="/courses/new/" class="btn btn-primary btn-small"><%- gettext('Add New Course') %></a>
</div> </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>
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