Commit d3e328dd by Dmitry Viskov

Description fields for all uploaded files

parent 179f685b
...@@ -12,15 +12,20 @@ ...@@ -12,15 +12,20 @@
<div class="{{ class_prefix }}__display__file {% if not file_urls %}is--hidden{% endif %} submission__{{ file_upload_type }}__upload" data-upload-type="{{ file_upload_type }}"> <div class="{{ class_prefix }}__display__file {% if not file_urls %}is--hidden{% endif %} submission__{{ file_upload_type }}__upload" data-upload-type="{{ file_upload_type }}">
<div class="submission__answer__files"> <div class="submission__answer__files">
{% for file_url in file_urls %} {% for file_url, file_description in file_urls %}
<div class="submission__answer__file__block__{{ forloop.counter0 }}"> <div class="submission__answer__file__block submission__answer__file__block__{{ forloop.counter0 }}">
{% if file_upload_type == "image" %} {% if file_upload_type == "image" %}
<img class="submission__answer__file submission--image" {% if file_description %}
alt="{% trans "The image associated with this submission:" %} #{{ forloop.counter }}" <div class="submission__file__description__label">{{ file_description }}:</div>
src="{{ file_url }}" /> {% endif %}
<div><img class="submission__answer__file submission--image" src="{{ file_url }}" /></div>
{% elif file_upload_type == "pdf-and-image" or file_upload_type == "custom" %} {% elif file_upload_type == "pdf-and-image" or file_upload_type == "custom" %}
<a href="{{ file_url }}" class="submission__answer__file submission--file" target="_blank"> <a href="{{ file_url }}" class="submission__answer__file submission--file" target="_blank">
{% trans "View the files associated with this submission:" %} #{{ forloop.counter }} {% if file_description %}
{{ file_description }}
{% else %}
{% trans "View the files associated with this submission:" %} #{{ forloop.counter }}
{% endif %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
......
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
{% trans "Your peer's response to the question above" as translated_label %} {% trans "Your peer's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=peer_submission.answer answer_text_label=translated_label %} {% include "openassessmentblock/oa_submission_answer.html" with answer=peer_submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %} {% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=peer_file_urls header=translated_header class_prefix="peer-assessment" show_warning="true" %} {% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=peer_file_urls header=translated_header class_prefix="peer-assessment" show_warning="true" %}
</div> </div>
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
{% trans "Your peer's response to the question above" as translated_label %} {% trans "Your peer's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=peer_submission.answer answer_text_label=translated_label %} {% include "openassessmentblock/oa_submission_answer.html" with answer=peer_submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %} {% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=peer_file_urls header=translated_header class_prefix="peer-assessment" show_warning="true" %} {% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=peer_file_urls header=translated_header class_prefix="peer-assessment" show_warning="true" %}
</div> </div>
......
...@@ -95,17 +95,19 @@ ...@@ -95,17 +95,19 @@
<li class="field"> <li class="field">
<div class="upload__error"> <div class="upload__error">
<div class="message message--inline message--error message--error-server" tabindex="-1"> <div class="message message--inline message--error message--error-server" tabindex="-1">
<h5 class="message__title">{% trans "We could not upload this file" %}</h5> <h5 class="message__title">{% trans "We could not upload files" %}</h5>
<div class="message__content"></div> <div class="message__content"></div>
</div> </div>
</div> </div>
<label class="sr" for="submission_answer_upload_{{ xblock_id }}">{% trans "Select a file to upload for this submission." %}</label> <label class="sr" for="submission_answer_upload_{{ xblock_id }}">{% trans "Select a file to upload for this submission." %}</label>
<input type="file" class="submission__answer__upload file--upload" id="submission_answer_upload_{{ xblock_id }}" multiple=""> <input type="file" class="submission__answer__upload file--upload" id="submission_answer_upload_{{ xblock_id }}" multiple="">
<div class="files__descriptions"></div>
<button type="submit" class="file__upload action action--upload" disabled>{% trans "Upload files" %}</button> <button type="submit" class="file__upload action action--upload" disabled>{% trans "Upload files" %}</button>
</li> </li>
{% endif %} {% endif %}
<li class="field">
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=file_urls class_prefix="submission__answer"%} {% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=file_urls class_prefix="submission__answer"%}
</li>
</ol> </ol>
<span class="tip" id="submission__answer__tip__{{ xblock_id }}">{% trans "You may continue to work on your response until you submit it." %}</span> <span class="tip" id="submission__answer__tip__{{ xblock_id }}">{% trans "You may continue to work on your response until you submit it." %}</span>
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
{% trans "Your response" as translated_label %} {% trans "Your response" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=self_submission.answer answer_text_label=translated_label %} {% include "openassessmentblock/oa_submission_answer.html" with answer=self_submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %} {% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=self_file_urls header=translated_header class_prefix="self-assessment" %} {% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=self_file_urls header=translated_header class_prefix="self-assessment" %}
</article> </article>
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
{% trans "The learner's response to the question above" as translated_label %} {% trans "The learner's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label=translated_label %} {% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %} {% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=staff_file_urls header=translated_header class_prefix="staff-assessment" show_warning="true" %} {% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=staff_file_urls header=translated_header class_prefix="staff-assessment" show_warning="true" %}
</div> </div>
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
{% trans "The learner's response to the question above" as translated_label %} {% trans "The learner's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label=translated_label %} {% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %} {% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=staff_file_urls header=translated_header class_prefix="staff-assessment" show_warning="true" %} {% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=staff_file_urls header=translated_header class_prefix="staff-assessment" show_warning="true" %}
</div> </div>
......
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
{% trans "The learner's response to the question above" as translated_label %} {% trans "The learner's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label=translated_label %} {% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %} {% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=staff_file_urls header=translated_header class_prefix="staff-assessment" show_warning="true" %} {% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=staff_file_urls header=translated_header class_prefix="staff-assessment" show_warning="true" %}
</div> </div>
{% endif %} {% endif %}
......
...@@ -216,6 +216,12 @@ class OpenAssessmentBlock(MessageMixin, ...@@ -216,6 +216,12 @@ class OpenAssessmentBlock(MessageMixin,
help="Saved response submission for the current user." help="Saved response submission for the current user."
) )
saved_files_descriptions = String(
default=u"",
scope=Scope.user_state,
help="Saved descriptions for each uploaded file."
)
no_peers = Boolean( no_peers = Boolean(
default=False, default=False,
scope=Scope.user_state, scope=Scope.user_state,
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -96,7 +96,8 @@ ...@@ -96,7 +96,8 @@
}, },
"save_status": "This response has not been saved.", "save_status": "This response has not been saved.",
"submit_enabled": false, "submit_enabled": false,
"submission_due": "" "submission_due": "",
"file_upload_type": "image"
}, },
"output": "oa_response.html" "output": "oa_response.html"
}, },
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -140,6 +140,12 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -140,6 +140,12 @@ describe("OpenAssessment.ResponseView", function() {
defer.resolve(); defer.resolve();
}); });
}); });
spyOn(view, 'saveFilesDescriptions').and.callFake(function() {
return $.Deferred(function(defer) {
view.removeFilesDescriptions();
defer.resolve();
});
});
}); });
afterEach(function() { afterEach(function() {
...@@ -511,7 +517,7 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -511,7 +517,7 @@ describe("OpenAssessment.ResponseView", function() {
it("uploads two images using a one-time URL", function() { it("uploads two images using a one-time URL", function() {
var files = [{type: 'image/jpeg', size: 1024, name: 'picture1.jpg', data: ''}, var files = [{type: 'image/jpeg', size: 1024, name: 'picture1.jpg', data: ''},
{type: 'image/jpeg', size: 1024, name: 'picture2.jpg', data: ''}]; {type: 'image/jpeg', size: 1024, name: 'picture2.jpg', data: ''}];
view.prepareUpload(files, 'image'); view.prepareUpload(files, 'image', ['text1', 'text2']);
view.uploadFiles(); view.uploadFiles();
expect(fileUploader.uploadArgs[0].url).toEqual(FAKE_URL); expect(fileUploader.uploadArgs[0].url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs[0].data).toEqual(files[0]); expect(fileUploader.uploadArgs[0].data).toEqual(files[0]);
...@@ -521,7 +527,7 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -521,7 +527,7 @@ describe("OpenAssessment.ResponseView", function() {
it("uploads a PDF using a one-time URL", function() { it("uploads a PDF using a one-time URL", function() {
var files = [{type: 'application/pdf', size: 1024, name: 'application.pdf', data: ''}]; var files = [{type: 'application/pdf', size: 1024, name: 'application.pdf', data: ''}];
view.prepareUpload(files, 'pdf-and-image'); view.prepareUpload(files, 'pdf-and-image', ['text']);
view.uploadFiles(); view.uploadFiles();
expect(fileUploader.uploadArgs[0].url).toEqual(FAKE_URL); expect(fileUploader.uploadArgs[0].url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs[0].data).toEqual(files[0]); expect(fileUploader.uploadArgs[0].data).toEqual(files[0]);
...@@ -529,7 +535,7 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -529,7 +535,7 @@ describe("OpenAssessment.ResponseView", function() {
it("uploads a arbitrary type file using a one-time URL", function() { it("uploads a arbitrary type file using a one-time URL", function() {
var files = [{type: 'text/html', size: 1024, name: 'index.html', data: ''}]; var files = [{type: 'text/html', size: 1024, name: 'index.html', data: ''}];
view.prepareUpload(files, 'custom'); view.prepareUpload(files, 'custom', ['text']);
view.uploadFiles(); view.uploadFiles();
expect(fileUploader.uploadArgs[0].url).toEqual(FAKE_URL); expect(fileUploader.uploadArgs[0].url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs[0].data).toEqual(files[0]); expect(fileUploader.uploadArgs[0].data).toEqual(files[0]);
...@@ -542,7 +548,7 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -542,7 +548,7 @@ describe("OpenAssessment.ResponseView", function() {
// Attempt to upload a file // Attempt to upload a file
var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}]; var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files, 'image'); view.prepareUpload(files, 'image', ['text']);
view.uploadFiles(); view.uploadFiles();
// Expect an error to be displayed // Expect an error to be displayed
...@@ -562,4 +568,48 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -562,4 +568,48 @@ describe("OpenAssessment.ResponseView", function() {
// Expect an error to be displayed // Expect an error to be displayed
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'ERROR'); expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'ERROR');
}); });
it("disables the upload button if any file description is not set", function() {
function getFileUploadField() {
return $(view.element).find('.file__upload').first();
}
spyOn(view, 'updateFilesDescriptionsFields').and.callThrough();
var files = [{type: 'image/jpeg', size: 1024, name: 'picture1.jpg', data: ''},
{type: 'image/jpeg', size: 1024, name: 'picture2.jpg', data: ''}];
view.prepareUpload(files, 'image');
expect(getFileUploadField().is(':disabled')).toEqual(true);
expect(view.updateFilesDescriptionsFields).toHaveBeenCalledWith(files, undefined);
// set the first description field (the second is still empty)
// and check that upload button is disabled
var firstDescriptionField1 = $(view.element).find('.file__description__0').first();
$(firstDescriptionField1).val('test');
view.checkFilesDescriptions();
expect(getFileUploadField().is(':disabled')).toEqual(true);
// set the second description field (now both descriptions are not empty)
// and check that upload button is enabled
var firstDescriptionField2 = $(view.element).find('.file__description__1').first();
$(firstDescriptionField2).val('test2');
view.checkFilesDescriptions();
expect(getFileUploadField().is(':disabled')).toEqual(false);
// remove value in the first upload field
// and check that upload button is disabled
$(firstDescriptionField1).val('');
view.checkFilesDescriptions();
expect(getFileUploadField().is(':disabled')).toEqual(true);
});
it("removes description fields after files upload", function() {
var files = [{type: 'image/jpeg', size: 1024, name: 'picture1.jpg', data: ''},
{type: 'image/jpeg', size: 1024, name: 'picture2.jpg', data: ''}];
view.prepareUpload(files, 'image', ['test1', 'test2']);
expect($(view.element).find('.file__description').length).toEqual(2);
view.uploadFiles();
expect($(view.element).find('.file__description').length).toEqual(0);
});
}); });
...@@ -167,6 +167,34 @@ describe("OpenAssessment.Server", function() { ...@@ -167,6 +167,34 @@ describe("OpenAssessment.Server", function() {
}); });
}); });
it("removes uploaded files", function() {
stubAjax(true, {'success': true, 'msg': ''});
var success = false;
server.removeUploadedFiles().done(function() { success = true; });
expect(success).toBe(true);
expect($.ajax).toHaveBeenCalledWith({
url: "/remove_all_uploaded_files",
type: "POST",
data: JSON.stringify({}),
contentType : jsonContentType
});
});
it("saves files descriptions", function() {
stubAjax(true, {'success': true, 'msg': ''});
var success = false;
server.saveFilesDescriptions(['test1', 'test2']).done(function() { success = true; });
expect(success).toBe(true);
expect($.ajax).toHaveBeenCalledWith({
url: "/save_files_descriptions",
type: "POST",
data: JSON.stringify({descriptions: ['test1', 'test2']}),
contentType : jsonContentType
});
});
it("sends a peer-assessment to the XBlock", function() { it("sends a peer-assessment to the XBlock", function() {
stubAjax(true, {success: true, msg: ''}); stubAjax(true, {success: true, msg: ''});
......
...@@ -18,6 +18,7 @@ OpenAssessment.ResponseView = function(element, server, fileUploader, baseView, ...@@ -18,6 +18,7 @@ OpenAssessment.ResponseView = function(element, server, fileUploader, baseView,
this.baseView = baseView; this.baseView = baseView;
this.savedResponse = []; this.savedResponse = [];
this.files = null; this.files = null;
this.filesDescriptions = [];
this.filesType = null; this.filesType = null;
this.lastChangeTime = Date.now(); this.lastChangeTime = Date.now();
this.errorOnLastSave = false; this.errorOnLastSave = false;
...@@ -130,8 +131,16 @@ OpenAssessment.ResponseView.prototype = { ...@@ -130,8 +131,16 @@ OpenAssessment.ResponseView.prototype = {
function(eventObject) { function(eventObject) {
// Override default form submission // Override default form submission
eventObject.preventDefault(); eventObject.preventDefault();
var previouslyUploadedFiles = sel.find('.submission__answer__file').length ? true : false;
$('.submission__answer__display__file', view.element).removeClass('is--hidden'); $('.submission__answer__display__file', view.element).removeClass('is--hidden');
view.uploadFiles(); if (previouslyUploadedFiles) {
var msg = gettext('After you upload new files all your previously uploaded files will be overwritten. Continue?'); // jscs:ignore maximumLineLength
if (confirm(msg)) {
view.uploadFiles();
}
} else {
view.uploadFiles();
}
} }
); );
}, },
...@@ -382,6 +391,9 @@ OpenAssessment.ResponseView.prototype = { ...@@ -382,6 +391,9 @@ OpenAssessment.ResponseView.prototype = {
var msg = gettext('Do you want to upload your file before submitting?'); var msg = gettext('Do you want to upload your file before submitting?');
if (confirm(msg)) { if (confirm(msg)) {
fileDefer = view.uploadFiles(); fileDefer = view.uploadFiles();
if (fileDefer === false) {
return;
}
} else { } else {
view.submitEnabled(true); view.submitEnabled(true);
return; return;
...@@ -475,7 +487,7 @@ OpenAssessment.ResponseView.prototype = { ...@@ -475,7 +487,7 @@ OpenAssessment.ResponseView.prototype = {
file or custom. file or custom.
**/ **/
prepareUpload: function(files, uploadType) { prepareUpload: function(files, uploadType, descriptions) {
this.files = null; this.files = null;
this.filesType = uploadType; this.filesType = uploadType;
this.filesUploaded = false; this.filesUploaded = false;
...@@ -535,14 +547,94 @@ OpenAssessment.ResponseView.prototype = { ...@@ -535,14 +547,94 @@ OpenAssessment.ResponseView.prototype = {
if (!errorCheckerTriggered) { if (!errorCheckerTriggered) {
this.baseView.toggleActionError('upload', null); this.baseView.toggleActionError('upload', null);
this.files = files; this.files = files;
this.updateFilesDescriptionsFields(files, descriptions);
}
if (this.files === null) {
sel.find('.file__upload').prop('disabled', true);
}
},
/**
Render textarea fields to input description for each uploaded file.
*/
updateFilesDescriptionsFields: function(files, descriptions) {
var filesDescriptions = $(this.element).find('.files__descriptions').first();
var mainDiv = null;
var div1 = null;
var div2 = null;
var textarea = null;
var descriptionsExists = true;
this.filesDescriptions = descriptions || [];
$(filesDescriptions).show().html('');
for (var i = 0; i < files.length; i++) {
mainDiv = $('<div/>');
div1 = $('<div/>');
div1.addClass('submission__file__description__label');
div1.text(gettext("Describe ") + files[i].name + ' ' + gettext("(required):"));
div1.appendTo(mainDiv);
div2 = $('<div/>');
div2.addClass('submission__file__description');
textarea = $('<textarea />');
if ((this.filesDescriptions.indexOf(i) !== -1) && (this.filesDescriptions[i] !== '')) {
textarea.val(this.filesDescriptions[i]);
} else {
descriptionsExists = false;
}
textarea.addClass('file__description file__description__' + i);
textarea.appendTo(div2);
div2.appendTo(mainDiv);
mainDiv.appendTo(filesDescriptions);
textarea.on("change keyup drop paste", $.proxy(this, "checkFilesDescriptions"));
}
$(this.element).find('.file__upload').prop('disabled', !descriptionsExists);
},
/**
When user type something in some file description field this function check input
and block/unblock "Upload" button
*/
checkFilesDescriptions: function() {
var isError = false;
var filesDescriptions = [];
$(this.element).find('.file__description').each(function() {
var filesDescriptionVal = $(this).val();
if (filesDescriptionVal) {
filesDescriptions.push(filesDescriptionVal);
} else {
isError = true;
}
});
$(this.element).find('.file__upload').prop('disabled', isError);
if (!isError) {
this.filesDescriptions = filesDescriptions;
} }
sel.find('.file__upload').prop('disabled', this.files === null); },
/**
Clear field with files descriptions.
*/
removeFilesDescriptions: function() {
var filesDescriptions = $(this.element).find('.files__descriptions').first();
$(filesDescriptions).hide().html('');
}, },
/** /**
Remove previously uploaded files. Remove previously uploaded files.
**/ */
removeUploadedFiles: function() { removeUploadedFiles: function() {
var view = this; var view = this;
var sel = $('.step--response', this.element); var sel = $('.step--response', this.element);
...@@ -559,6 +651,24 @@ OpenAssessment.ResponseView.prototype = { ...@@ -559,6 +651,24 @@ OpenAssessment.ResponseView.prototype = {
}, },
/** /**
Sends request to server to save all file descriptions.
*/
saveFilesDescriptions: function() {
var view = this;
var sel = $('.step--response', this.element);
return this.server.saveFilesDescriptions(this.filesDescriptions).done(
function() {
view.removeFilesDescriptions();
}
).fail(function(errMsg) {
view.baseView.toggleActionError('upload', errMsg);
sel.find('.file__upload').prop('disabled', false);
});
},
/**
Manages file uploads for submission attachments. Manages file uploads for submission attachments.
**/ **/
...@@ -571,6 +681,9 @@ OpenAssessment.ResponseView.prototype = { ...@@ -571,6 +681,9 @@ OpenAssessment.ResponseView.prototype = {
sel.find('.file__upload').prop('disabled', true); sel.find('.file__upload').prop('disabled', true);
promise = view.removeUploadedFiles(); promise = view.removeUploadedFiles();
promise = promise.then(function() {
return view.saveFilesDescriptions();
});
$.each(view.files, function(index, file) { $.each(view.files, function(index, file) {
promise = promise.then(function() { promise = promise.then(function() {
...@@ -605,6 +718,7 @@ OpenAssessment.ResponseView.prototype = { ...@@ -605,6 +718,7 @@ OpenAssessment.ResponseView.prototype = {
view.baseView.toggleActionError('upload', null); view.baseView.toggleActionError('upload', null);
if (finalUpload) { if (finalUpload) {
view.filesUploaded = true; view.filesUploaded = true;
sel.find('input[type=file]').val('');
} }
}) })
.fail(handleError); .fail(handleError);
...@@ -622,32 +736,39 @@ OpenAssessment.ResponseView.prototype = { ...@@ -622,32 +736,39 @@ OpenAssessment.ResponseView.prototype = {
view.server.getDownloadUrl(filenum).done(function(url) { view.server.getDownloadUrl(filenum).done(function(url) {
var className = 'submission__answer__file__block__' + filenum; var className = 'submission__answer__file__block__' + filenum;
var file = null; var file = null;
var img = null;
var fileBlock = null; var fileBlock = null;
var fileBlockExists = sel.find("." + className).length ? true : false; var fileBlockExists = sel.find("." + className).length ? true : false;
var div1 = null;
var div2 = null;
if (!fileBlockExists) {
fileBlock = $('<div/>');
fileBlock.addClass('submission__answer__file__block ' + className);
fileBlock.appendTo(sel.find('.submission__answer__files').first());
}
if (view.filesType === 'image') { if (view.filesType === 'image') {
file = $('<img />'); div1 = $('<div/>');
file.addClass('submission__answer__file submission--image'); div1.addClass('submission__file__description__label');
file.attr('alt', gettext("The image associated with this submission:") + ' #' + (filenum + 1)); div1.text(view.filesDescriptions[filenum] + ':');
file.attr('src', url); div1.appendTo(fileBlock);
img = $('<img />');
img.addClass('submission__answer__file submission--image');
img.attr('src', url);
div2 = $('<div/>');
div2.html(img);
div2.appendTo(fileBlock);
} else { } else {
file = $('<a />', { file = $('<a />', {
href: url, href: url,
text: gettext("View the file associated with this submission:") + ' #' + (filenum + 1) text: view.filesDescriptions[filenum]
}); });
file.addClass('submission__answer__file submission--file'); file.addClass('submission__answer__file submission--file');
file.attr('target', '_blank'); file.attr('target', '_blank');
} file.appendTo(fileBlock);
if (file) {
if (fileBlockExists) {
sel.find("." + className).html(file);
} else {
fileBlock = $('<div/>');
fileBlock.addClass(className);
file.appendTo(fileBlock);
fileBlock.appendTo(sel.find('.submission__answer__files').first());
}
} }
return url; return url;
......
...@@ -519,8 +519,29 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) { ...@@ -519,8 +519,29 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
url: url, url: url,
data: JSON.stringify({}), data: JSON.stringify({}),
contentType: jsonContentType contentType: jsonContentType
}).done(function() { }).done(function(data) {
defer.resolve(); if (data.success) { defer.resolve(); }
else { defer.rejectWith(this, [data.msg]); }
}).fail(function() {
defer.rejectWith(this, [gettext('Server error.')]);
});
}).promise();
},
/**
* Sends request to server to save descriptions for each uploaded file.
*/
saveFilesDescriptions: function(descriptions) {
var url = this.url('save_files_descriptions');
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify({descriptions: descriptions}),
contentType: jsonContentType
}).done(function(data) {
if (data.success) { defer.resolve(); }
else { defer.rejectWith(this, [data.msg]); }
}).fail(function() { }).fail(function() {
defer.rejectWith(this, [gettext('Server error.')]); defer.rejectWith(this, [gettext('Server error.')]);
}); });
......
...@@ -1048,6 +1048,7 @@ ...@@ -1048,6 +1048,7 @@
@extend %action-2; @extend %action-2;
@include text-align(center); @include text-align(center);
@include float(right); @include float(right);
display: inline-block; display: inline-block;
margin: ($baseline-v/2) 0; margin: ($baseline-v/2) 0;
box-shadow: none; box-shadow: none;
......
...@@ -554,6 +554,14 @@ ...@@ -554,6 +554,14 @@
} }
} }
.submission__file__description__label {
margin-bottom: 5px;
}
.submission__answer__file__block {
margin-bottom: 8px;
}
// -------------------- // --------------------
// response // response
// -------------------- // --------------------
...@@ -573,9 +581,26 @@ ...@@ -573,9 +581,26 @@
@extend %text-sr; @extend %text-sr;
} }
textarea { .files__descriptions {
@extend %ui-content-longanswer; display: none;
min-height: ($baseline-v*10);
.submission__file__description {
padding-bottom: 10px;
}
}
.submission__answer__part__text {
textarea {
@extend %ui-content-longanswer;
min-height: ($baseline-v*10);
}
}
.submission__file__description {
textarea {
@extend %ui-content-longanswer;
min-height: ($baseline-v*4);
}
} }
.tip { .tip {
......
...@@ -104,9 +104,14 @@ class SubmissionMixin(object): ...@@ -104,9 +104,14 @@ class SubmissionMixin(object):
status_text = self._(u'Multiple submissions are not allowed.') status_text = self._(u'Multiple submissions are not allowed.')
if not workflow: if not workflow:
try: try:
try:
saved_files_descriptions = json.loads(self.saved_files_descriptions)
except ValueError:
saved_files_descriptions = None
submission = self.create_submission( submission = self.create_submission(
student_item_dict, student_item_dict,
student_sub_data student_sub_data,
saved_files_descriptions
) )
except api.SubmissionRequestError as err: except api.SubmissionRequestError as err:
...@@ -187,25 +192,73 @@ class SubmissionMixin(object): ...@@ -187,25 +192,73 @@ class SubmissionMixin(object):
else: else:
return {'success': False, 'msg': self._(u"This response was not submitted.")} return {'success': False, 'msg': self._(u"This response was not submitted.")}
def create_submission(self, student_item_dict, student_sub_data): @XBlock.json_handler
def save_files_descriptions(self, data, suffix=''):
"""
Save the descriptions for each uploaded file.
Args:
data (dict): Data should have a single key 'descriptions' that contains
the texts for each uploaded file.
suffix (str): Not used.
Returns:
dict: Contains a bool 'success' and unicode string 'msg'.
"""
if 'descriptions' in data:
descriptions = data['descriptions']
if isinstance(descriptions, list):
all_description_correct = True
for description in descriptions:
if not isinstance(description, basestring):
all_description_correct = False
break
if all_description_correct:
try:
self.saved_files_descriptions = json.dumps(descriptions)
# Emit analytics event...
self.runtime.publish(
self,
"openassessmentblock.save_files_descriptions",
{"saved_response": self.saved_files_descriptions}
)
except:
return {'success': False, 'msg': self._(u"Files descriptions could not be saved.")}
else:
return {'success': True, 'msg': u''}
return {'success': False, 'msg': self._(u"Files descriptions were not submitted.")}
def create_submission(self, student_item_dict, student_sub_data, files_descriptions=None):
# Store the student's response text in a JSON-encodable dict # Store the student's response text in a JSON-encodable dict
# so that later we can add additional response fields. # so that later we can add additional response fields.
files_descriptions = files_descriptions if files_descriptions else []
student_sub_dict = prepare_submission_for_serialization(student_sub_data) student_sub_dict = prepare_submission_for_serialization(student_sub_data)
if self.file_upload_type: if self.file_upload_type:
student_sub_dict['file_keys'] = [] student_sub_dict['file_keys'] = []
student_sub_dict['files_descriptions'] = []
for i in range(self.MAX_FILES_COUNT): for i in range(self.MAX_FILES_COUNT):
key_to_save = '' key_to_save = ''
file_description = ''
item_key = self._get_student_item_key(i) item_key = self._get_student_item_key(i)
try: try:
url = file_upload_api.get_download_url(item_key) url = file_upload_api.get_download_url(item_key)
if url: if url:
key_to_save = item_key key_to_save = item_key
try:
file_description = files_descriptions[i]
except IndexError:
pass
except FileUploadError: except FileUploadError:
pass pass
if key_to_save: if key_to_save:
student_sub_dict['file_keys'].append(key_to_save) student_sub_dict['file_keys'].append(key_to_save)
student_sub_dict['files_descriptions'].append(file_description)
else: else:
break break
...@@ -355,18 +408,24 @@ class SubmissionMixin(object): ...@@ -355,18 +408,24 @@ class SubmissionMixin(object):
""" """
urls = [] urls = []
if 'file_keys' in submission['answer']: if 'file_keys' in submission['answer']:
keys = submission['answer'].get('file_keys', '') keys = submission['answer'].get('file_keys', [])
for key in keys: descriptions = submission['answer'].get('files_descriptions', [])
for idx, key in enumerate(keys):
url = self._get_url_by_file_key(key) url = self._get_url_by_file_key(key)
if url: if url:
urls.append(url) description = ''
try:
description = descriptions[idx]
except IndexError:
pass
urls.append((url, description))
else: else:
break break
elif 'file_key' in submission['answer']: elif 'file_key' in submission['answer']:
key = submission['answer'].get('file_key', '') key = submission['answer'].get('file_key', '')
url = self._get_url_by_file_key(key) url = self._get_url_by_file_key(key)
if url: if url:
urls.append(url) urls.append((url, ''))
return urls return urls
@staticmethod @staticmethod
...@@ -455,11 +514,21 @@ class SubmissionMixin(object): ...@@ -455,11 +514,21 @@ class SubmissionMixin(object):
context['allow_latex'] = self.allow_latex context['allow_latex'] = self.allow_latex
if self.file_upload_type: if self.file_upload_type:
try:
saved_files_descriptions = json.loads(self.saved_files_descriptions)
except ValueError:
saved_files_descriptions = []
context['file_urls'] = [] context['file_urls'] = []
for i in range(self.MAX_FILES_COUNT): for i in range(self.MAX_FILES_COUNT):
file_url = self._get_download_url(i) file_url = self._get_download_url(i)
file_description = ''
if file_url: if file_url:
context['file_urls'].append(file_url) try:
file_description = saved_files_descriptions[i]
except IndexError:
pass
context['file_urls'].append((file_url, file_description))
else: else:
break break
if self.file_upload_type == 'custom': if self.file_upload_type == 'custom':
......
<openassessment> <openassessment file_upload_type="pdf-and-image">
<title>Open Assessment Test</title> <title>Open Assessment Test</title>
<prompts> <prompts>
<prompt> <prompt>
......
# -*- coding: utf-8 -*-
"""
Test that the student can save a files descriptions.
"""
import json
import mock
from .base import XBlockHandlerTestCase, scenario
class SaveFilesDescriptionsTest(XBlockHandlerTestCase):
@scenario('data/save_scenario.xml', user_id="Daniels")
def test_save_files_descriptions_blank(self, xblock):
resp = self.request(xblock, 'save_files_descriptions', json.dumps({}))
self.assertIn('descriptions were not submitted', resp)
@scenario('data/save_scenario.xml', user_id="Perleman")
def test_save_files_descriptions(self, xblock):
# Save the response
descriptions = [u"Ѕраѓтаиѕ! ГоиіБЂт, Щэ ↁіиэ іи Нэll!", u"Ѕраѓтаиѕ! ГоиіБЂт, Щэ ↁіиэ іи Нэll!"]
payload = json.dumps({'descriptions': descriptions})
resp = self.request(xblock, 'save_files_descriptions', payload, response_format="json")
self.assertTrue(resp['success'])
self.assertEqual(resp['msg'], u'')
# Reload the submission UI
xblock._get_download_url = mock.MagicMock(side_effect=lambda i: "https://img-url/%d" % i)
resp = self.request(xblock, 'render_submission', json.dumps({}))
self.assertIn(descriptions[0], resp.decode('utf-8'))
self.assertIn(descriptions[1], resp.decode('utf-8'))
@scenario('data/save_scenario.xml', user_id="Valchek")
def test_overwrite_files_descriptions(self, xblock):
descriptions1 = [u"Ѕраѓтаиѕ! ГоиіБЂт, Щэ ↁіиэ іи Нэll!", u"Ѕраѓтаиѕ! ГоиіБЂт, Щэ ↁіиэ іи Нэll!"]
payload = json.dumps({'descriptions': descriptions1})
self.request(xblock, 'save_files_descriptions', payload, response_format="json")
descriptions2 = [u"test1", u"test2"]
payload = json.dumps({'descriptions': descriptions2})
self.request(xblock, 'save_files_descriptions', payload, response_format="json")
# Reload the submission UI
xblock._get_download_url = mock.MagicMock(side_effect=lambda i: "https://img-url/%d" % i)
resp = self.request(xblock, 'render_submission', json.dumps({}))
self.assertNotIn(descriptions1[0], resp.decode('utf-8'))
self.assertNotIn(descriptions1[1], resp.decode('utf-8'))
self.assertIn(descriptions2[0], resp.decode('utf-8'))
self.assertIn(descriptions2[1], resp.decode('utf-8'))
...@@ -408,7 +408,8 @@ class TestCourseStaff(XBlockHandlerTestCase): ...@@ -408,7 +408,8 @@ class TestCourseStaff(XBlockHandlerTestCase):
# Create an image submission for Bob, and corresponding workflow. # Create an image submission for Bob, and corresponding workflow.
self._create_submission(bob_item, { self._create_submission(bob_item, {
'text': "Bob Answer", 'text': "Bob Answer",
'file_keys': ["test_key"] 'file_keys': ["test_key"],
'files_descriptions': ["test_description"]
}, ['self']) }, ['self'])
# Mock the file upload API to avoid hitting S3 # Mock the file upload API to avoid hitting S3
...@@ -423,7 +424,7 @@ class TestCourseStaff(XBlockHandlerTestCase): ...@@ -423,7 +424,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
file_api.get_download_url.assert_called_with("test_key") file_api.get_download_url.assert_called_with("test_key")
# Check the context passed to the template # Check the context passed to the template
self.assertEquals(['http://www.example.com/image.jpeg'], context['staff_file_urls']) self.assertEquals([('http://www.example.com/image.jpeg', 'test_description')], context['staff_file_urls'])
self.assertEquals('image', context['file_upload_type']) self.assertEquals('image', context['file_upload_type'])
# Check the fully rendered template # Check the fully rendered template
...@@ -446,13 +447,15 @@ class TestCourseStaff(XBlockHandlerTestCase): ...@@ -446,13 +447,15 @@ class TestCourseStaff(XBlockHandlerTestCase):
bob_item["item_id"] = xblock.scope_ids.usage_id bob_item["item_id"] = xblock.scope_ids.usage_id
file_keys = ["test_key0", "test_key1", "test_key2"] file_keys = ["test_key0", "test_key1", "test_key2"]
files_descriptions = ["test_description0", "test_description1", "test_description2"]
images = ["http://www.example.com/image%d.jpeg" % i for i in range(3)] images = ["http://www.example.com/image%d.jpeg" % i for i in range(3)]
file_keys_with_images = dict(zip(file_keys, images)) file_keys_with_images = dict(zip(file_keys, images))
# Create an image submission for Bob, and corresponding workflow. # Create an image submission for Bob, and corresponding workflow.
self._create_submission(bob_item, { self._create_submission(bob_item, {
'text': "Bob Answer", 'text': "Bob Answer",
'file_keys': file_keys 'file_keys': file_keys,
'files_descriptions': files_descriptions
}, ['self']) }, ['self'])
# Mock the file upload API to avoid hitting S3 # Mock the file upload API to avoid hitting S3
...@@ -470,7 +473,8 @@ class TestCourseStaff(XBlockHandlerTestCase): ...@@ -470,7 +473,8 @@ class TestCourseStaff(XBlockHandlerTestCase):
file_api.get_download_url.assert_has_calls(calls) file_api.get_download_url.assert_has_calls(calls)
# Check the context passed to the template # Check the context passed to the template
self.assertEquals(images, context['staff_file_urls']) self.assertEquals([(image, "test_description%d" % i) for i, image in enumerate(images)],
context['staff_file_urls'])
self.assertEquals('image', context['file_upload_type']) self.assertEquals('image', context['file_upload_type'])
# Check the fully rendered template # Check the fully rendered template
...@@ -478,6 +482,7 @@ class TestCourseStaff(XBlockHandlerTestCase): ...@@ -478,6 +482,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
resp = self.request(xblock, "render_student_info", payload) resp = self.request(xblock, "render_student_info", payload)
for i in range(3): for i in range(3):
self.assertIn("http://www.example.com/image%d.jpeg" % i, resp) self.assertIn("http://www.example.com/image%d.jpeg" % i, resp)
self.assertIn("test_description%d" % i, resp)
@scenario('data/self_only_scenario.xml', user_id='Bob') @scenario('data/self_only_scenario.xml', user_id='Bob')
def test_staff_area_student_info_file_download_url_error(self, xblock): def test_staff_area_student_info_file_download_url_error(self, xblock):
......
...@@ -158,6 +158,38 @@ class SubmissionPage(OpenAssessmentPage): ...@@ -158,6 +158,38 @@ class SubmissionPage(OpenAssessmentPage):
self.wait_for_element_visibility(".submission__answer__upload", "File select button is present") self.wait_for_element_visibility(".submission__answer__upload", "File select button is present")
self.q(css=".submission__answer__upload").results[0].send_keys(file_path_name) self.q(css=".submission__answer__upload").results[0].send_keys(file_path_name)
def add_file_description(self, file_num, description):
"""
Submit a description for some file.
Args:
file_num (integer): file number
description (string): file description
"""
textarea_element = self._bounded_selector("textarea.file__description__%d" % file_num)
self.wait_for_element_visibility(textarea_element, "Textarea is present")
self.q(css=textarea_element).fill(description)
@property
def upload_file_button_is_enabled(self):
"""
Check if 'Upload files' button is enabled
Returns:
bool
"""
return self.q(css="button.file__upload").attrs('disabled') == ['false']
@property
def upload_file_button_is_disabled(self):
"""
Check if 'Upload files' button is disabled
Returns:
bool
"""
return self.q(css="button.file__upload").attrs('disabled') == ['true']
def upload_file(self): def upload_file(self):
""" """
Upload the selected file Upload the selected file
......
...@@ -743,9 +743,18 @@ class FileUploadTest(OpenAssessmentTest): ...@@ -743,9 +743,18 @@ class FileUploadTest(OpenAssessmentTest):
# trying to upload a acceptable file # trying to upload a acceptable file
readme1 = os.path.dirname(os.path.realpath(__file__)) + '/README.rst' readme1 = os.path.dirname(os.path.realpath(__file__)) + '/README.rst'
readme2 = readme1.replace('test/acceptance/', '') # There's another README located at ../../ readme2 = readme1.replace('test/acceptance/', '') # There's another README located at ../../
files = ', '.join([readme1, readme2]) files = ', '.join([readme1, readme2])
self.submission_page.visit().select_file(files) self.submission_page.visit().select_file(files)
self.assertFalse(self.submission_page.has_file_error) self.assertFalse(self.submission_page.has_file_error)
self.assertTrue(self.submission_page.upload_file_button_is_disabled)
self.submission_page.add_file_description(0, 'file description 1')
self.assertTrue(self.submission_page.upload_file_button_is_disabled)
self.submission_page.add_file_description(1, 'file description 2')
self.assertTrue(self.submission_page.upload_file_button_is_enabled)
self.submission_page.upload_file() self.submission_page.upload_file()
self.assertTrue(self.submission_page.have_files_uploaded) self.assertTrue(self.submission_page.have_files_uploaded)
......
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