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