Commit 2bd3d401 by Muzaffar yousaf

Merge pull request #749 from edx/muzaffar/os-file-upload

Add support for file upload
parents c425531e 37c1176a
......@@ -17,5 +17,9 @@ Bastien Abadie <bastien@nextcairn.com>
Omar Al-Ithawi <oithawi@qrf.org>
Ahsan Ulhaq <ahsan@edx.org>
Ben Patterson <bpatterson@edx.org>
Giulio Gratta <giulio@giuliogratta.com>
Julien Romagnoli <julien.romagnoli@fbmx.net>
William Ono <william.ono@ubc.ca>
Pan Luo <pan.luo@ubc.ca>
Eric Fischer <efischer@edx.org>
Andy Armstrong <andya@edx.org>
......@@ -9,7 +9,7 @@ from django.core.urlresolvers import reverse
class Backend(BaseBackend):
"""
Upload openassessment student files (images) to a local filesystem. Note
Upload openassessment student files to a local filesystem. Note
that in order to use this file storage backend, you need to include the
urls from openassessment.fileupload in your urls.py file:
......
......@@ -101,15 +101,33 @@
</div>
<p class="setting-help">{% trans "The date and time when learners can no longer submit responses." %}</p>
</li>
<li id="openassessment_submission_image_wrapper" class="field comp-setting-entry">
<li id="openassessment_submission_file_wrapper" class="field comp-setting-entry">
<div class="wrapper-comp-setting">
<label for="openassessment_submission_image_editor" class="setting-label">{% trans "Allow Image Responses"%}</label>
<select id="openassessment_submission_image_editor" class="input setting-input" name="image submission">
<option value="0">{% trans "False"%}</option>
<option value="1" {% if allow_file_upload %} selected="true" {% endif %}>{% trans "True"%}</option>
<label for="openassessment_submission_upload_selector" class="setting-label">{% trans "Allow File Upload"%}</label>
<select id="openassessment_submission_upload_selector" class="input setting-input" name="upload submission">
<option value="">{% trans "None"%}</option>
<option value="image" {% if file_upload_type == "image" %} selected="true" {% endif %}>{% trans "Image File"%}</option>
<option value="pdf-and-image" {% if file_upload_type == "pdf-and-image" %} selected="true" {% endif %}>{% trans "PDF or Image File"%}</option>
<option value="custom" {% if file_upload_type == "custom" %} selected="true" {% endif %}>{% trans "Custom File Types"%}</option>
</select>
</div>
<p class="setting-help">{% trans "Specify whether learners can submit an image file along with their text response." %}</p>
<p class="setting-help">
{% trans "Specify whether learners can submit a file along with their text response. Select Image to allow JPG, GIF, or PNG files. Select PDF or Image to allow PDF files and images. Select Custom File Types to allow files with extensions that you specify below. (Use this option with caution.)" %}
</p>
<div id="openassessment_submission_white_listed_file_types_wrapper" class="{% if file_upload_type != "custom" %}is--hidden{% endif %}">
<div class="wrapper-comp-setting">
<label for="openassessment_submission_white_listed_file_types" class="setting-label">{% trans "File Types" %}</label>
<input id="openassessment_submission_white_listed_file_types"
class="input setting-input"
type="text"
value="{{ white_listed_file_types }}"
/>
</div>
<p class="setting-help">
{% trans "Enter the file extensions, separated by commas, that you want learners to be able to upload. For example: pdf,doc,docx." %}
</p>&nbsp;
<p class="setting-help message-status error"></p>
</div>
</li>
<li id="openassessment_submission_latex_wrapper" class="field comp-setting-entry">
<div class="wrapper-comp-setting">
......
......@@ -30,15 +30,7 @@
{% include "openassessmentblock/oa_submission_answer.html" with answer=student_submission.answer answer_text_label="Your response to the question above:" %}
{% if allow_file_upload and file_url %}
<h3 class="submission__answer__display__title">
{% trans "Your Image" %}
</h3>
<div class="submission__answer__display__image">
<img class="submission--image" alt="{% trans "The image associated with your submission." %}" src="{{ file_url }}"/>
</div>
{% endif %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url header="Your Upload" class_prefix="submission__answer" %}
</article>
<article class="submission__peer-evaluations step__content__section">
......
{% spaceless %}
{% load i18n %}
{% if file_upload_type %}
{% if header %}
<header class="{{ class_prefix }}__display__header">
<h3 class="{{ class_prefix }}__display__title">
{% trans header %}
</h3>
</header>
{% endif %}
<div class="{{ class_prefix }}__display__file {% if not file_url %}is--hidden{% endif %}" id="submission__{{ file_upload_type }}__upload" data-upload-type="{{ file_upload_type }}">
{% if file_upload_type == "image" %}
<img id="submission__answer__file"
class="submission--image"
alt="{% trans "The image associated with this submission." %}"
src="{{ file_url }}" />
{% elif file_upload_type == "pdf-and-image" or file_upload_type == "custom" %}
<a href="{{ file_url }}" id="submission__answer__file" class="submission--file" target="_blank">
{% trans "View the file associated with this submission." %}
</a>
{% if show_warning %}
<p class="submission_file_warning">{% trans "(Caution: This file was uploaded by another course learner and has not been verified, screened, approved, reviewed or endorsed by edX. If you decide to access it, you do so at your own risk.)" %}</p>
{% endif %}
{% endif %}
</div>
{% endif %}
{% endspaceless %}
......@@ -68,17 +68,7 @@
{% include "openassessmentblock/oa_submission_answer.html" with answer=peer_submission.answer answer_text_label="Your peer's response to the question above:" %}
{% if allow_file_upload and peer_file_url %}
<header class="peer-assessment__display__header">
<h3 class="peer-assessment__display__title">
{% trans "Associated Image" %}
</h3>
</header>
<div class="peer-assessment__display__image">
<img class="submission--image" alt="{% trans "The image associated with your peer's submission." %}" src="{{ peer_file_url }}"/>
</div>
{% endif %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=peer_file_url header="Associated File" class_prefix="peer-assessment" show_warning="true" %}
</div>
<form id="peer-assessment--001__assessment" class="peer-assessment__assessment" method="post">
......
......@@ -52,17 +52,7 @@
{% include "openassessmentblock/oa_submission_answer.html" with answer=peer_submission.answer answer_text_label="Your peer's response to the question above:" %}
{% if allow_file_upload and peer_file_url %}
<header class="peer-assessment__display__header">
<h3 class="peer-assessment__display__title">
{% trans "Associated Image" %}
</h3>
</header>
<div class="peer-assessment__display__image">
<img class="submission--image" alt="{% trans "The image associated with your peer's submission." %}" src="{{ peer_file_url }}"/>
</div>
{% endif %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=peer_file_url header="Associated File" class_prefix="peer-assessment" show_warning="true" %}
</div>
<form id="peer-assessment--001__assessment" class="peer-assessment__assessment" method="post">
......
......@@ -72,29 +72,21 @@
</div>
</li>
{% endfor %}
{% if allow_file_upload %}
{% if file_upload_type %}
<li class="field">
<div id="upload__error">
<div class="message message--inline message--error message--error-server">
<h3 class="message__title">{% trans "We could not upload this image" %}</h3>
<h3 class="message__title">{% trans "We could not upload this file" %}</h3>
<div class="message__content"></div>
</div>
</div>
<label class="sr" for="submission__answer__upload">{% trans "Select an image to upload for this submission." %}</label>
<label class="sr" for="submission__answer__upload">{% trans "Select a file to upload for this submission." %}</label>
<input type="file" id="submission__answer__upload" class="file--upload">
<button type="submit" id="file__upload" class="action action--upload is--disabled">{% trans "Upload your image" %}</button>
</li>
<li>
<div class="submission__answer__display__image is--hidden">
<img id="submission__answer__image"
class="submission--image"
{% if file_url %}
alt="{% trans "The image associated with your submission." %}"
{% endif %}
src="{{ file_url }}"/>
</div>
<button type="submit" id="file__upload" class="action action--upload is--disabled">{% trans "Upload your file" %}</button>
</li>
{% endif %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url class_prefix="submission__answer"%}
</ol>
<span class="tip">{% trans "You may continue to work on your response until you submit it." %}</span>
......
......@@ -24,13 +24,7 @@
{% include "openassessmentblock/oa_submission_answer.html" with answer=student_submission.answer answer_text_label="Your response to the question above:" %}
{% if allow_file_upload and file_url %}
<h3 class="submission__answer__display__title">{% trans "Your Image" %}</h3>
<div class="submission__answer__display__image">
<img class="submission--image" alt="{% trans "The image associated with your submission." %}" src="{{ file_url }}"/>
</div>
{% endif %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url header="Your Uploaded File" class_prefix="submission__answer" %}
</article>
</div>
</div>
......
......@@ -44,13 +44,7 @@
{% include "openassessmentblock/oa_submission_answer.html" with answer=student_submission.answer answer_text_label="Your response to the question above:" %}
{% if allow_file_upload and file_url %}
<h3 class="submission__answer__display__title">{% trans "Your Image" %}</h3>
<div class="submission__answer__display__image">
<img class="submission--image" alt="{% trans "The image associated with your submission." %}" src="{{ file_url }}"/>
</div>
{% endif %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url header="Your Uploaded File" class_prefix="submission__answer" %}
</article>
</div>
</div>
......
......@@ -45,17 +45,7 @@
{% include "openassessmentblock/oa_submission_answer.html" with answer=self_submission.answer answer_text_label="Your response to the question above:" %}
{% if allow_file_upload and self_file_url %}
<header class="self-assessment__display__header">
<h3 class="self-assessment__display__title">
{% trans "Associated Image" %}
</h3>
</header>
<div class="self-assessment__display__image">
<img class="submission--image" alt="{% trans "The image associated with your submission." %}" src="{{ self_file_url }}"/>
</div>
{% endif %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=self_file_url header="Associated File" class_prefix="self-assessment" %}
</article>
<form id="self-assessment--001__assessment" class="self-assessment__assessment" method="post">
......
......@@ -25,12 +25,11 @@
{% endif %}
</div>
{% if submission.image_url %}
<img
class="submission--image"
alt="{% trans "The image associated with this response" %}"
src="{{ submission.image_url }}"
/>
{% if submission.file_url %}
<a href="{{ submission.file_url }}" class="submission--file">
{% trans "The file associated with this response." %}
</a>
<span>{% trans "Caution: This file was uploaded by another course learner and has not been verified, screened, approved, reviewed or endorsed by edX. If you decide to access it, you do so at your own risk." %}</span>
{% endif %}
</div>
</div>
......
......@@ -141,7 +141,7 @@ class GradeMixin(object):
'example_based_assessment': example_based_assessment,
'rubric_criteria': self._rubric_criteria_grade_context(peer_assessments, self_assessment),
'has_submitted_feedback': has_submitted_feedback,
'allow_file_upload': self.allow_file_upload,
'file_upload_type': self.file_upload_type,
'allow_latex': self.allow_latex,
'file_url': self.get_download_url_from_submission(student_submission)
}
......
......@@ -122,9 +122,21 @@ class OpenAssessmentBlock(
)
allow_file_upload = Boolean(
default=False,
default=None,
scope=Scope.content,
help="Do not use. For backwards compatibility only."
)
file_upload_type_raw = String(
default=None,
scope=Scope.content,
help="File upload allowed with submission."
help="File upload to be included with submission (can be 'image', 'pdf-and-image', or 'custom')."
)
white_listed_file_types = List(
default=[],
scope=Scope.content,
help="Custom list of file types allowed with submission."
)
allow_latex = Boolean(
......@@ -209,6 +221,46 @@ class OpenAssessmentBlock(
def course_id(self):
return self._serialize_opaque_key(self.xmodule_runtime.course_id) # pylint:disable=E1101
@property
def file_upload_type(self):
"""
Backward compatibility for existing block before the change from allow_file_upload to file_upload_type_raw.
This property will use new file_upload_type_raw field when available, otherwise will fall back to
allow_file_upload field for old blocks.
"""
if self.file_upload_type_raw is not None:
return self.file_upload_type_raw
if self.allow_file_upload:
return 'image'
else:
return None
@file_upload_type.setter
def file_upload_type(self, value):
"""
Setter for file_upload_type_raw
"""
self.file_upload_type_raw = value
@property
def white_listed_file_types_string(self):
"""
Join the white listed file types into comma delimited string
"""
if self.white_listed_file_types:
return ','.join(self.white_listed_file_types)
else:
return ''
@white_listed_file_types_string.setter
def white_listed_file_types_string(self, value):
"""
Convert comma delimited white list string into list with some clean up
"""
self.white_listed_file_types = [file_type.strip().strip('.').lower()
for file_type in value.split(',')] if value else None
def get_anonymous_user_id(self, username, course_id):
"""
Get the anonymous user id from Xblock user service.
......@@ -322,10 +374,15 @@ class OpenAssessmentBlock(
# TODO: load CSS and JavaScript as URLs once they can be served by the CDN
fragment.add_css(load(css_url))
fragment.add_javascript(load("static/js/openassessment-lms.min.js"))
fragment.initialize_js('OpenAssessmentBlock')
js_context_dict = {
"ALLOWED_IMAGE_MIME_TYPES": self.ALLOWED_IMAGE_MIME_TYPES,
"ALLOWED_FILE_MIME_TYPES": self.ALLOWED_FILE_MIME_TYPES,
"FILE_EXT_BLACK_LIST": self.FILE_EXT_BLACK_LIST,
"FILE_TYPE_WHITE_LIST": self.white_listed_file_types,
}
fragment.initialize_js('OpenAssessmentBlock', js_context_dict)
return fragment
@property
def is_admin(self):
"""
......@@ -410,6 +467,22 @@ class OpenAssessmentBlock(
"""
return [
(
"OpenAssessmentBlock File Upload: Images",
load('static/xml/file_upload_image_only.xml')
),
(
"OpenAssessmentBlock File Upload: PDF and Images",
load('static/xml/file_upload_pdf_and_image.xml')
),
(
"OpenAssessmentBlock File Upload: Custom File Types",
load('static/xml/file_upload_custom.xml')
),
(
"OpenAssessmentBlock File Upload: allow_file_upload compatibility",
load('static/xml/file_upload_compat.xml')
),
(
"OpenAssessmentBlock Unicode",
load('static/xml/unicode.xml')
),
......@@ -426,6 +499,10 @@ class OpenAssessmentBlock(
load('static/xml/leaderboard.xml')
),
(
"OpenAssessmentBlock Leaderboard with Custom File Type",
load('static/xml/leaderboard_custom.xml')
),
(
"OpenAssessmentBlock (Peer Only) Rubric",
load('static/xml/poverty_peer_only_example.xml')
),
......@@ -471,6 +548,8 @@ class OpenAssessmentBlock(
block.title = config['title']
block.prompts = config['prompts']
block.allow_file_upload = config['allow_file_upload']
block.file_upload_type = config['file_upload_type']
block.white_listed_file_types_string = config['white_listed_file_types']
block.allow_latex = config['allow_latex']
block.leaderboard_show = config['leaderboard_show']
......
......@@ -235,7 +235,7 @@ class PeerAssessmentMixin(object):
context_dict["peer_submission"] = create_submission_dict(peer_sub, self.prompts)
# Determine if file upload is supported for this XBlock.
context_dict["allow_file_upload"] = self.allow_file_upload
context_dict["file_upload_type"] = self.file_upload_type
context_dict["peer_file_url"] = self.get_download_url_from_submission(peer_sub)
else:
path = 'openassessmentblock/peer/oa_peer_turbo_mode_waiting.html'
......@@ -250,7 +250,7 @@ class PeerAssessmentMixin(object):
path = 'openassessmentblock/peer/oa_peer_assessment.html'
context_dict["peer_submission"] = create_submission_dict(peer_sub, self.prompts)
# Determine if file upload is supported for this XBlock.
context_dict["allow_file_upload"] = self.allow_file_upload
context_dict["file_upload_type"] = self.file_upload_type
context_dict["peer_file_url"] = self.get_download_url_from_submission(peer_sub)
# Sets the XBlock boolean to signal to Message that it WAS NOT able to grab a submission
self.no_peers = False
......
......@@ -63,6 +63,12 @@ VALID_ASSESSMENT_TYPES = [
u'student-training'
]
VALID_UPLOAD_FILE_TYPES = [
u'',
u'image',
u'pdf-and-image',
u'custom'
]
# Schema definition for an update from the Studio JavaScript editor.
EDITOR_UPDATE_SCHEMA = Schema({
......@@ -76,7 +82,12 @@ EDITOR_UPDATE_SCHEMA = Schema({
Required('feedback_default_text'): utf8_validator,
Required('submission_start'): Any(datetime_validator, None),
Required('submission_due'): Any(datetime_validator, None),
Required('allow_file_upload'): bool,
'allow_file_upload': bool, # Backwards compatibility.
Required('file_upload_type', default=None): Any(
All(utf8_validator, In(VALID_UPLOAD_FILE_TYPES)),
None
),
'white_listed_file_types': utf8_validator,
Required('allow_latex'): bool,
Required('leaderboard_show'): int,
Required('assessments'): [
......
......@@ -90,8 +90,8 @@ class SelfAssessmentMixin(object):
context["estimated_time"] = "20 minutes" # TODO: Need to configure this.
context["self_submission"] = create_submission_dict(submission, self.prompts)
# Determine if file upload is supported for this XBlock.
context["allow_file_upload"] = self.allow_file_upload
# Determine if file upload is supported for this XBlock and what kind of files can be uploaded.
context["file_upload_type"] = self.file_upload_type
context['self_file_url'] = self.get_download_url_from_submission(submission)
path = 'openassessmentblock/self/oa_self_assessment.html'
......
......@@ -253,7 +253,7 @@ class StaffAreaMixin(object):
file_key = submission['answer']['file_key']
try:
submission['image_url'] = file_api.get_download_url(file_key)
submission['file_url'] = file_api.get_download_url(file_key)
except file_exceptions.FileUploadError:
# Log the error, but do not prevent the rest of the student info
# from being displayed.
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -5,6 +5,23 @@ Tests for OpenAssessment response (submission) view.
describe("OpenAssessment.ResponseView", function() {
var FAKE_URL = "http://www.example.com";
var ALLOWED_IMAGE_MIME_TYPES = [
'image/gif',
'image/jpeg',
'image/pjpeg',
'image/png',
];
var ALLOWED_FILE_MIME_TYPES = [
'application/pdf',
'image/gif',
'image/jpeg',
'image/pjpeg',
'image/png',
];
var FILE_TYPE_WHITE_LIST = ['pdf', 'doc', 'docx', 'html'];
var FILE_EXT_BLACK_LIST = ['exe', 'msi', 'app', 'dmg'];
var StubServer = function() {
......@@ -75,6 +92,7 @@ describe("OpenAssessment.ResponseView", function() {
var baseView = null;
var server = null;
var fileUploader = null;
var data = null;
// View under test
var view = null;
......@@ -103,10 +121,16 @@ describe("OpenAssessment.ResponseView", function() {
server.renderLatex = jasmine.createSpy('renderLatex');
fileUploader = new StubFileUploader();
baseView = new StubBaseView();
data = {
"ALLOWED_IMAGE_MIME_TYPES": ALLOWED_IMAGE_MIME_TYPES,
"ALLOWED_FILE_MIME_TYPES": ALLOWED_FILE_MIME_TYPES,
"FILE_TYPE_WHITE_LIST": FILE_TYPE_WHITE_LIST,
"FILE_EXT_BLACK_LIST": FILE_EXT_BLACK_LIST
};
// Create and install the view
var el = $('#openassessment-base').get(0);
view = new OpenAssessment.ResponseView(el, server, fileUploader, baseView);
view = new OpenAssessment.ResponseView(el, server, fileUploader, baseView, data);
view.installHandlers();
// Stub the confirmation step
......@@ -419,21 +443,59 @@ describe("OpenAssessment.ResponseView", function() {
it("selects too large of a file", function() {
spyOn(baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'image/jpg', size: 6000000, name: 'huge-picture.jpg', data: ''}];
view.prepareUpload(files);
var files = [{type: 'image/jpeg', size: 6000000, name: 'huge-picture.jpg', data: ''}];
view.prepareUpload(files, 'image');
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File size must be 5MB or less.');
});
it("selects the wrong file type", function() {
it("selects the wrong image file type", function() {
spyOn(baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'image/jpg', size: 1024, name: 'picture.exe', data: ''}];
view.prepareUpload(files, 'image');
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'You can upload files with these file types: JPG, PNG or GIF');
});
it("selects the wrong pdf or image file type", function() {
spyOn(baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}];
view.prepareUpload(files, 'pdf-and-image');
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'You can upload files with these file types: JPG, PNG, GIF or PDF');
});
it("selects the wrong file extension", function() {
spyOn(baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}];
view.prepareUpload(files, 'custom');
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'You can upload files with these file types: pdf, doc, docx, html');
});
it("submits a file with extension in the black list", function() {
spyOn(baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'bogus/jpg', size: 1024, name: 'picture.exe', data: ''}];
view.prepareUpload(files);
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File must be an image.');
view.data.FILE_TYPE_WHITE_LIST = ['exe'];
var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}];
view.prepareUpload(files, 'custom');
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File type is not allowed.');
});
it("uploads an image using a one-time URL", function() {
var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files, 'image');
view.fileUpload();
expect(fileUploader.uploadArgs.url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs.data).toEqual(files[0]);
});
it("uploads a PDF using a one-time URL", function() {
var files = [{type: 'application/pdf', size: 1024, name: 'application.pdf', data: ''}];
view.prepareUpload(files, 'pdf-and-image');
view.fileUpload();
expect(fileUploader.uploadArgs.url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs.data).toEqual(files[0]);
});
it("uploads a file using a one-time URL", function() {
var files = [{type: 'image/jpg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files);
it("uploads a arbitrary type file using a one-time URL", function() {
var files = [{type: 'text/html', size: 1024, name: 'index.html', data: ''}];
view.prepareUpload(files, 'custom');
view.fileUpload();
expect(fileUploader.uploadArgs.url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs.data).toEqual(files[0]);
......@@ -445,8 +507,8 @@ describe("OpenAssessment.ResponseView", function() {
spyOn(baseView, 'toggleActionError').and.callThrough();
// Attempt to upload a file
var files = [{type: 'image/jpg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files);
var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files, 'image');
view.fileUpload();
// Expect an error to be displayed
......@@ -459,8 +521,8 @@ describe("OpenAssessment.ResponseView", function() {
spyOn(baseView, 'toggleActionError').and.callThrough();
// Attempt to upload a file
var files = [{type: 'image/jpg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files);
var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files, 'image');
view.fileUpload();
// Expect an error to be displayed
......
......@@ -271,7 +271,9 @@ describe("OpenAssessment.Server", function() {
criteria: CRITERIA,
assessments: ASSESSMENTS,
editorAssessmentsOrder: EDITOR_ASSESSMENTS_ORDER,
imageSubmissionEnabled: true,
fileUploadType: "image",
fileTypeWhiteList: ['pdf', 'doc'],
latexEnabled: true,
leaderboardNum: 15
});
expect($.ajax).toHaveBeenCalledWith({
......@@ -286,7 +288,9 @@ describe("OpenAssessment.Server", function() {
criteria: CRITERIA,
assessments: ASSESSMENTS,
editor_assessments_order: EDITOR_ASSESSMENTS_ORDER,
allow_file_upload: true,
file_upload_type: "image",
white_listed_file_types: ['pdf', 'doc'],
allow_latex: true,
leaderboard_show: 15
}),
contentType : jsonContentType
......
......@@ -40,6 +40,7 @@ describe("OpenAssessment.StudioView", function() {
var server = null;
var view = null;
var data = null;
var EXPECTED_SERVER_DATA = {
title: "The most important of all questions.",
......@@ -47,7 +48,7 @@ describe("OpenAssessment.StudioView", function() {
feedbackPrompt: "",
submissionStart: "2014-01-02T12:15",
submissionDue: "2014-10-01T04:53",
imageSubmissionEnabled: false,
fileUploadType: "",
leaderboardNum: 12,
criteria: [
{
......@@ -126,13 +127,17 @@ describe("OpenAssessment.StudioView", function() {
// Create the stub server
server = new StubServer();
// mock data sent from backend
data = {
FILE_EXT_BLACK_LIST: ['exe','app']
};
// Mock the runtime
spyOn(runtime, 'notify');
// Create the object under test
var el = $('#openassessment-editor').get(0);
view = new OpenAssessment.StudioView(runtime, el, server);
view = new OpenAssessment.StudioView(runtime, el, server, data);
});
it("sends the editor context to the server", function() {
......@@ -149,7 +154,7 @@ describe("OpenAssessment.StudioView", function() {
expect(server.receivedData.feedbackPrompt).toEqual(EXPECTED_SERVER_DATA.feedbackPrompt);
expect(server.receivedData.submissionStart).toEqual(EXPECTED_SERVER_DATA.submissionStart);
expect(server.receivedData.submissionDue).toEqual(EXPECTED_SERVER_DATA.submissionDue);
expect(server.receivedData.imageSubmissionEnabled).toEqual(EXPECTED_SERVER_DATA.imageSubmissionEnabled);
expect(server.receivedData.fileUploadType).toEqual(EXPECTED_SERVER_DATA.fileUploadType);
expect(server.receivedData.leaderboardNum).toEqual(EXPECTED_SERVER_DATA.leaderboardNum);
// Criteria
......
var StubNotifier = function() {
this.receivedNotifications = [];
this.notificationFired = function(name, data) {
this.receivedNotifications.push({
name: name,
data: data
});
};
};
describe("OpenAssessment.DatetimeControl", function() {
var datetimeControl = null;
......@@ -86,16 +96,6 @@ describe("OpenAssessment.DatetimeControl", function() {
describe("OpenAssessment.ToggleControl", function() {
var StubNotifier = function() {
this.receivedNotifications = [];
this.notificationFired = function(name, data) {
this.receivedNotifications.push({
name: name,
data: data
});
};
};
var notifier = null;
var toggleControl = null;
......@@ -159,3 +159,137 @@ describe("OpenAssessment.ToggleControl", function() {
});
});
describe("OpenAssessment.SelectControl", function() {
var notifier = null;
var selectControl = null;
beforeEach(function() {
setFixtures(
'<div id="toggle_test"> \
<div id="shown_for_option1" /> \
<div id="shown_for_option2" class="is--hidden"/> \
</div> \
<select id="select"> \
<option value="1">1</option> \
<option value="2">2</option> \
</select>'
);
notifier = new StubNotifier();
selectControl = new OpenAssessment.SelectControl(
$("#select"),
{'1': $("#shown_for_option1"), '2': $("#shown_for_option2")},
notifier
).install();
});
it("shows and hides elements", function() {
var assertIsVisible = function(selected) {
$.each(selectControl.mapping, function(option, sel) {
expect(sel.hasClass('is--hidden')).toBe(option != selected);
});
};
// Initially, the section is visible (default from the fixture)
assertIsVisible(1);
// Simulate select the option, hiding the section 2
selectControl.select.val(2).change();
assertIsVisible(2);
// Click it again, hiding section 1
selectControl.select.val(1).change();
assertIsVisible(1);
});
it("fires notifications", function() {
selectControl.select.val(1).change();
expect(notifier.receivedNotifications).toContain({
name: "selectionChanged",
data: {selected: "1"}
});
selectControl.select.val(2).change();
expect(notifier.receivedNotifications).toContain({
name: "selectionChanged",
data: {selected: "2"}
});
selectControl.select.val(1).change();
expect(notifier.receivedNotifications).toContain({
name: "selectionChanged",
data: {selected: "1"}
});
});
});
describe("OpenAssessment.InputControl", function() {
var inputControl = null;
var validator = jasmine.createSpy('validator');
beforeEach(function() {
setFixtures(
'<div><input type="text" id="input"></div><p class="message-status error" id="error"></p>'
);
inputControl = new OpenAssessment.InputControl($("#input"), validator);
});
it("should call validator function when validate is called", function() {
validator.and.returnValue([]);
inputControl.set('test');
inputControl.validate();
expect(validator).toHaveBeenCalledWith('test');
});
it("should return true when validate is called and there is no error", function() {
validator.and.returnValue([]);
inputControl.set('test');
var isValid = inputControl.validate();
expect(isValid).toBe(true);
});
it("should return false when validate is called and there is an error", function() {
validator.and.returnValue(['error']);
inputControl.set('error input');
var isValid = inputControl.validate();
expect(isValid).toBe(false);
});
it("should show the error message when validate is called and there is an error", function() {
validator.and.returnValue(['error']);
inputControl.set('error input');
inputControl.validate();
expect(inputControl.input.hasClass("openassessment_highlighted_field")).toBe(true);
expect(inputControl.input.parent().nextAll('.message-status').hasClass("is-shown")).toBe(true);
});
it("should clear the errors when clearValidationErrors is called", function() {
validator.and.returnValue(['error']);
inputControl.set('error input');
inputControl.validate();
inputControl.clearValidationErrors();
expect(inputControl.input.hasClass("openassessment_highlighted_field")).toBe(false);
expect(inputControl.input.parent().nextAll('.message-status').hasClass("is-shown")).toBe(false);
});
it("should return errors generated by validator when validationErrors is called", function() {
var errors = ['error1', 'error2'];
validator.and.returnValue(errors);
inputControl.set('error input');
inputControl.validate();
expect(inputControl.validationErrors()).toEqual(errors);
})
});
......@@ -44,6 +44,7 @@ describe("OpenAssessment.EditSettingsView", function() {
var view = null;
var assessmentViews = null;
var data = null;
// The Peer and Self Editor ID's
var PEER = "oa_peer_assessment_editor";
......@@ -62,9 +63,14 @@ describe("OpenAssessment.EditSettingsView", function() {
assessmentViews[AI] = new StubView("ai-assessment", "Example Based assessment description");
assessmentViews[TRAINING] = new StubView("student-training", "Student Training description");
// mock data from backend
data = {
FILE_EXT_BLACK_LIST: ['exe','app']
};
// Create the view
var element = $("#oa_basic_settings_editor").get(0);
view = new OpenAssessment.EditSettingsView(element, assessmentViews);
view = new OpenAssessment.EditSettingsView(element, assessmentViews, data);
view.submissionStart("2014-01-01", "00:00");
view.submissionDue("2014-03-04", "00:00");
});
......@@ -84,11 +90,23 @@ describe("OpenAssessment.EditSettingsView", function() {
expect(view.submissionDue()).toEqual("2014-05-02T12:34");
});
it("sets and loads the image enabled state", function() {
view.imageSubmissionEnabled(true);
expect(view.imageSubmissionEnabled()).toBe(true);
view.imageSubmissionEnabled(false);
expect(view.imageSubmissionEnabled()).toBe(false);
it("sets and loads the file upload state", function() {
view.fileUploadType('image');
expect(view.fileUploadType()).toBe('image');
view.fileUploadType('pdf-and-image');
expect(view.fileUploadType()).toBe('pdf-and-image');
view.fileUploadType('custom');
expect(view.fileUploadType()).toBe('custom');
view.fileUploadType('');
expect(view.fileUploadType()).toBe('');
});
it("sets and loads the file type white list", function() {
view.fileTypeWhiteList('pdf,gif,png,doc');
expect(view.fileTypeWhiteList()).toBe('pdf,gif,png,doc');
view.fileTypeWhiteList('');
expect(view.fileTypeWhiteList()).toBe('');
});
it("sets and loads the leaderboard number", function() {
......@@ -219,4 +237,21 @@ describe("OpenAssessment.EditSettingsView", function() {
// to mark anything as invalid
expect(assessmentViews[PEER].validate).not.toHaveBeenCalled();
});
it("validates file upload type and white list fields", function() {
view.fileUploadType("image");
expect(view.validate()).toBe(true);
expect(view.validationErrors().length).toBe(0);
// expect white list field is not empty when upload type is custom
view.fileUploadType("custom");
expect(view.validate()).toBe(false);
expect(view.validationErrors()).toContain('File types can not be empty.');
// expect white list field doesn't contain black listed exts
view.fileUploadType("custom");
view.fileTypeWhiteList("pdf, EXE, .app");
expect(view.validate()).toBe(false);
expect(view.validationErrors()).toContain('The following file types are not allowed: exe,app');
})
});
......@@ -5,17 +5,18 @@ Args:
runtime (Runtime): an XBlock runtime instance.
element (DOM element): The DOM element representing this XBlock.
server (OpenAssessment.Server): The interface to the XBlock server.
data (Object): The data object passed from XBlock backend.
Returns:
OpenAssessment.BaseView
**/
OpenAssessment.BaseView = function(runtime, element, server) {
OpenAssessment.BaseView = function(runtime, element, server, data) {
this.runtime = runtime;
this.element = element;
this.server = server;
this.fileUploader = new OpenAssessment.FileUploader();
this.responseView = new OpenAssessment.ResponseView(this.element, this.server, this.fileUploader, this);
this.responseView = new OpenAssessment.ResponseView(this.element, this.server, this.fileUploader, this, data);
this.trainingView = new OpenAssessment.StudentTrainingView(this.element, this.server, this);
this.selfView = new OpenAssessment.SelfView(this.element, this.server, this);
this.peerView = new OpenAssessment.PeerView(this.element, this.server, this);
......@@ -151,11 +152,11 @@ OpenAssessment.BaseView.prototype = {
/* XBlock JavaScript entry point for OpenAssessmentXBlock. */
/* jshint unused:false */
function OpenAssessmentBlock(runtime, element) {
function OpenAssessmentBlock(runtime, element, data) {
/**
Render views within the base view on page load.
**/
var server = new OpenAssessment.Server(runtime, element);
var view = new OpenAssessment.BaseView(runtime, element, server);
var view = new OpenAssessment.BaseView(runtime, element, server, data);
view.load();
}
......@@ -4,22 +4,26 @@ Interface for response (submission) view.
Args:
element (DOM element): The DOM element representing the XBlock.
server (OpenAssessment.Server): The interface to the XBlock server.
fileUploader (OpenAssessment.FileUploader): File uploader instance.
baseView (OpenAssessment.BaseView): Container view.
data (Object): The data object passed from XBlock backend.
Returns:
OpenAssessment.ResponseView
**/
OpenAssessment.ResponseView = function(element, server, fileUploader, baseView) {
OpenAssessment.ResponseView = function(element, server, fileUploader, baseView, data) {
this.element = element;
this.server = server;
this.fileUploader = fileUploader;
this.baseView = baseView;
this.savedResponse = [];
this.files = null;
this.imageType = null;
this.fileType = null;
this.lastChangeTime = Date.now();
this.errorOnLastSave = false;
this.autoSaveTimerId = null;
this.data = data;
this.fileUploaded = false;
};
......@@ -59,6 +63,10 @@ OpenAssessment.ResponseView.prototype = {
installHandlers: function() {
var sel = $('#openassessment__response', this.element);
var view = this;
var uploadType = '';
if (sel.find('.submission__answer__display__file').length) {
uploadType = sel.find('.submission__answer__display__file').data('upload-type');
}
// Install a click handler for collapse/expand
this.baseView.setUpCollapseExpand(sel);
......@@ -68,9 +76,9 @@ OpenAssessment.ResponseView.prototype = {
var handleChange = function() { view.handleResponseChanged(); };
sel.find('.submission__answer__part__text__value').on('change keyup drop paste', handleChange);
var handlePrepareUpload = function(eventData) { view.prepareUpload(eventData.target.files); };
var handlePrepareUpload = function(eventData) { view.prepareUpload(eventData.target.files, uploadType); };
sel.find('input[type=file]').on('change', handlePrepareUpload);
// keep the preview as display none at first
// keep the preview as display none at first
sel.find('#submission__preview__item').hide();
// Install a click handler for submission
......@@ -111,7 +119,7 @@ OpenAssessment.ResponseView.prototype = {
function(eventObject) {
// Override default form submission
eventObject.preventDefault();
$('.submission__answer__display__image', view.element).removeClass('is--hidden');
$('.submission__answer__display__file', view.element).removeClass('is--hidden');
view.fileUpload();
}
);
......@@ -396,19 +404,36 @@ OpenAssessment.ResponseView.prototype = {
var view = this;
var baseView = this.baseView;
var fileDefer = $.Deferred();
// check if there is a file selected but not uploaded yet
if (view.files !== null && !view.fileUploaded) {
var msg = gettext('Do you want to upload your file before submitting?');
if(confirm(msg)) {
fileDefer = view.fileUpload();
} else {
view.submitEnabled(true);
return;
}
} else {
fileDefer.resolve();
}
this.confirmSubmission()
// On confirmation, send the submission to the server
// The callback returns a promise so we can attach
// additional callbacks after the confirmation.
// NOTE: in JQuery >=1.8, `pipe()` is deprecated in favor of `then()`,
// but we're using JQuery 1.7 in the LMS, so for now we're stuck with `pipe()`.
fileDefer
.pipe(function() {
var submission = view.response();
baseView.toggleActionError('response', null);
// Send the submission to the server, returning the promise.
return view.server.submit(submission);
return view.confirmSubmission()
// On confirmation, send the submission to the server
// The callback returns a promise so we can attach
// additional callbacks after the confirmation.
// NOTE: in JQuery >=1.8, `pipe()` is deprecated in favor of `then()`,
// but we're using JQuery 1.7 in the LMS, so for now we're stuck with `pipe()`.
.pipe(function() {
var submission = view.response();
baseView.toggleActionError('response', null);
// Send the submission to the server, returning the promise.
return view.server.submit(submission);
});
})
// If the submission was submitted successfully, move to the next step
......@@ -463,25 +488,46 @@ OpenAssessment.ResponseView.prototype = {
/**
When selecting a file for upload, do some quick client-side validation
to ensure that it is an image, and is not larger than the maximum file
size.
to ensure that it is an image, a PDF or other allowed types, and is not
larger than the maximum file size.
Args:
files (list): A collection of files used for upload. This function assumes
there is only one file being uploaded at any time. This file must
be less than 5 MB and an image.
be less than 5 MB and an image, PDF or other allowed types.
uploadType (string): uploaded file type allowed, could be none, image,
file or custom.
**/
prepareUpload: function(files) {
prepareUpload: function(files, uploadType) {
this.files = null;
this.imageType = files[0].type;
this.fileType = files[0].type;
var ext = files[0].name.split('.').pop().toLowerCase();
if (files[0].size > this.MAX_FILE_SIZE) {
this.baseView.toggleActionError(
'upload', gettext("File size must be 5MB or less.")
'upload',
gettext("File size must be 5MB or less.")
);
} else if (this.imageType.substring(0,6) !== 'image/') {
} else if (uploadType === "image" && this.data.ALLOWED_IMAGE_MIME_TYPES.indexOf(this.fileType) === -1) {
this.baseView.toggleActionError(
'upload', gettext("File must be an image.")
'upload',
gettext("You can upload files with these file types: ") + "JPG, PNG or GIF"
);
} else if (uploadType === "pdf-and-image" && this.data.ALLOWED_FILE_MIME_TYPES.indexOf(this.fileType) === -1) {
this.baseView.toggleActionError(
'upload',
gettext("You can upload files with these file types: ") + "JPG, PNG, GIF or PDF"
);
} else if (uploadType === "custom" && this.data.FILE_TYPE_WHITE_LIST.indexOf(ext) === -1) {
this.baseView.toggleActionError(
'upload',
gettext("You can upload files with these file types: ") + this.data.FILE_TYPE_WHITE_LIST.join(", ")
);
} else if (this.data.FILE_EXT_BLACK_LIST.indexOf(ext) !== -1) {
this.baseView.toggleActionError(
'upload',
gettext("File type is not allowed.")
);
} else {
this.baseView.toggleActionError('upload', null);
......@@ -511,13 +557,14 @@ OpenAssessment.ResponseView.prototype = {
// completed, execute a sequential AJAX call to upload to the returned
// URL. This request requires appropriate CORS configuration for AJAX
// PUT requests on the server.
this.server.getUploadUrl(view.imageType).done(
return this.server.getUploadUrl(view.fileType, view.files[0].name).done(
function(url) {
var image = view.files[0];
view.fileUploader.upload(url, image)
var file = view.files[0];
view.fileUploader.upload(url, file)
.done(function() {
view.imageUrl();
view.fileUrl();
view.baseView.toggleActionError('upload', null);
view.fileUploaded = true;
})
.fail(handleError);
}
......@@ -525,13 +572,17 @@ OpenAssessment.ResponseView.prototype = {
},
/**
Set the image URL, or retrieve it.
Set the file URL, or retrieve it.
**/
imageUrl: function() {
fileUrl: function() {
var view = this;
var image = $('#submission__answer__image', view.element);
var file = $('#submission__answer__file', view.element);
view.server.getDownloadUrl().done(function(url) {
image.attr('src', url);
if (file.prop("tagName") === "IMG") {
file.attr('src', url);
} else {
file.attr('href', url);
}
return url;
});
}
......
......@@ -436,7 +436,9 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
submissionDue (ISO-formatted datetime string or null): The date the submission is due.
criteria (list of object literals): The rubric criteria.
assessments (list of object literals): The assessments the student will be evaluated on.
imageSubmissionEnabled (boolean): TRUE if image attachments are allowed.
fileUploadType (string): 'image' if image attachments are allowed, 'pdf-and-image' if pdf and
image attachments are allowed, 'custom' if file type is restricted by a white list.
fileTypeWhiteList (string): Comma separated file type white list
latexEnabled: TRUE if latex rendering is enabled.
leaderboardNum (int): The number of scores to show in the leaderboard.
......@@ -457,7 +459,8 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
criteria: kwargs.criteria,
assessments: kwargs.assessments,
editor_assessments_order: kwargs.editorAssessmentsOrder,
allow_file_upload: kwargs.imageSubmissionEnabled,
file_upload_type: kwargs.fileUploadType,
white_listed_file_types: kwargs.fileTypeWhiteList,
allow_latex: kwargs.latexEnabled,
leaderboard_show: kwargs.leaderboardNum
});
......@@ -509,19 +512,20 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
Args:
contentType (str): The Content Type for the file being uploaded.
filename (str): The name of the file to be uploaded.
Returns:
A presigned upload URL from the specified service used for uploading
files.
**/
getUploadUrl: function(contentType) {
getUploadUrl: function(contentType, filename) {
var url = this.url('upload_url');
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify({contentType: contentType}),
data: JSON.stringify({contentType: contentType, filename: filename}),
contentType: jsonContentType
}).done(function(data) {
if (data.success) { defer.resolve(data.url); }
......
......@@ -6,15 +6,17 @@
runtime (Runtime): an XBlock runtime instance.
element (DOM element): The DOM element representing this XBlock.
server (OpenAssessment.Server): The interface to the XBlock server.
data (Object literal): The data object passed from XBlock backend.
Returns:
OpenAssessment.StudioView
**/
OpenAssessment.StudioView = function(runtime, element, server) {
OpenAssessment.StudioView = function(runtime, element, server, data) {
this.element = element;
this.runtime = runtime;
this.server = server;
this.data = data;
// Resize the editing modal
this.fixModalHeight();
......@@ -55,7 +57,7 @@ OpenAssessment.StudioView = function(runtime, element, server) {
assessmentLookupDictionary[exampleBasedAssessmentView.getID()] = exampleBasedAssessmentView;
this.settingsView = new OpenAssessment.EditSettingsView(
$("#oa_basic_settings_editor", this.element).get(0), assessmentLookupDictionary
$("#oa_basic_settings_editor", this.element).get(0), assessmentLookupDictionary, data
);
// Initialize the rubric tab view
......@@ -198,7 +200,8 @@ OpenAssessment.StudioView.prototype = {
submissionStart: view.settingsView.submissionStart(),
submissionDue: view.settingsView.submissionDue(),
assessments: view.settingsView.assessmentsDescription(),
imageSubmissionEnabled: view.settingsView.imageSubmissionEnabled(),
fileUploadType: view.settingsView.fileUploadType(),
fileTypeWhiteList: view.settingsView.fileTypeWhiteList(),
latexEnabled: view.settingsView.latexEnabled(),
leaderboardNum: view.settingsView.leaderboardNum(),
editorAssessmentsOrder: view.settingsView.editorAssessmentsOrder()
......@@ -274,11 +277,11 @@ OpenAssessment.StudioView.prototype = {
/* XBlock entry point for Studio view */
/* jshint unused:false */
function OpenAssessmentEditor(runtime, element) {
function OpenAssessmentEditor(runtime, element, data) {
/**
Initialize the editing interface on page load.
**/
var server = new OpenAssessment.Server(runtime, element);
new OpenAssessment.StudioView(runtime, element, server);
new OpenAssessment.StudioView(runtime, element, server, data);
}
......@@ -280,4 +280,137 @@ OpenAssessment.DatetimeControl.prototype = {
return errors;
},
};
\ No newline at end of file
};
/**
Show and hide elements based on select options.
Args:
selectSel (JQuery selector): The select used to toggle whether sections
are shown or hidden.
mapping (Object): A mapping object that is used to specify the relationship
between option and section. e.g.
{
option1: selector1,
option2: selector2,
}
When an option is selected, the section is shown and all other sections will be hidden.
notifier (OpenAssessment.Notifier): Receives notifications when the select state changes.
Sends the following notifications:
* selectionChanged
**/
OpenAssessment.SelectControl = function(selectSel, mapping, notifier) {
this.select = selectSel;
this.mapping = mapping;
this.notifier = notifier;
};
OpenAssessment.SelectControl.prototype = {
/**
Install the event handler for the select,
passing in the toggle control object as the event data.
Returns:
OpenAssessment.ToggleControl
**/
install: function() {
this.select.change(
this, function(event) {
var control = event.data;
control.notifier.notificationFired('selectionChanged', {selected: this.value});
control.change(this.value);
}
);
return this;
},
change: function(selected) {
$.each(this.mapping, function(option, sel) {
if (option === selected) {
sel.removeClass('is--hidden');
} else {
sel.addClass('is--hidden');
}
});
}
};
/**
Input field that support custom validation.
This is similar to string field but allow you to pass in a custom validation function to validate the input field.
Args:
inputSel (JQuery selector or DOM element): The input field.
validator (callable): The callback for custom validation function. The function should accept
one parameter for the value of the input and returns an array of errors strings. If not error, return [].
*/
OpenAssessment.InputControl = function(inputSel, validator) {
this.input = $(inputSel);
this.validator = validator;
this.errors = [];
};
OpenAssessment.InputControl.prototype = {
/**
Retrieve the string value from the input.
Returns:
string
**/
get: function() {
return this.input.val();
},
/**
Set the input value.
Args:
val (string)
**/
set: function(val) {
this.input.val(val);
},
/**
Mark validation errors if the field does not pass the validation callback function.
Returns:
Boolean indicating whether the field's value is valid.
**/
validate: function() {
this.errors = this.validator(this.get());
if (this.errors.length) {
this.input.addClass("openassessment_highlighted_field");
this.input.parent().nextAll('.message-status').text(this.errors.join(";"));
this.input.parent().nextAll('.message-status').addClass("is-shown");
}
return this.errors.length === 0;
},
/**
Clear any validation errors from the UI.
**/
clearValidationErrors: function() {
this.input.removeClass("openassessment_highlighted_field");
this.input.parent().nextAll('.message-status').removeClass("is-shown");
},
/**
Return a list of validation errors currently displayed
in the UI.
Returns:
list of strings that contain error messages
**/
validationErrors: function() {
return this.errors;
}
};
......@@ -4,12 +4,13 @@ Editing interface for OpenAssessment settings (including assessments).
Args:
element (DOM element): The DOM element representing this view.
assessmentViews (object literal): Mapping of CSS IDs to view objects.
data (Object literal): The data object passed from XBlock backend.
Returns:
OpenAssessment.EditSettingsView
**/
OpenAssessment.EditSettingsView = function(element, assessmentViews) {
OpenAssessment.EditSettingsView = function(element, assessmentViews, data) {
this.settingsElement = element;
this.assessmentsElement = $(element).siblings('#openassessment_assessment_module_settings_editors').get(0);
this.assessmentViews = assessmentViews;
......@@ -27,11 +28,42 @@ OpenAssessment.EditSettingsView = function(element, assessmentViews) {
"#openassessment_submission_due_time"
).install();
new OpenAssessment.SelectControl(
$("#openassessment_submission_upload_selector", this.element),
{'custom': $("#openassessment_submission_white_listed_file_types_wrapper", this.element)},
new OpenAssessment.Notifier([
new OpenAssessment.AssessmentToggleListener()
])
).install();
this.leaderboardIntField = new OpenAssessment.IntField(
$("#openassessment_leaderboard_editor", this.element),
{ min: 0, max: 100 }
);
this.fileTypeWhiteListInputField = new OpenAssessment.InputControl(
$("#openassessment_submission_white_listed_file_types", this.element),
function(value) {
var badExts = [];
var errors = [];
if (!value) {
errors.push(gettext('File types can not be empty.'));
return errors;
}
var whiteList = $.map(value.replace(/\./g, '').toLowerCase().split(','), $.trim);
$.each(whiteList, function(index, ext) {
if (data.FILE_EXT_BLACK_LIST.indexOf(ext) !== -1) {
badExts.push(ext);
}
});
if (badExts.length) {
errors.push(gettext('The following file types are not allowed: ') + badExts.join(','));
}
return errors;
}
);
this.initializeSortableAssessments();
};
......@@ -122,28 +154,43 @@ OpenAssessment.EditSettingsView.prototype = {
},
/**
Enable / disable image submission.
Get or set upload file type.
Args:
isEnabled (boolean, optional): If provided, enable/disable image submission.
uploadType (string, optional): If provided, enable specified upload type submission.
Returns:
boolean
string (image, file or custom)
**/
imageSubmissionEnabled: function(isEnabled) {
var sel = $("#openassessment_submission_image_editor", this.settingsElement);
if (isEnabled !== undefined) {
if (isEnabled) { sel.val("1"); }
else { sel.val("0"); }
fileUploadType: function(uploadType) {
var sel = $("#openassessment_submission_upload_selector", this.settingsElement);
if (uploadType !== undefined) {
sel.val(uploadType);
}
return sel.val() === "1";
return sel.val();
},
/**
Get or set upload file extension white list.
Args:
exts (string, optional): If provided, set the file extension white list
Returns:
string: comma separated file extension white list string
**/
fileTypeWhiteList: function(exts) {
if (exts !== undefined) {
this.fileTypeWhiteListInputField.set(exts);
}
return this.fileTypeWhiteListInputField.get();
},
/**
Enable / disable latex rendering.
Args:
Args:
isEnabled(boolean, optional): if provided enable/disable latex rendering
Returns:
boolean
......@@ -255,6 +302,15 @@ OpenAssessment.EditSettingsView.prototype = {
isValid = (this.startDatetimeControl.validate() && isValid);
isValid = (this.dueDatetimeControl.validate() && isValid);
isValid = (this.leaderboardIntField.validate() && isValid);
if (this.fileUploadType() === 'custom') {
isValid = (this.fileTypeWhiteListInputField.validate() && isValid);
} else {
// we want to keep the valid white list in case author changes upload type back to custom
if (this.fileTypeWhiteListInputField.get() && !this.fileTypeWhiteListInputField.validate()) {
// but will clear the field in case it is invalid
this.fileTypeWhiteListInputField.set('');
}
}
// Validate each of the *enabled* assessment views
$.each(this.assessmentViews, function() {
......@@ -286,6 +342,9 @@ OpenAssessment.EditSettingsView.prototype = {
if (this.leaderboardIntField.validationErrors().length > 0) {
errors.push("Leaderboard number is invalid");
}
if (this.fileTypeWhiteListInputField.validationErrors().length > 0) {
errors = errors.concat(this.fileTypeWhiteListInputField.validationErrors());
}
$.each(this.assessmentViews, function() {
errors = errors.concat(this.validationErrors());
......@@ -301,8 +360,9 @@ OpenAssessment.EditSettingsView.prototype = {
this.startDatetimeControl.clearValidationErrors();
this.dueDatetimeControl.clearValidationErrors();
this.leaderboardIntField.clearValidationErrors();
this.fileTypeWhiteListInputField.clearValidationErrors();
$.each(this.assessmentViews, function() {
this.clearValidationErrors();
});
},
};
\ No newline at end of file
};
<openassessment allow_file_upload="true" submission_due="2035-03-11T18:20">
<title>
Global Poverty
</title>
<rubric>
<prompt>
Given the state of the world today, what do you think should be done to combat poverty?
Read for conciseness, clarity of thought, and form.
</prompt>
<criterion feedback="optional">
<name>concise</name>
<prompt>How concise is it?</prompt>
<option points="0">
<name>Neal Stephenson (late)</name>
<explanation>
In "Cryptonomicon", Stephenson spent multiple pages talking about breakfast cereal.
While hilarious, in recent years his work has been anything but 'concise'.
</explanation>
</option>
<option points="1">
<name>HP Lovecraft</name>
<explanation>
If the author wrote something cyclopean that staggers the mind, score it thus.
</explanation>
</option>
<option points="3">
<name>Robert Heinlein</name>
<explanation>
Tight prose that conveys a wealth of information about the world in relatively
few words. Example, "The door irised open and he stepped inside."
</explanation>
</option>
<option points="4">
<name>Neal Stephenson (early)</name>
<explanation>
When Stephenson still had an editor, his prose was dense, with anecdotes about
nitrox abuse implying main characters' whole life stories.
</explanation>
</option>
<option points="5">
<name>Earnest Hemingway</name>
<explanation>
Score the work this way if it makes you weep, and the removal of a single
word would make you sneer.
</explanation>
</option>
</criterion>
<criterion>
<name>clear-headed</name>
<prompt>How clear is the thinking?</prompt>
<option points="0">
<name>Yogi Berra</name>
<explanation></explanation>
</option>
<option points="1">
<name>Hunter S. Thompson</name>
<explanation></explanation>
</option>
<option points="2">
<name>Robert Heinlein</name>
<explanation></explanation>
</option>
<option points="3">
<name>Isaac Asimov</name>
<explanation></explanation>
</option>
<option points="10">
<name>Spock</name>
<explanation>
Coolly rational, with a firm grasp of the main topics, a crystal-clear train of thought,
and unemotional examination of the facts. This is the only item explained in this category,
to show that explained and unexplained items can be mixed.
</explanation>
</option>
</criterion>
<criterion feedback="optional">
<name>form</name>
<prompt>Lastly, how is its form? Punctuation, grammar, and spelling all count.</prompt>
<option points="0">
<name>lolcats</name>
<explanation></explanation>
</option>
<option points="1">
<name>Facebook</name>
<explanation></explanation>
</option>
<option points="2">
<name>Reddit</name>
<explanation></explanation>
</option>
<option points="3">
<name>metafilter</name>
<explanation></explanation>
</option>
<option points="4">
<name>Usenet, 1996</name>
<explanation></explanation>
</option>
<option points="5">
<name>The Elements of Style</name>
<explanation></explanation>
</option>
</criterion>
<criterion feedback="required">
<name>Feedback only</name>
<prompt>This criterion has only written feedback, no options</prompt>
</criterion>
</rubric>
<assessments>
<assessment name="peer-assessment"
start="2014-03-11T10:00-18:10"
due="2035-12-21T22:22-7:00"
must_grade="1"
must_be_graded_by="1" />
<assessment name="self-assessment" />
</assessments>
</openassessment>
<openassessment file_upload_type="custom" white_listed_file_types="pdf,doc,docx" submission_due="2035-03-11T18:20">
<title>
Global Poverty
</title>
<rubric>
<prompt>
Given the state of the world today, what do you think should be done to combat poverty?
Read for conciseness, clarity of thought, and form.
</prompt>
<criterion feedback="optional">
<name>concise</name>
<prompt>How concise is it?</prompt>
<option points="0">
<name>Neal Stephenson (late)</name>
<explanation>
In "Cryptonomicon", Stephenson spent multiple pages talking about breakfast cereal.
While hilarious, in recent years his work has been anything but 'concise'.
</explanation>
</option>
<option points="1">
<name>HP Lovecraft</name>
<explanation>
If the author wrote something cyclopean that staggers the mind, score it thus.
</explanation>
</option>
<option points="3">
<name>Robert Heinlein</name>
<explanation>
Tight prose that conveys a wealth of information about the world in relatively
few words. Example, "The door irised open and he stepped inside."
</explanation>
</option>
<option points="4">
<name>Neal Stephenson (early)</name>
<explanation>
When Stephenson still had an editor, his prose was dense, with anecdotes about
nitrox abuse implying main characters' whole life stories.
</explanation>
</option>
<option points="5">
<name>Earnest Hemingway</name>
<explanation>
Score the work this way if it makes you weep, and the removal of a single
word would make you sneer.
</explanation>
</option>
</criterion>
<criterion>
<name>clear-headed</name>
<prompt>How clear is the thinking?</prompt>
<option points="0">
<name>Yogi Berra</name>
<explanation></explanation>
</option>
<option points="1">
<name>Hunter S. Thompson</name>
<explanation></explanation>
</option>
<option points="2">
<name>Robert Heinlein</name>
<explanation></explanation>
</option>
<option points="3">
<name>Isaac Asimov</name>
<explanation></explanation>
</option>
<option points="10">
<name>Spock</name>
<explanation>
Coolly rational, with a firm grasp of the main topics, a crystal-clear train of thought,
and unemotional examination of the facts. This is the only item explained in this category,
to show that explained and unexplained items can be mixed.
</explanation>
</option>
</criterion>
<criterion feedback="optional">
<name>form</name>
<prompt>Lastly, how is its form? Punctuation, grammar, and spelling all count.</prompt>
<option points="0">
<name>lolcats</name>
<explanation></explanation>
</option>
<option points="1">
<name>Facebook</name>
<explanation></explanation>
</option>
<option points="2">
<name>Reddit</name>
<explanation></explanation>
</option>
<option points="3">
<name>metafilter</name>
<explanation></explanation>
</option>
<option points="4">
<name>Usenet, 1996</name>
<explanation></explanation>
</option>
<option points="5">
<name>The Elements of Style</name>
<explanation></explanation>
</option>
</criterion>
<criterion feedback="required">
<name>Feedback only</name>
<prompt>This criterion has only written feedback, no options</prompt>
</criterion>
</rubric>
<assessments>
<assessment name="peer-assessment"
start="2014-03-11T10:00-18:10"
due="2035-12-21T22:22-7:00"
must_grade="1"
must_be_graded_by="1" />
<assessment name="self-assessment" />
</assessments>
</openassessment>
<openassessment file_upload_type="image" submission_due="2035-03-11T18:20">
<title>
Global Poverty
</title>
<rubric>
<prompt>
Given the state of the world today, what do you think should be done to combat poverty?
Read for conciseness, clarity of thought, and form.
</prompt>
<criterion feedback="optional">
<name>concise</name>
<prompt>How concise is it?</prompt>
<option points="0">
<name>Neal Stephenson (late)</name>
<explanation>
In "Cryptonomicon", Stephenson spent multiple pages talking about breakfast cereal.
While hilarious, in recent years his work has been anything but 'concise'.
</explanation>
</option>
<option points="1">
<name>HP Lovecraft</name>
<explanation>
If the author wrote something cyclopean that staggers the mind, score it thus.
</explanation>
</option>
<option points="3">
<name>Robert Heinlein</name>
<explanation>
Tight prose that conveys a wealth of information about the world in relatively
few words. Example, "The door irised open and he stepped inside."
</explanation>
</option>
<option points="4">
<name>Neal Stephenson (early)</name>
<explanation>
When Stephenson still had an editor, his prose was dense, with anecdotes about
nitrox abuse implying main characters' whole life stories.
</explanation>
</option>
<option points="5">
<name>Earnest Hemingway</name>
<explanation>
Score the work this way if it makes you weep, and the removal of a single
word would make you sneer.
</explanation>
</option>
</criterion>
<criterion>
<name>clear-headed</name>
<prompt>How clear is the thinking?</prompt>
<option points="0">
<name>Yogi Berra</name>
<explanation></explanation>
</option>
<option points="1">
<name>Hunter S. Thompson</name>
<explanation></explanation>
</option>
<option points="2">
<name>Robert Heinlein</name>
<explanation></explanation>
</option>
<option points="3">
<name>Isaac Asimov</name>
<explanation></explanation>
</option>
<option points="10">
<name>Spock</name>
<explanation>
Coolly rational, with a firm grasp of the main topics, a crystal-clear train of thought,
and unemotional examination of the facts. This is the only item explained in this category,
to show that explained and unexplained items can be mixed.
</explanation>
</option>
</criterion>
<criterion feedback="optional">
<name>form</name>
<prompt>Lastly, how is its form? Punctuation, grammar, and spelling all count.</prompt>
<option points="0">
<name>lolcats</name>
<explanation></explanation>
</option>
<option points="1">
<name>Facebook</name>
<explanation></explanation>
</option>
<option points="2">
<name>Reddit</name>
<explanation></explanation>
</option>
<option points="3">
<name>metafilter</name>
<explanation></explanation>
</option>
<option points="4">
<name>Usenet, 1996</name>
<explanation></explanation>
</option>
<option points="5">
<name>The Elements of Style</name>
<explanation></explanation>
</option>
</criterion>
<criterion feedback="required">
<name>Feedback only</name>
<prompt>This criterion has only written feedback, no options</prompt>
</criterion>
</rubric>
<assessments>
<assessment name="peer-assessment"
start="2014-03-11T10:00-18:10"
due="2035-12-21T22:22-7:00"
must_grade="1"
must_be_graded_by="1" />
<assessment name="self-assessment" />
</assessments>
</openassessment>
<openassessment file_upload_type="pdf-and-image" submission_due="2035-03-11T18:20">
<title>
Global Poverty
</title>
<rubric>
<prompt>
Given the state of the world today, what do you think should be done to combat poverty?
Read for conciseness, clarity of thought, and form.
</prompt>
<criterion feedback="optional">
<name>concise</name>
<prompt>How concise is it?</prompt>
<option points="0">
<name>Neal Stephenson (late)</name>
<explanation>
In "Cryptonomicon", Stephenson spent multiple pages talking about breakfast cereal.
While hilarious, in recent years his work has been anything but 'concise'.
</explanation>
</option>
<option points="1">
<name>HP Lovecraft</name>
<explanation>
If the author wrote something cyclopean that staggers the mind, score it thus.
</explanation>
</option>
<option points="3">
<name>Robert Heinlein</name>
<explanation>
Tight prose that conveys a wealth of information about the world in relatively
few words. Example, "The door irised open and he stepped inside."
</explanation>
</option>
<option points="4">
<name>Neal Stephenson (early)</name>
<explanation>
When Stephenson still had an editor, his prose was dense, with anecdotes about
nitrox abuse implying main characters' whole life stories.
</explanation>
</option>
<option points="5">
<name>Earnest Hemingway</name>
<explanation>
Score the work this way if it makes you weep, and the removal of a single
word would make you sneer.
</explanation>
</option>
</criterion>
<criterion>
<name>clear-headed</name>
<prompt>How clear is the thinking?</prompt>
<option points="0">
<name>Yogi Berra</name>
<explanation></explanation>
</option>
<option points="1">
<name>Hunter S. Thompson</name>
<explanation></explanation>
</option>
<option points="2">
<name>Robert Heinlein</name>
<explanation></explanation>
</option>
<option points="3">
<name>Isaac Asimov</name>
<explanation></explanation>
</option>
<option points="10">
<name>Spock</name>
<explanation>
Coolly rational, with a firm grasp of the main topics, a crystal-clear train of thought,
and unemotional examination of the facts. This is the only item explained in this category,
to show that explained and unexplained items can be mixed.
</explanation>
</option>
</criterion>
<criterion feedback="optional">
<name>form</name>
<prompt>Lastly, how is its form? Punctuation, grammar, and spelling all count.</prompt>
<option points="0">
<name>lolcats</name>
<explanation></explanation>
</option>
<option points="1">
<name>Facebook</name>
<explanation></explanation>
</option>
<option points="2">
<name>Reddit</name>
<explanation></explanation>
</option>
<option points="3">
<name>metafilter</name>
<explanation></explanation>
</option>
<option points="4">
<name>Usenet, 1996</name>
<explanation></explanation>
</option>
<option points="5">
<name>The Elements of Style</name>
<explanation></explanation>
</option>
</criterion>
<criterion feedback="required">
<name>Feedback only</name>
<prompt>This criterion has only written feedback, no options</prompt>
</criterion>
</rubric>
<assessments>
<assessment name="peer-assessment"
start="2014-03-11T10:00-18:10"
due="2035-12-21T22:22-7:00"
must_grade="1"
must_be_graded_by="1" />
<assessment name="self-assessment" />
</assessments>
</openassessment>
<openassessment submission_due="2030-03-11T18:20" leaderboard_show="10" allow_file_upload="True">
<openassessment submission_due="2030-03-11T18:20" leaderboard_show="10" file_upload_type="image">
<title>
My favourite pet
</title>
......
<openassessment submission_due="2030-03-11T18:20" leaderboard_show="10" file_upload_type="custom" white_listed_file_types="pdf,doc">
<title>
My favourite pet
</title>
<rubric>
<prompt>
Which animal would you like to have as a pet?
</prompt>
<criterion feedback='optional'>
<name>concise</name>
<prompt>How rare is the animal?</prompt>
<option points="0">
<name>Very common</name>
<explanation>
You can pick it up on the street
</explanation>
</option>
<option points="2">
<name>Common</name>
<explanation>
Can get it at the local pet store
</explanation>
</option>
<option points="4">
<name>Somewhat common</name>
<explanation>
Easy to see but hard to purchase as a pet
</explanation>
</option>
<option points="8">
<name>Rare</name>
<explanation>
Need to travel the world to find it
</explanation>
</option>
<option points="10">
<name>Extinct</name>
<explanation>
Maybe in the ice-age
</explanation>
</option>
</criterion>
<criterion feedback='optional'>
<name>form</name>
<prompt>How hard would it be to care for the animal?</prompt>
<option points="0">
<name>It feeds itself</name>
<explanation></explanation>
</option>
<option points="2">
<name>Any pet food will do</name>
<explanation></explanation>
</option>
<option points="4">
<name>Some work required to care for the animal</name>
<explanation></explanation>
</option>
<option points="6">
<name>A full time job to care for the animal</name>
<explanation></explanation>
</option>
<option points="8">
<name>A team required to care for the animal</name>
<explanation></explanation>
</option>
<option points="10">
<name>The pet has special needs</name>
<explanation></explanation>
</option>
</criterion>
</rubric>
<assessments>
<assessment name="self-assessment" />
</assessments>
</openassessment>
<openassessment allow_file_upload="True" submission_due="2015-03-11T18:20">
<openassessment file_upload_type="image" submission_due="2015-03-11T18:20">
<title>
Global Poverty
</title>
......
......@@ -73,7 +73,10 @@ class StudioMixin(object):
else:
# TODO: switch to add_javascript_url once XBlock resources are loaded from the CDN
fragment.add_javascript(pkg_resources.resource_string(__name__, "static/js/openassessment-studio.min.js"))
fragment.initialize_js('OpenAssessmentEditor')
js_context_dict = {
"FILE_EXT_BLACK_LIST": self.FILE_EXT_BLACK_LIST,
}
fragment.initialize_js('OpenAssessmentEditor', js_context_dict)
return fragment
def editor_context(self):
......@@ -115,7 +118,7 @@ class StudioMixin(object):
if not criteria:
criteria = self.DEFAULT_CRITERIA
# To maintain backwards compatibility, if there is no
# To maintain backwards compatibility, if there is no
# feedback_default_text configured for the xblock, use the default text
feedback_default_text = copy.deepcopy(self.rubric_feedback_default_text)
if not feedback_default_text:
......@@ -130,7 +133,8 @@ class StudioMixin(object):
'criteria': criteria,
'feedbackprompt': self.rubric_feedback_prompt,
'feedback_default_text': feedback_default_text,
'allow_file_upload': self.allow_file_upload,
'file_upload_type': self.file_upload_type,
'white_listed_file_types': self.white_listed_file_types_string,
'allow_latex': self.allow_latex,
'leaderboard_show': self.leaderboard_show,
'editor_assessments_order': [
......@@ -228,7 +232,8 @@ class StudioMixin(object):
self.rubric_feedback_default_text = data['feedback_default_text']
self.submission_start = data['submission_start']
self.submission_due = data['submission_due']
self.allow_file_upload = bool(data['allow_file_upload'])
self.file_upload_type = data['file_upload_type']
self.white_listed_file_types_string = data['white_listed_file_types']
self.allow_latex = bool(data['allow_latex'])
self.leaderboard_show = data['leaderboard_show']
......
......@@ -29,6 +29,23 @@ class SubmissionMixin(object):
"""
ALLOWED_IMAGE_MIME_TYPES = ['image/gif', 'image/jpeg', 'image/pjpeg', 'image/png']
ALLOWED_FILE_MIME_TYPES = ['application/pdf'] + ALLOWED_IMAGE_MIME_TYPES
# taken from http://www.howtogeek.com/137270/50-file-extensions-that-are-potentially-dangerous-on-windows/
# and http://pcsupport.about.com/od/tipstricks/a/execfileext.htm
# left out .js and office extensions
FILE_EXT_BLACK_LIST = [
'exe', 'msi', 'app', 'dmg', 'com', 'pif', 'application', 'gadget',
'msp', 'scr', 'hta', 'cpl', 'msc', 'jar', 'bat', 'cmd', 'vb', 'vbs',
'jse', 'ws', 'wsf', 'wsc', 'wsh', 'scf', 'lnk', 'inf', 'reg', 'ps1',
'ps1xml', 'ps2', 'ps2xml', 'psc1', 'psc2', 'msh', 'msh1', 'msh2', 'mshxml',
'msh1xml', 'msh2xml', 'action', 'apk', 'app', 'bin', 'command', 'csh',
'ins', 'inx', 'ipa', 'isu', 'job', 'mst', 'osx', 'out', 'paf', 'prg',
'rgs', 'run', 'sct', 'shb', 'shs', 'u3p', 'vbscript', 'vbe', 'workflow',
]
@XBlock.json_handler
def submit(self, data, suffix=''):
"""Place the submission text into Openassessment system
......@@ -172,7 +189,7 @@ class SubmissionMixin(object):
# so that later we can add additional response fields.
student_sub_dict = prepare_submission_for_serialization(student_sub_data)
if self.allow_file_upload:
if self.file_upload_type:
student_sub_dict['file_key'] = self._get_student_item_key()
submission = api.create_submission(student_item_dict, student_sub_dict)
self.create_workflow(submission["uuid"])
......@@ -203,13 +220,25 @@ class SubmissionMixin(object):
A URL to be used to upload content associated with this submission.
"""
if "contentType" not in data:
return {'success': False, 'msg': self._(u"Must specify contentType.")}
if 'contentType' not in data or 'filename' not in data:
return {'success': False, 'msg': self._(u"There was an error uploading your file.")}
content_type = data['contentType']
file_name = data['filename']
file_name_parts = file_name.split('.')
file_ext = file_name_parts[-1] if len(file_name_parts) > 1 else None
if self.file_upload_type == 'image' and content_type not in self.ALLOWED_IMAGE_MIME_TYPES:
return {'success': False, 'msg': self._(u"Content type must be GIF, PNG or JPG.")}
if not content_type.startswith('image/'):
return {'success': False, 'msg': self._(u"contentType must be an image.")}
if self.file_upload_type == 'pdf-and-image' and content_type not in self.ALLOWED_FILE_MIME_TYPES:
return {'success': False, 'msg': self._(u"Content type must be PDF, GIF, PNG or JPG.")}
if self.file_upload_type == 'custom' and file_ext not in self.white_listed_file_types:
return {'success': False, 'msg': self._(u"File type must be one of the following types: {}").format(
', '.join(self.white_listed_file_types))}
if file_ext in self.FILE_EXT_BLACK_LIST:
return {'success': False, 'msg': self._(u"File type is not allowed.")}
try:
key = self._get_student_item_key()
url = file_upload_api.get_upload_url(key, content_type)
......@@ -249,8 +278,9 @@ class SubmissionMixin(object):
A string representation of the key.
"""
student_item_dict = self.get_student_item_dict()
return u"{student_id}/{course_id}/{item_id}".format(
**self.get_student_item_dict()
**student_item_dict
)
def get_download_url_from_submission(self, submission):
......@@ -308,7 +338,8 @@ class SubmissionMixin(object):
Returns:
unicode
"""
return self._(u'This response has been saved but not submitted.') if self.has_saved else self._(u'This response has not been saved.')
return self._(u'This response has been saved but not submitted.') if self.has_saved else self._(
u'This response has not been saved.')
@XBlock.handler
def render_submission(self, data, suffix=''):
......@@ -354,13 +385,15 @@ class SubmissionMixin(object):
if due_date < DISTANT_FUTURE:
context["submission_due"] = due_date
context['allow_file_upload'] = self.allow_file_upload
context['file_upload_type'] = self.file_upload_type
context['allow_latex'] = self.allow_latex
context['has_peer'] = 'peer-assessment' in self.assessment_steps
context['has_self'] = 'self-assessment' in self.assessment_steps
if self.allow_file_upload:
if self.file_upload_type:
context['file_url'] = self._get_download_url()
if self.file_upload_type == 'custom':
context['white_listed_file_types'] = self.white_listed_file_types
if not workflow and problem_closed:
if reason == 'due':
......
<openassessment file_upload_type="pdf-and-image">
<title>Open Assessment Test</title>
<prompts>
<prompt>
<description>Given the state of the world today, what do you think should be done to combat poverty? Please answer in a short essay of 200-300 words.</description>
</prompt>
<prompt>
<description>Given the state of the world today, what do you think should be done to combat pollution?</description>
</prompt>
</prompts>
<rubric>
<criterion>
<name>Concise</name>
<prompt>How concise is it?</prompt>
<option points="0">
<name>Neal Stephenson (late)</name>
<explanation>Neal Stephenson explanation</explanation>
</option>
<option points="1">
<name>HP Lovecraft</name>
<explanation>HP Lovecraft explanation</explanation>
</option>
<option points="3">
<name>Robert Heinlein</name>
<explanation>Robert Heinlein explanation</explanation>
</option>
<option points="4">
<name>Neal Stephenson (early)</name>
<explanation>Neal Stephenson (early) explanation</explanation>
</option>
<option points="5">
<name>Earnest Hemingway</name>
<explanation>Earnest Hemingway</explanation>
</option>
</criterion>
<criterion>
<name>Clear-headed</name>
<prompt>How clear is the thinking?</prompt>
<option points="0">
<name>Yogi Berra</name>
<explanation>Yogi Berra explanation</explanation>
</option>
<option points="1">
<name>Hunter S. Thompson</name>
<explanation>Hunter S. Thompson explanation</explanation>
</option>
<option points="2">
<name>Robert Heinlein</name>
<explanation>Robert Heinlein explanation</explanation>
</option>
<option points="3">
<name>Isaac Asimov</name>
<explanation>Isaac Asimov explanation</explanation>
</option>
<option points="10">
<name>Spock</name>
<explanation>Spock explanation</explanation>
</option>
</criterion>
<criterion>
<name>Form</name>
<prompt>Lastly, how is its form? Punctuation, grammar, and spelling all count.</prompt>
<option points="0">
<name>lolcats</name>
<explanation>lolcats explanation</explanation>
</option>
<option points="1">
<name>Facebook</name>
<explanation>Facebook explanation</explanation>
</option>
<option points="2">
<name>Reddit</name>
<explanation>Reddit explanation</explanation>
</option>
<option points="3">
<name>metafilter</name>
<explanation>metafilter explanation</explanation>
</option>
<option points="4">
<name>Usenet, 1996</name>
<explanation>Usenet, 1996 explanation</explanation>
</option>
<option points="5">
<name>The Elements of Style</name>
<explanation>The Elements of Style explanation</explanation>
</option>
</criterion>
</rubric>
<assessments>
<assessment name="peer-assessment" must_grade="5" must_be_graded_by="3" />
<assessment name="self-assessment" />
</assessments>
</openassessment>
......@@ -21,7 +21,7 @@
"submission_due": "4014-02-27T09:46:28",
"submission_start": "4014-02-10T09:46:28",
"title": "My new title.",
"allow_file_upload": false,
"file_upload_type": null,
"assessments": [
{
"name": "peer-assessment",
......
......@@ -4,7 +4,7 @@
"title": "My new title.",
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"assessments": [
......@@ -29,7 +29,7 @@
"no_prompt": {
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -79,7 +79,7 @@
"no_feedback_prompt": {
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -131,7 +131,7 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -182,7 +182,7 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -235,7 +235,7 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -288,7 +288,7 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -333,7 +333,7 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -377,7 +377,7 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -400,7 +400,7 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -444,7 +444,7 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -474,7 +474,7 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -513,7 +513,7 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -557,7 +557,7 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -596,11 +596,11 @@
"expected_error": "error updating xblock configuration"
},
"allow_file_upload_must_be_boolean": {
"invalid_file_upload_type": {
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": 6,
"file_upload_type": "invalid",
"allow_latex": false,
"criteria": [
{
......@@ -714,7 +714,7 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"assessments": [
......@@ -771,7 +771,7 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"assessments": [
......@@ -826,7 +826,7 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"assessments": [
......@@ -884,7 +884,7 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"assessments": [
......@@ -942,7 +942,7 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"assessments": [
......@@ -994,7 +994,7 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"assessments": [
......@@ -1053,7 +1053,7 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"assessments": [
......@@ -1104,7 +1104,7 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"assessments": [
......@@ -1156,7 +1156,7 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"assessments": [
......@@ -1184,7 +1184,7 @@
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -1235,7 +1235,7 @@
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -1286,7 +1286,7 @@
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......@@ -1360,7 +1360,7 @@
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"allow_file_upload": false,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
......
<openassessment leaderboard_show="3" allow_file_upload="True">
<openassessment leaderboard_show="3" file_upload_type="image">
<title>Open Assessment Test</title>
<prompts>
<prompt>
......
......@@ -8,7 +8,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -80,7 +80,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -150,7 +150,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -291,7 +291,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -358,7 +358,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -428,7 +428,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -496,7 +496,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -560,7 +560,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 2,
......@@ -643,7 +643,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -785,7 +785,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -856,7 +856,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -927,7 +927,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -990,7 +990,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -1101,7 +1101,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -1244,7 +1244,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -1343,12 +1343,78 @@
]
},
"allow_file_upload": {
"file_upload_type_none": {
"title": "Foo",
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
"name": "Test criterion",
"prompt": "Test criterion prompt",
"options": [
{
"order_num": 0,
"points": 0,
"name": "No",
"explanation": "No explanation"
},
{
"order_num": 1,
"points": 2,
"name": "Yes",
"explanation": "Yes explanation"
}
]
}
],
"assessments": [
{
"name": "peer-assessment",
"start": "2014-02-27T09:46:28",
"due": "2014-03-01T00:00:00",
"must_grade": 5,
"must_be_graded_by": 3
},
{
"name": "self-assessment",
"start": "2014-04-01T00:00:00",
"due": "2014-06-01T00:00:00"
}
],
"expected_xml": [
"<openassessment>",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"peer-assessment\" start=\"2014-02-27T09:46:28\" due=\"2014-03-01T00:00:00\" must_grade=\"5\" must_be_graded_by=\"3\" />",
"<assessment name=\"self-assessment\" start=\"2014-04-01T00:00:00\" due=\"2014-06-01T00:00:00\" />",
"</assessments>",
"<prompts>",
"<prompt><description>Test prompt</description></prompt>",
"</prompts>",
"<rubric>",
"<criterion>",
"<name>Test criterion</name>",
"<label>Test criterion</label>",
"<prompt>Test criterion prompt</prompt>",
"<option points=\"0\"><name>No</name><label>No</label><explanation>No explanation</explanation></option>",
"<option points=\"2\"><name>Yes</name><label>Yes</label><explanation>Yes explanation</explanation></option>",
"</criterion>",
"<feedbackprompt>Test Feedback Prompt</feedbackprompt>",
"<feedback_default_text>Test default text...</feedback_default_text>",
"</rubric>",
"</openassessment>"
]
},
"file_upload_type_image": {
"title": "Foo",
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
"allow_file_upload": true,
"file_upload_type": "image",
"criteria": [
{
"order_num": 0,
......@@ -1385,7 +1451,7 @@
}
],
"expected_xml": [
"<openassessment allow_file_upload=\"True\">",
"<openassessment file_upload_type=\"image\">",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"peer-assessment\" start=\"2014-02-27T09:46:28\" due=\"2014-03-01T00:00:00\" must_grade=\"5\" must_be_graded_by=\"3\" />",
......@@ -1417,7 +1483,7 @@
"due": null,
"submission_start": null,
"submission_due": null,
"allow_file_upload": null,
"file_upload_type": null,
"criteria": [
{
"order_num": 0,
......@@ -1482,7 +1548,7 @@
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "",
"allow_file_upload": true,
"file_upload_type": "image",
"start": null,
"due": null,
"submission_start": null,
......@@ -1523,7 +1589,7 @@
}
],
"expected_xml": [
"<openassessment allow_file_upload=\"True\">",
"<openassessment file_upload_type=\"image\">",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"peer-assessment\" start=\"2014-02-27T09:46:28\" due=\"2014-03-01T00:00:00\" must_grade=\"5\" must_be_graded_by=\"3\" />",
......
......@@ -676,9 +676,31 @@
]
},
"file_upload": {
"file_upload_type_none": {
"xml": [
"<openassessment allow_file_upload=\"True\">",
"<openassessment>",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"peer-assessment\" start=\"2014-02-27T09:46:28\" due=\"2014-03-01T00:00:00\" must_grade=\"5\" must_be_graded_by=\"3\" />",
"<assessment name=\"self-assessment\" start=\"2014-04-01T00:00:00\" due=\"2014-06-01T00:00:00\" />",
"</assessments>",
"<rubric>",
"<prompt>Test prompt</prompt>",
"<criterion>",
"<name>Test criterion</name>",
"<prompt>Test criterion prompt</prompt>",
"<option points=\"0\"><name>No</name><explanation>No explanation</explanation></option>",
"<option points=\"2\"><name>Yes</name><explanation>Yes explanation</explanation></option>",
"</criterion>",
"</rubric>",
"</openassessment>"
],
"file_upload_type": null
},
"file_upload_type_image": {
"xml": [
"<openassessment file_upload_type=\"image\">",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"peer-assessment\" start=\"2014-02-27T09:46:28\" due=\"2014-03-01T00:00:00\" must_grade=\"5\" must_be_graded_by=\"3\" />",
......@@ -695,7 +717,7 @@
"</rubric>",
"</openassessment>"
],
"allow_file_upload": true
"file_upload_type": "image"
},
"leaderboard": {
......
......@@ -31,7 +31,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"allow_file_upload": false,
"file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false,
"leaderboard_show": 0,
"assessments": [
......@@ -83,7 +84,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "ɯʎ uǝʍ ʇıʇןǝ",
"allow_file_upload": false,
"file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false,
"leaderboard_show": 0,
"assessments": [
......@@ -135,7 +137,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"allow_file_upload": false,
"file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false,
"leaderboard_show": 0,
"assessments": [
......@@ -198,7 +201,167 @@
"feedback_default_text": "Feedback default text",
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"allow_file_upload": false,
"file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false,
"leaderboard_show": 0,
"title": "My new title.",
"assessments": [
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": null,
"due": "4014-03-10T00:00"
},
{
"name": "self-assessment",
"start": null,
"due": null
}
],
"editor_assessments_order": ["student-training", "peer-assessment", "self-assessment"]
},
"file_upload_type_image_treated_as_image_file_upload_type": {
"criteria": [
{
"order_num": 0,
"name": "cd316c145cb14e06b377db65719ed41c",
"label": "Test criterion",
"prompt": "Test criterion prompt",
"options": [
{
"order_num": 0,
"points": 0,
"name": "7c080ee29c38414291c92eb42b2ab310",
"label": "No",
"explanation": "No explanation"
},
{
"order_num": 1,
"points": 2,
"name": "8bcdb0769b15482d9b2c3791d22e8ad2",
"label": "Yes",
"explanation": "Yes explanation"
}
],
"feedback": "required"
}
],
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"file_upload_type": "image",
"white_listed_file_types": null,
"allow_latex": false,
"leaderboard_show": 0,
"title": "My new title.",
"assessments": [
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": null,
"due": "4014-03-10T00:00"
},
{
"name": "self-assessment",
"start": null,
"due": null
}
],
"editor_assessments_order": ["student-training", "peer-assessment", "self-assessment"]
},
"file_upload_type_file_treated_as_restricted_file_upload": {
"criteria": [
{
"order_num": 0,
"name": "cd316c145cb14e06b377db65719ed41c",
"label": "Test criterion",
"prompt": "Test criterion prompt",
"options": [
{
"order_num": 0,
"points": 0,
"name": "7c080ee29c38414291c92eb42b2ab310",
"label": "No",
"explanation": "No explanation"
},
{
"order_num": 1,
"points": 2,
"name": "8bcdb0769b15482d9b2c3791d22e8ad2",
"label": "Yes",
"explanation": "Yes explanation"
}
],
"feedback": "required"
}
],
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"file_upload_type": "pdf-and-image",
"white_listed_file_types": null,
"allow_latex": false,
"leaderboard_show": 0,
"title": "My new title.",
"assessments": [
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": null,
"due": "4014-03-10T00:00"
},
{
"name": "self-assessment",
"start": null,
"due": null
}
],
"editor_assessments_order": ["student-training", "peer-assessment", "self-assessment"]
},
"file_upload_type_custom_treated_as_restrictive_file_upload": {
"criteria": [
{
"order_num": 0,
"name": "cd316c145cb14e06b377db65719ed41c",
"label": "Test criterion",
"prompt": "Test criterion prompt",
"options": [
{
"order_num": 0,
"points": 0,
"name": "7c080ee29c38414291c92eb42b2ab310",
"label": "No",
"explanation": "No explanation"
},
{
"order_num": 1,
"points": 2,
"name": "8bcdb0769b15482d9b2c3791d22e8ad2",
"label": "Yes",
"explanation": "Yes explanation"
}
],
"feedback": "required"
}
],
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"file_upload_type": "custom",
"white_listed_file_types": "pdf,doc,docx",
"allow_latex": false,
"leaderboard_show": 0,
"title": "My new title.",
......
......@@ -368,7 +368,7 @@ class TestPeerAssessmentRender(XBlockHandlerTestCase):
'must_grade': 5,
'review_num': 1,
'peer_submission': create_submission_dict(submission, xblock.prompts),
'allow_file_upload': False,
'file_upload_type': None,
'peer_file_url': '',
'submit_button_text': 'submit your assessment & move to response #2',
'allow_latex': False,
......@@ -538,7 +538,7 @@ class TestPeerAssessmentRender(XBlockHandlerTestCase):
'must_grade': 5,
'peer_due': dt.datetime(2000, 1, 1).replace(tzinfo=pytz.utc),
'peer_submission': create_submission_dict(submission, xblock.prompts),
'allow_file_upload': False,
'file_upload_type': None,
'peer_file_url': '',
'review_num': 1,
'rubric_criteria': xblock.rubric_criteria,
......
......@@ -266,7 +266,7 @@ class TestSelfAssessmentRender(XBlockHandlerTestCase):
'rubric_criteria': xblock.rubric_criteria,
'estimated_time': '20 minutes',
'self_submission': submission,
'allow_file_upload': False,
'file_upload_type': None,
'self_file_url': '',
'allow_latex': False,
},
......
......@@ -337,7 +337,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
file_api.get_download_url.assert_called_with("test_key")
# Check the context passed to the template
self.assertEquals('http://www.example.com/image.jpeg', context['submission']['image_url'])
self.assertEquals('http://www.example.com/image.jpeg', context['submission']['file_url'])
# Check the fully rendered template
payload = urllib.urlencode({"student_username": "Bob"})
......@@ -368,7 +368,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
# Expect that the page still renders, but without the image url
self.assertIn('submission', context)
self.assertNotIn('image_url', context['submission'])
self.assertNotIn('file_url', context['submission'])
# Check the fully rendered template
payload = urllib.urlencode({"student_username": "Bob"})
......
......@@ -23,7 +23,8 @@ class StudioViewTest(XBlockHandlerTestCase):
"feedback_default_text": "Test feedback default text",
"submission_start": "4014-02-10T09:46",
"submission_due": "4014-02-27T09:46",
"allow_file_upload": False,
"file_upload_type": None,
"white_listed_file_types": '',
"allow_latex": False,
"leaderboard_show": 4,
"assessments": [{"name": "self-assessment"}],
......@@ -151,7 +152,6 @@ class StudioViewTest(XBlockHandlerTestCase):
self.assertTrue(resp['success'], msg=resp.get('msg'))
self.assertEqual(xblock.editor_assessments_order, data['editor_assessments_order'])
@scenario('data/basic_scenario.xml')
def test_update_editor_context_saves_assessment_order_with_ai(self, xblock):
# Update the XBlock with a different editor assessment order
......
......@@ -122,7 +122,8 @@ class TestSerializeContent(TestCase):
self.oa_block.rubric_criteria = data.get('criteria', copy.deepcopy(self.BASIC_CRITERIA))
self.oa_block.rubric_assessments = data.get('assessments', copy.deepcopy(self.BASIC_ASSESSMENTS))
self.oa_block.allow_file_upload = data.get('allow_file_upload')
self.oa_block.file_upload_type = data.get('file_upload_type')
self.oa_block.white_listed_file_types = data.get('white_listed_file_types')
self.oa_block.allow_latex = data.get('allow_latex')
self.oa_block.leaderboard_show = data.get('leaderboard_show', 0)
......@@ -492,7 +493,8 @@ class TestParseFromXml(TestCase):
'submission_due',
'criteria',
'assessments',
'allow_file_upload',
'file_upload_type',
'white_listed_file_types',
'allow_latex',
'leaderboard_show'
]
......
......@@ -686,9 +686,13 @@ def serialize_content_to_xml(oa_block, root):
if oa_block.leaderboard_show:
root.set('leaderboard_show', unicode(oa_block.leaderboard_show))
# Allow file upload
if oa_block.allow_file_upload is not None:
root.set('allow_file_upload', unicode(oa_block.allow_file_upload))
# Set File upload settings
if oa_block.file_upload_type:
root.set('file_upload_type', unicode(oa_block.file_upload_type))
# Set File type white listing
if oa_block.white_listed_file_types:
root.set('white_listed_file_types', unicode(oa_block.white_listed_file_types_string))
if oa_block.allow_latex is not None:
root.set('allow_latex', unicode(oa_block.allow_latex))
......@@ -815,10 +819,18 @@ def parse_from_xml(root):
if 'submission_due' in root.attrib:
submission_due = parse_date(unicode(root.attrib['submission_due']), name="submission due date")
allow_file_upload = False
allow_file_upload = None
if 'allow_file_upload' in root.attrib:
allow_file_upload = _parse_boolean(unicode(root.attrib['allow_file_upload']))
file_upload_type = None
if 'file_upload_type' in root.attrib:
file_upload_type = unicode(root.attrib['file_upload_type'])
white_listed_file_types = None
if 'white_listed_file_types' in root.attrib:
white_listed_file_types = unicode(root.attrib['white_listed_file_types'])
allow_latex = False
if 'allow_latex' in root.attrib:
allow_latex = _parse_boolean(unicode(root.attrib['allow_latex']))
......@@ -865,6 +877,8 @@ def parse_from_xml(root):
'submission_start': submission_start,
'submission_due': submission_due,
'allow_file_upload': allow_file_upload,
'file_upload_type': file_upload_type,
'white_listed_file_types': white_listed_file_types,
'allow_latex': allow_latex,
'leaderboard_show': leaderboard_show
}
......
......@@ -53,6 +53,10 @@ class OpenAssessmentPage(PageObject):
with self.handle_alert():
self.q(css=".action--submit").first.click()
def hide_django_debug_tool(self):
if self.q(css='#djDebug').visible:
self.q(css='#djHideToolBarButton').click()
class SubmissionPage(OpenAssessmentPage):
"""
......@@ -92,6 +96,23 @@ class SubmissionPage(OpenAssessmentPage):
self.q(css="button#submission__preview").click()
self.wait_for_element_visibility("#preview_content .MathJax", "Verify Preview Latex expression")
def select_file(self, file_path_name):
"""
Select a file from local file system for uploading
Args:
file_path_name (string): full path and name of the file
"""
self.wait_for_element_visibility("#submission__answer__upload", "File select button is present")
self.q(css="#submission__answer__upload").fill(file_path_name)
def upload_file(self):
"""
Upload the selected file
"""
self.wait_for_element_visibility("#file__upload", "Upload button is present")
self.q(css="#file__upload").click()
@property
def latex_preview_button_is_disabled(self):
"""
......@@ -113,6 +134,26 @@ class SubmissionPage(OpenAssessmentPage):
"""
return self.q(css=".step--response.is--complete").is_present()
@property
def has_file_error(self):
"""
Check whether there is an error message for file upload.
Returns:
bool
"""
return self.q(css="#upload__error > div").visible
@property
def has_file_uploaded(self):
"""
Check whether file is successfully uploaded
Returns:
bool
"""
return self.q(css="#submission__custom__upload").visible
class AssessmentPage(OpenAssessmentPage):
"""
......
......@@ -60,6 +60,9 @@ class OpenAssessmentTest(WebAppTest):
'student_training':
u'courses/{test_course_id}/courseware/'
u'676026889c884ac1827688750871c825/5663e9b038434636977a4226d668fe02/'.format(test_course_id=TEST_COURSE_ID),
'file_upload':
u'courses/{test_course_id}/courseware/'
u'57a3f9d51d424f6cb922f0d69cba868d/bb563abc989340d8806920902f267ca3/'.format(test_course_id=TEST_COURSE_ID),
}
SUBMISSION = u"This is a test submission."
......@@ -283,6 +286,33 @@ class StaffAreaTest(OpenAssessmentTest):
self.assertEqual(self.staff_area_page.visible_staff_panels, [])
class FileUploadTest(OpenAssessmentTest):
"""
Test file upload
"""
def setUp(self):
super(FileUploadTest, self).setUp('file_upload')
@retry()
@attr('acceptance')
def test_file_upload(self):
self.auto_auth_page.visit()
# trying to upload a unacceptable file
self.submission_page.visit()
# hide django debug tool, otherwise, it will cover the button on the right side,
# which will cause the button non-clickable and tests to fail
self.submission_page.hide_django_debug_tool()
self.submission_page.select_file(os.path.dirname(os.path.realpath(__file__)) + '/__init__.py')
self.assertTrue(self.submission_page.has_file_error)
# trying to upload a acceptable file
self.submission_page.visit().select_file(os.path.dirname(os.path.realpath(__file__)) + '/README.rst')
self.assertFalse(self.submission_page.has_file_error)
self.submission_page.upload_file()
self.assertTrue(self.submission_page.has_file_uploaded)
if __name__ == "__main__":
# Configure the screenshot directory
......
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