Commit f6fb64f1 by cahrens

Saving checkbox state, client template for individual checklist.

parent 1aa8786b
......@@ -1103,11 +1103,7 @@ def course_info_updates(request, org, course, provided_id=None):
if not has_access(request.user, location):
raise PermissionDenied()
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
real_method = get_request_method(request)
if request.method == 'GET':
return HttpResponse(json.dumps(get_course_updates(location)), mimetype="application/json")
......@@ -1130,11 +1126,7 @@ def module_info(request, module_location):
if not has_access(request.user, location):
raise PermissionDenied()
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
real_method = get_request_method(request)
rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links))
......@@ -1259,21 +1251,18 @@ def course_grader_updates(request, org, course, name, grader_index=None):
location = get_location_and_verify_access(request, org, course, name)
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
real_method = get_request_method(request)
if real_method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course', name]), grader_index)),
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(location), grader_index)),
mimetype="application/json")
elif real_method == "DELETE":
# ??? Shoudl this return anything? Perhaps success fail?
CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course', name]), grader_index)
# ??? Should this return anything? Perhaps success fail?
CourseGradingModel.delete_grader(Location(location), grader_index)
return HttpResponse()
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course', name]), request.POST)),
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)),
mimetype="application/json")
......@@ -1289,11 +1278,7 @@ def course_advanced_updates(request, org, course, name):
"""
location = get_location_and_verify_access(request, org, course, name)
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
real_method = get_request_method(request)
if real_method == 'GET':
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
......@@ -1320,13 +1305,32 @@ def get_checklists(request, org, course, name):
course_module.metadata[key] = template_module.metadata[key]
modulestore.update_metadata(location, course_module.metadata)
checklists = course_module.metadata[key]
return render_to_response('checklists.html',
{
'context_course': course_module,
'checklists' : course_module.metadata[key]
'checklists' : checklists,
'checklists_json' : json.dumps(checklists)
})
@login_required
def update_checklist(request, org, course, name, checklist_index=None):
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
key = "checklists"
real_method = get_request_method(request)
if checklist_index is not None and (real_method == 'POST' or real_method == 'PUT'):
modified_checklist = json.loads(request.body)
(course_module.metadata[key])[int(checklist_index)] = modified_checklist
modulestore.update_metadata(location, course_module.metadata)
return HttpResponse(json.dumps(modified_checklist), mimetype="application/json")
elif request.method == 'GET':
# TODO: Would we ever get in this condition? Any point in having this code?
return HttpResponse(json.dumps(course_module.metadata[key]), mimetype="application/json")
@login_required
@ensure_csrf_cookie
......@@ -1593,3 +1597,12 @@ def get_location_and_verify_access(request, org, course, name):
raise PermissionDenied()
return location
def get_request_method(request):
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
return real_method
<section class="course-checklist" id="<%= 'course-checklist' + checklistIndex %>">
<% var widthPercentage = 'width:' + percentChecked + '%;'; %>
<span class="viz viz-checklist-status"><span class="viz value viz-checklist-status-value" style="<%= widthPercentage %>">
<span class="int"><%= percentChecked %></span>% of checklist completed</span></span>
<header>
<h3 class="checklist-title title-2 is-selectable" title="Collapse/Expand this Checklist">
<i class="ss-icon ss-symbolicons-standard icon-arrow ui-toggle-expansion">&#x25BE;</i>
<%= checklistShortDescription %></h3>
<span class="checklist-status status">
Tasks Completed: <span class="status-count"><%= itemsChecked %></span>/<span class="status-amount"><%= items.length %></span>
<i class="ss-icon ss-symbolicons-standard icon-confirm">&#x2713;</i>
</span>
</header>
<ul class="list list-tasks">
<% var taskIndex = 0; %>
<% _.each(items, function(item) { %>
<% var checked = item['is_checked']; %>
<li
<% if (checked) { %>
class="task is-completed"
<% } else { %>
class="task"
<% } %>
>
<% var taskId = 'course-checklist' + checklistIndex + '-task' + taskIndex; %>
<input type="checkbox" class="task-input" data-checklist="<%= checklistIndex %>" data-task="<%= taskIndex %>"
name="<%= taskId %>" id="<%= taskId %>"
<% if (checked) { %>
checked="checked"
<% } %>
>
<label class="task-details" for="<%= taskId %>">
<h4 class="task-name title title-3"><%= item['short_description'] %></h4>
<p class="task-description"><%= item['long_description'] %></p>
</label>
<% if (item['action_text'] !== '' && item['action_url'] !== '') { %>
<ul class="list-actions task-actions">
<li>
<a href="<%= item['action_url'] %>" class="action action-primary"><%= item['action_text'] %></a>
</li>
</ul>
<% } %>
</li>
<% taskIndex+=1; }) %>
</ul>
</section>
\ No newline at end of file
......@@ -14,10 +14,6 @@ $(document).ready(function () {
// scopes (namely the course-info tab)
window.$modalCover = $modalCover;
// Control whether template caching in local memory occurs (see template_loader.js). Caching screws up development but may
// be a good optimization in production (it works fairly well)
window.cachetemplates = false;
$body.append($modalCover);
$newComponentItem = $('.new-component-item');
$newComponentTypePicker = $('.new-component');
......
if (!CMS.Models['Checklists']) CMS.Models.Checklists = new Object();
\ No newline at end of file
CMS.Models.Checklist = Backbone.Model.extend({
});
CMS.Models.ChecklistCollection = Backbone.Collection.extend({
model : CMS.Models.Checklist,
parse: function(response) {
_.each(response,
function( element, idx ) {
element.id = idx;
});
return response;
}
});
// <!-- from https://github.com/Gazler/Underscore-Template-Loader/blob/master/index.html -->
// TODO Figure out how to initialize w/ static views from server (don't call .load but instead inject in django as strings)
// so this only loads the lazily loaded ones.
(function() {
if (typeof window.templateLoader == 'function') return;
(function () {
if (typeof window.templateLoader == 'function') return;
var templateLoader = {
templateVersion: "0.0.15",
templates: {},
loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) {
var self = this;
jQuery.ajax({url : filename,
success : function(data) {
self.addTemplate(templateName, data);
self.saveLocalTemplates();
callback(data);
},
error : function(xhdr, textStatus, errorThrown) {
console.log(textStatus); },
dataType : "html"
})
}
else {
callback(this.templates[templateName]);
}
},
addTemplate: function(templateName, data) {
// is there a reason this doesn't go ahead and compile the template? _.template(data)
// I suppose localstorage use would still req raw string rather than compiled version, but that sd work
// if it maintains a separate cache of uncompiled ones
this.templates[templateName] = data;
},
localStorageAvailable: function() {
try {
// window.cachetemplates is global set in base.js to turn caching on/off
return window.cachetemplates && 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
},
saveLocalTemplates: function() {
if (this.localStorageAvailable) {
localStorage.setItem("templates", JSON.stringify(this.templates));
localStorage.setItem("templateVersion", this.templateVersion);
}
},
loadLocalTemplates: function() {
if (this.localStorageAvailable) {
var templateVersion = localStorage.getItem("templateVersion");
if (templateVersion && templateVersion == this.templateVersion) {
var templates = localStorage.getItem("templates");
if (templates) {
templates = JSON.parse(templates);
for (var x in templates) {
if (!this.templates[x]) {
this.addTemplate(x, templates[x]);
templateVersion: "0.0.15",
templates: {},
// Control whether template caching in local memory occurs. Caching screws up development but may
// be a good optimization in production (it works fairly well).
cacheTemplates: false,
loadRemoteTemplate: function (templateName, filename, callback) {
if (!this.templates[templateName]) {
var self = this;
jQuery.ajax({url: filename,
success: function (data) {
self.addTemplate(templateName, data);
self.saveLocalTemplates();
callback(data);
},
error: function (xhdr, textStatus, errorThrown) {
console.log(textStatus);
},
dataType: "html"
})
}
else {
callback(this.templates[templateName]);
}
},
addTemplate: function (templateName, data) {
// is there a reason this doesn't go ahead and compile the template? _.template(data)
// I suppose localstorage use would still req raw string rather than compiled version, but that sd work
// if it maintains a separate cache of uncompiled ones
this.templates[templateName] = data;
},
localStorageAvailable: function () {
try {
return this.cacheTemplates && 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
},
saveLocalTemplates: function () {
if (this.localStorageAvailable()) {
localStorage.setItem("templates", JSON.stringify(this.templates));
localStorage.setItem("templateVersion", this.templateVersion);
}
},
loadLocalTemplates: function () {
if (this.localStorageAvailable()) {
var templateVersion = localStorage.getItem("templateVersion");
if (templateVersion && templateVersion == this.templateVersion) {
var templates = localStorage.getItem("templates");
if (templates) {
templates = JSON.parse(templates);
for (var x in templates) {
if (!this.templates[x]) {
this.addTemplate(x, templates[x]);
}
}
}
}
else {
localStorage.removeItem("templates");
localStorage.removeItem("templateVersion");
}
}
}
}
else {
localStorage.removeItem("templates");
localStorage.removeItem("templateVersion");
}
}
}
};
templateLoader.loadLocalTemplates();
window.templateLoader = templateLoader;
})();
})();
......@@ -5,44 +5,79 @@ CMS.Views.Checklists = Backbone.View.extend({
events : {
'click .course-checklist .checklist-title' : "toggleChecklist",
'click .course-checklist .task label' : "toggleTask",
'click .demo-checklistviz' : "demoUpdateProgress"
'click .course-checklist .task input' : "toggleTask"
},
initialize : function() {
// adding class and title needs to happen in HTML
// $('.course-checklist .checklist-title').each(function(e){
// $(this).addClass('is-selectable').attr('title','Collapse/Expand this Checklist').bind('click', this.toggleChecklist);
// });
var self = this;
// instantiates an editor template for each update in the collection
window.templateLoader.loadRemoteTemplate("checklist",
"/static/client_templates/checklist.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
}
);
},
toggleChecklist : function(e) {
(e).preventDefault();
$(e.target).closest('.course-checklist').toggleClass('is-collapsed');
render: function() {
// catch potential outside call before template loaded
if (!this.template) return this;
this.$el.empty();
var self = this;
_.each(this.collection.models,
function(checklist, index) {
self.$el.append(self.renderTemplate(checklist, index));
});
return this;
},
toggleTask : function (e) {
$(e.target).closest('.task').toggleClass('is-completed');
renderTemplate: function (checklist, index) {
var checklistItems = checklist.attributes['items'];
var itemsChecked = 0;
_.each(checklistItems,
function(checklist) {
if (checklist['is_checked']) {
itemsChecked +=1;
}
});
var percentChecked = Math.round((itemsChecked/checklistItems.length)*100);
return this.template({
checklistIndex : index,
checklistShortDescription : checklist.attributes['short_description'],
items: checklistItems,
itemsChecked: itemsChecked,
percentChecked: percentChecked});
},
// TODO: remove
demoUpdateProgress : function(e) {
toggleChecklist : function(e) {
(e).preventDefault();
$('#course-checklist0 .viz-checklist-status .viz-checklist-status-value').css('width','25%');
$(e.target).closest('.course-checklist').toggleClass('is-collapsed');
},
// TODO: not used. In-progress update checklist progress (based on checkbox check/uncheck events)
updateChecklistProgress : function(e) {
var $statusCount = this.$el.closest('.course-checklist').find('.status-count');
var $statusAmount = this.$el.closest('.course-checklist').find('.status-amount');
toggleTask : function (e) {
var self = this;
if (this.$el.attr('checked')) {
console.log('adding');
}
var completed = 'is-completed';
var $checkbox = $(e.target);
var $task = $checkbox.closest('.task');
$task.toggleClass(completed);
else {
console.log('subtracting');
}
var checklist_index = $checkbox.data('checklist');
var task_index = $checkbox.data('task');
var model = this.collection.at(checklist_index);
model.attributes.items[task_index].is_checked = $task.hasClass(completed);
model.save({},
{
success : function() {
var updatedTemplate = self.renderTemplate(model, checklist_index);
self.$el.find('#course-checklist'+checklist_index).first().replaceWith(updatedTemplate);
},
error : CMS.ServerError
});
}
});
\ No newline at end of file
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Course Checklists</%block>
<%block name="bodyclass">is-signedin course uxdesign checklists</%block>
<%namespace name='static' file='static_content.html'/>
<%block name="jsextra">
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/checklists_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/checklists.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
<script type="text/javascript">
$(document).ready(function () {
// TODO: do a fetch instead of passing text into constructor.
var checklistCollection = new CMS.Models.ChecklistCollection(${checklists_json | n}, {parse: true});
checklistCollection.url = "${reverse('checklists_updates', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}";
var editor = new CMS.Views.Checklists({
el: $('.course-checklists')
el: $('.course-checklists'),
collection: checklistCollection
});
// No need to call render yet-- state is not being injected.
// editor.render();
editor.render();
});
......@@ -42,46 +49,6 @@
<article class="content-primary" role="main">
<form id="course-checklists" class="course-checklists" method="post" action="">
<h2 class="title title-3 sr">Current Checklists</h2>
% for checklist in checklists:
<section class="course-checklist" id=${'course-checklist' + str(loop.index)}>
<span class="viz viz-checklist-status"><span class="viz value viz-checklist-status-value"><span class="int">0</span>% of checklist completed</span></span>
<header>
<h3 class="checklist-title title-2 is-selectable" title="Collapse/Expand this Checklist">
<i class="ss-icon ss-symbolicons-standard icon-arrow ui-toggle-expansion">&#x25BE;</i>
${checklist['short_description']}</h3>
<span class="checklist-status status">
Tasks Completed: <span class="status-count">0</span>/<span class="status-amount">4</span>
<i class="ss-icon ss-symbolicons-standard icon-confirm">&#x2713;</i>
</span>
</header>
<ul class="list list-tasks">
% for item in checklist['items']:
<li class="task">
<input type="checkbox" class="task-input" name=${'course-checklist' + str(loop.parent.index) + '-task' + str(loop.index)}
id=${'course-checklist' + str(loop.parent.index) + '-task' + str(loop.index)}
value=${item['is_checked']}>
<label class="task-details" for=${'course-checklist' + str(loop.parent.index) + '-task' + str(loop.index)}>
<h4 class="task-name title title-3">${item['short_description']}</h4>
<p class="task-description">${item['long_description']}</p>
</label>
% if item['action_text'] is not '' and item['action_url'] is not '':
<ul class="list-actions task-actions">
<li>
<a href=${item['action_url']} class="action action-primary">${item['action_text']}</a>
</li>
</ul>
% endif
</li>
% endfor
</ul>
</section>
% endfor
</form>
</article>
......
......@@ -84,6 +84,8 @@ urlpatterns = ('',
# User creation and updating views
urlpatterns += (
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/checklists/(?P<name>[^/]+)$', 'contentstore.views.get_checklists', name='checklists'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/checklists/(?P<name>[^/]+)/update(/)?(?P<checklist_index>.+)?.*$',
'contentstore.views.update_checklist', name='checklists_updates'),
url(r'^howitworks$', 'contentstore.views.howitworks', name='howitworks'),
url(r'^signup$', 'contentstore.views.signup', name='signup'),
......
......@@ -23,10 +23,10 @@ metadata:
"long_description": "They'll help you learn the other course authoring tools available to you, and will also help you find help when you need it.",
"is_checked": false,
"action_url": "",
"action_text": ""}],
"completed" : false},
{"short_description" : "Draft a Rough Course Outline",
"items" : [{"short_description": "Create your first Section and Subsection",
"action_text": ""}]
},
{"short_description" : "Draft a Rough Course Outline",
"items" : [{"short_description": "Create your first Section and Subsection",
"long_description": "Walk through the mechanics of building your course's first section and subsection through your course outline to start.",
"is_checked": false,
"action_url": "/course/",
......@@ -60,10 +60,10 @@ metadata:
"long_description": "Some course authors find creating a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future.",
"is_checked": false,
"action_url": "/course/",
"action_text": "Edit in Course Outline"}],
"completed" : false},
{"short_description" : "Explore edX's Support Tools",
"items" : [{"short_description": "Explore the Studio Help Forum",
"action_text": "Edit in Course Outline"}]
},
{"short_description" : "Explore edX's Support Tools",
"items" : [{"short_description": "Explore the Studio Help Forum",
"long_description": "Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio.",
"is_checked": false,
"action_url": "http://help.edge.edx.org/",
......@@ -77,10 +77,10 @@ metadata:
"long_description": "View the searchable Studio documentation to find answers to your questions or information about how to do specific tasks. Once you download the PDF, you can view it offline.",
"is_checked": false,
"action_url": "/",
"action_text": "Download Documentation"}],
"completed" : false},
{"short_description" : "Draft your Course Introduction",
"items" : [{"short_description": "Drafting a Course Description",
"action_text": "Download Documentation"}]
},
{"short_description" : "Draft your Course Introduction",
"items" : [{"short_description": "Drafting a Course Description",
"long_description": "Courses on edX each have their own introduction page, including a course video, description, and more. Draft the introduction students will read before deciding to enroll in your course.",
"is_checked": false,
"action_url": "/settings-details/",
......@@ -99,8 +99,8 @@ metadata:
"long_description": "Before a student jumps into a course without the necessary preparation, we'd like them to understand the prerequisites that will make them more likely to succeed.",
"is_checked": false,
"action_url": "/settings-details/",
"action_text": "Edit Course Details &amp; Schedule"}],
"completed" : false}
]
"action_text": "Edit Course Details &amp; Schedule"}]
}
]
data: { 'textbooks' : [ ], 'wiki_slug' : null }
children: []
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