Commit 2d6f8a3f by Tim Krones

Implement client-server communication for pagination.

parent cd69698b
......@@ -23,14 +23,16 @@ Instructor Tool: An XBlock for instructors to export student answers from a cour
All processing is done offline.
"""
import json
from django.core.paginator import Paginator
from xblock.core import XBlock
from xblock.exceptions import JsonHandlerError
from xblock.fields import Scope, String, Dict
from xblock.fields import Scope, String, Dict, List
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
loader = ResourceLoader(__name__)
PAGE_SIZE = 15
# Make '_' a no-op so we can scrape strings
def _(text):
......@@ -63,6 +65,12 @@ class InstructorToolBlock(XBlock):
default=None,
scope=Scope.user_state,
)
display_data = List(
# The list of results associated with the most recent successful export.
# Stored separately to avoid the overhead of sending it to the client.
default=None,
scope=Scope.user_state,
)
has_author_view = True
@property
......@@ -90,11 +98,26 @@ class InstructorToolBlock(XBlock):
self.active_export_task_id = ''
if task_result.successful():
if isinstance(task_result.result, dict) and not task_result.result.get('error'):
self.display_data = task_result.result['display_data']
del task_result.result['display_data']
self.last_export_result = task_result.result
else:
self.last_export_result = {'error': u'Unexpected result: {}'.format(repr(task_result.result))}
self.display_data = None
else:
self.last_export_result = {'error': unicode(task_result.result)}
self.display_data = None
@XBlock.json_handler
def get_result_page(self, data, suffix=''):
""" Return requested page of `last_export_result`. """
paginator = Paginator(self.display_data, PAGE_SIZE)
page = data.get('page', None)
return {
'display_data': paginator.page(page).object_list,
'num_results': len(self.display_data),
'page_size': PAGE_SIZE
}
def student_view(self, context=None):
""" Normal View """
......@@ -144,6 +167,7 @@ class InstructorToolBlock(XBlock):
self.last_export_result = {
'error': message,
}
self.display_data = None
raise JsonHandlerError(code, message)
@XBlock.json_handler
......@@ -157,6 +181,7 @@ class InstructorToolBlock(XBlock):
def _delete_export(self):
self.last_export_result = None
self.display_data = None
self.active_export_task_id = ''
@XBlock.json_handler
......
......@@ -18,12 +18,57 @@ function InstructorToolBlock(runtime, element) {
model: Result,
getCurrentPage: function(returnObject) {
var currentPage = this.state.currentPage;
if (returnObject) {
return this.getPage(currentPage);
state: {
order: 0
},
url: runtime.handlerUrl(element, 'get_result_page'),
parseState: function(response) {
return {
totalRecords: response.num_results,
pageSize: response.page_size
};
},
parseRecords: function(response) {
return _.map(response.display_data, function(row) {
return new Result(null, { values: row });
});
},
fetchOptions: {
reset: true,
type: 'POST',
contentType: 'application/json',
processData: false,
beforeSend: function(jqXHR, options) {
options.data = JSON.stringify(options.data);
}
return currentPage;
},
getFirstPage: function() {
Backbone.PageableCollection.prototype
.getFirstPage.call(this, this.fetchOptions);
},
getPreviousPage: function() {
Backbone.PageableCollection.prototype
.getPreviousPage.call(this, this.fetchOptions);
},
getNextPage: function() {
Backbone.PageableCollection.prototype
.getNextPage.call(this, this.fetchOptions);
},
getLastPage: function() {
Backbone.PageableCollection.prototype
.getLastPage.call(this, this.fetchOptions);
},
getCurrentPage: function() {
return this.state.currentPage;
},
getTotalPages: function() {
......@@ -34,17 +79,21 @@ function InstructorToolBlock(runtime, element) {
var ResultsView = Backbone.View.extend({
initialize: function() {
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
this._insertRecords(this.collection.getCurrentPage(true));
this._insertRecords();
this._updateControls();
this.$('#total-pages').text(this.collection.getTotalPages() || 0);
return this;
},
_insertRecords: function(records) {
_insertRecords: function() {
var tbody = this.$('tbody');
tbody.empty();
records.each(function(result, index) {
this.collection.each(function(result, index) {
var row = $('<tr>');
_.each(Result.properties, function(name) {
row.append($('<td>').text(result.get(name)));
......@@ -66,26 +115,26 @@ function InstructorToolBlock(runtime, element) {
},
_firstPage: function() {
this._insertRecords(this.collection.getFirstPage());
this.collection.getFirstPage();
this._updateControls();
},
_prevPage: function() {
if (this.collection.hasPreviousPage()) {
this._insertRecords(this.collection.getPreviousPage());
this.collection.getPreviousPage();
}
this._updateControls();
},
_nextPage: function() {
if (this.collection.hasNextPage()) {
this._insertRecords(this.collection.getNextPage());
this.collection.getNextPage();
}
this._updateControls();
},
_lastPage: function() {
this._insertRecords(this.collection.getLastPage());
this.collection.getLastPage();
this._updateControls();
},
......@@ -107,7 +156,7 @@ function InstructorToolBlock(runtime, element) {
});
var resultsView = new ResultsView({
collection: new Results([], { mode: "client", state: { pageSize: 15 } }),
collection: new Results([]),
el: $element.find('#results')
});
......@@ -207,13 +256,7 @@ function InstructorToolBlock(runtime, element) {
)
));
// Display results
var results = _.map(status.last_export_result.display_data, function(row) {
return new Result(null, { values: row });
});
resultsView.collection.fullCollection.reset(results);
resultsView.render();
resultsView.collection.getFirstPage();
showResults();
}
......
......@@ -7,7 +7,7 @@ from mock import patch, Mock
from selenium.common.exceptions import NoSuchElementException
from xblockutils.base_test import SeleniumXBlockTest
from problem_builder.instructor_tool import InstructorToolBlock
from problem_builder.instructor_tool import PAGE_SIZE, InstructorToolBlock
class MockTasksModule(object):
......@@ -199,7 +199,7 @@ class InstructorToolTest(SeleniumXBlockTest):
successful=True, display_data=[[
'Test section', 'Test subsection', 'Test unit',
'Test type', 'Test question', 'Test answer', 'Test username'
] for _ in range(45)]),
] for _ in range(PAGE_SIZE*3)]),
'instructor_task': True,
'instructor_task.models': MockInstructorTaskModelsModule(),
})
......@@ -224,7 +224,7 @@ class InstructorToolTest(SeleniumXBlockTest):
'Test type', 'Test question', 'Test answer', 'Test username'
]:
occurrences = re.findall(contents, result_block.text)
self.assertEqual(len(occurrences), 15)
self.assertEqual(len(occurrences), PAGE_SIZE)
self.assertFalse(first_page_button.is_enabled())
self.assertFalse(prev_page_button.is_enabled())
......
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