Commit 37c1176a by Pan Luo Committed by muzaffaryousaf

Add support for file upload

- Allow image, pdf-image, or custom file types
- Global black list extensions to prevent unsafe file types
- Custom file upload is white listed only

# Conflicts:
#	AUTHORS
#	openassessment/templates/openassessmentblock/edit/oa_edit.html
#	openassessment/xblock/static/js/openassessment-lms.min.js
#	openassessment/xblock/static/js/openassessment-studio.min.js
#	openassessment/xblock/static/js/spec/lms/oa_response.js
#	openassessment/xblock/static/js/src/lms/oa_base.js
#	openassessment/xblock/static/js/src/lms/oa_response.js
#	openassessment/xblock/static/js/src/oa_server.js
#	openassessment/xblock/static/js/src/studio/oa_edit.js
#	openassessment/xblock/static/js/src/studio/oa_edit_settings.js
#	test/acceptance/tests.py
parent 1f80d7a3
...@@ -17,5 +17,9 @@ Bastien Abadie <bastien@nextcairn.com> ...@@ -17,5 +17,9 @@ Bastien Abadie <bastien@nextcairn.com>
Omar Al-Ithawi <oithawi@qrf.org> Omar Al-Ithawi <oithawi@qrf.org>
Ahsan Ulhaq <ahsan@edx.org> Ahsan Ulhaq <ahsan@edx.org>
Ben Patterson <bpatterson@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> Eric Fischer <efischer@edx.org>
Andy Armstrong <andya@edx.org> Andy Armstrong <andya@edx.org>
...@@ -9,7 +9,7 @@ from django.core.urlresolvers import reverse ...@@ -9,7 +9,7 @@ from django.core.urlresolvers import reverse
class Backend(BaseBackend): 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 that in order to use this file storage backend, you need to include the
urls from openassessment.fileupload in your urls.py file: urls from openassessment.fileupload in your urls.py file:
......
...@@ -101,15 +101,33 @@ ...@@ -101,15 +101,33 @@
</div> </div>
<p class="setting-help">{% trans "The date and time when learners can no longer submit responses." %}</p> <p class="setting-help">{% trans "The date and time when learners can no longer submit responses." %}</p>
</li> </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"> <div class="wrapper-comp-setting">
<label for="openassessment_submission_image_editor" class="setting-label">{% trans "Allow Image Responses"%}</label> <label for="openassessment_submission_upload_selector" class="setting-label">{% trans "Allow File Upload"%}</label>
<select id="openassessment_submission_image_editor" class="input setting-input" name="image submission"> <select id="openassessment_submission_upload_selector" class="input setting-input" name="upload submission">
<option value="0">{% trans "False"%}</option> <option value="">{% trans "None"%}</option>
<option value="1" {% if allow_file_upload %} selected="true" {% endif %}>{% trans "True"%}</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> </select>
</div> </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>
<li id="openassessment_submission_latex_wrapper" class="field comp-setting-entry"> <li id="openassessment_submission_latex_wrapper" class="field comp-setting-entry">
<div class="wrapper-comp-setting"> <div class="wrapper-comp-setting">
......
...@@ -30,15 +30,7 @@ ...@@ -30,15 +30,7 @@
{% include "openassessmentblock/oa_submission_answer.html" with answer=student_submission.answer answer_text_label="Your response to the question above:" %} {% 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 %} {% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url header="Your Upload" class_prefix="submission__answer" %}
<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 %}
</article> </article>
<article class="submission__peer-evaluations step__content__section"> <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 @@ ...@@ -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:" %} {% 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 %} {% 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" %}
<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 %}
</div> </div>
<form id="peer-assessment--001__assessment" class="peer-assessment__assessment" method="post"> <form id="peer-assessment--001__assessment" class="peer-assessment__assessment" method="post">
......
...@@ -52,17 +52,7 @@ ...@@ -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:" %} {% 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 %} {% 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" %}
<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 %}
</div> </div>
<form id="peer-assessment--001__assessment" class="peer-assessment__assessment" method="post"> <form id="peer-assessment--001__assessment" class="peer-assessment__assessment" method="post">
......
...@@ -72,29 +72,21 @@ ...@@ -72,29 +72,21 @@
</div> </div>
</li> </li>
{% endfor %} {% endfor %}
{% if allow_file_upload %} {% if file_upload_type %}
<li class="field"> <li class="field">
<div id="upload__error"> <div id="upload__error">
<div class="message message--inline message--error message--error-server"> <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 class="message__content"></div>
</div> </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"> <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> <button type="submit" id="file__upload" class="action action--upload is--disabled">{% trans "Upload your file" %}</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>
</li> </li>
{% endif %} {% endif %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url class_prefix="submission__answer"%}
</ol> </ol>
<span class="tip">{% trans "You may continue to work on your response until you submit it." %}</span> <span class="tip">{% trans "You may continue to work on your response until you submit it." %}</span>
......
...@@ -24,13 +24,7 @@ ...@@ -24,13 +24,7 @@
{% include "openassessmentblock/oa_submission_answer.html" with answer=student_submission.answer answer_text_label="Your response to the question above:" %} {% 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 %} {% 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" %}
<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 %}
</article> </article>
</div> </div>
</div> </div>
......
...@@ -44,13 +44,7 @@ ...@@ -44,13 +44,7 @@
{% include "openassessmentblock/oa_submission_answer.html" with answer=student_submission.answer answer_text_label="Your response to the question above:" %} {% 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 %} {% 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" %}
<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 %}
</article> </article>
</div> </div>
</div> </div>
......
...@@ -45,17 +45,7 @@ ...@@ -45,17 +45,7 @@
{% include "openassessmentblock/oa_submission_answer.html" with answer=self_submission.answer answer_text_label="Your response to the question above:" %} {% 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 %} {% 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" %}
<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 %}
</article> </article>
<form id="self-assessment--001__assessment" class="self-assessment__assessment" method="post"> <form id="self-assessment--001__assessment" class="self-assessment__assessment" method="post">
......
...@@ -25,12 +25,11 @@ ...@@ -25,12 +25,11 @@
{% endif %} {% endif %}
</div> </div>
{% if submission.image_url %} {% if submission.file_url %}
<img <a href="{{ submission.file_url }}" class="submission--file">
class="submission--image" {% trans "The file associated with this response." %}
alt="{% trans "The image associated with this response" %}" </a>
src="{{ submission.image_url }}" <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 %} {% endif %}
</div> </div>
</div> </div>
......
...@@ -141,7 +141,7 @@ class GradeMixin(object): ...@@ -141,7 +141,7 @@ class GradeMixin(object):
'example_based_assessment': example_based_assessment, 'example_based_assessment': example_based_assessment,
'rubric_criteria': self._rubric_criteria_grade_context(peer_assessments, self_assessment), 'rubric_criteria': self._rubric_criteria_grade_context(peer_assessments, self_assessment),
'has_submitted_feedback': has_submitted_feedback, '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, 'allow_latex': self.allow_latex,
'file_url': self.get_download_url_from_submission(student_submission) 'file_url': self.get_download_url_from_submission(student_submission)
} }
......
...@@ -122,9 +122,21 @@ class OpenAssessmentBlock( ...@@ -122,9 +122,21 @@ class OpenAssessmentBlock(
) )
allow_file_upload = Boolean( 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, 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( allow_latex = Boolean(
...@@ -209,6 +221,46 @@ class OpenAssessmentBlock( ...@@ -209,6 +221,46 @@ class OpenAssessmentBlock(
def course_id(self): def course_id(self):
return self._serialize_opaque_key(self.xmodule_runtime.course_id) # pylint:disable=E1101 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): def get_anonymous_user_id(self, username, course_id):
""" """
Get the anonymous user id from Xblock user service. Get the anonymous user id from Xblock user service.
...@@ -322,10 +374,15 @@ class OpenAssessmentBlock( ...@@ -322,10 +374,15 @@ class OpenAssessmentBlock(
# TODO: load CSS and JavaScript as URLs once they can be served by the CDN # TODO: load CSS and JavaScript as URLs once they can be served by the CDN
fragment.add_css(load(css_url)) fragment.add_css(load(css_url))
fragment.add_javascript(load("static/js/openassessment-lms.min.js")) 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 return fragment
@property @property
def is_admin(self): def is_admin(self):
""" """
...@@ -410,6 +467,22 @@ class OpenAssessmentBlock( ...@@ -410,6 +467,22 @@ class OpenAssessmentBlock(
""" """
return [ 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", "OpenAssessmentBlock Unicode",
load('static/xml/unicode.xml') load('static/xml/unicode.xml')
), ),
...@@ -426,6 +499,10 @@ class OpenAssessmentBlock( ...@@ -426,6 +499,10 @@ class OpenAssessmentBlock(
load('static/xml/leaderboard.xml') load('static/xml/leaderboard.xml')
), ),
( (
"OpenAssessmentBlock Leaderboard with Custom File Type",
load('static/xml/leaderboard_custom.xml')
),
(
"OpenAssessmentBlock (Peer Only) Rubric", "OpenAssessmentBlock (Peer Only) Rubric",
load('static/xml/poverty_peer_only_example.xml') load('static/xml/poverty_peer_only_example.xml')
), ),
...@@ -471,6 +548,8 @@ class OpenAssessmentBlock( ...@@ -471,6 +548,8 @@ class OpenAssessmentBlock(
block.title = config['title'] block.title = config['title']
block.prompts = config['prompts'] block.prompts = config['prompts']
block.allow_file_upload = config['allow_file_upload'] 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.allow_latex = config['allow_latex']
block.leaderboard_show = config['leaderboard_show'] block.leaderboard_show = config['leaderboard_show']
......
...@@ -235,7 +235,7 @@ class PeerAssessmentMixin(object): ...@@ -235,7 +235,7 @@ class PeerAssessmentMixin(object):
context_dict["peer_submission"] = create_submission_dict(peer_sub, self.prompts) context_dict["peer_submission"] = create_submission_dict(peer_sub, self.prompts)
# Determine if file upload is supported for this XBlock. # 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) context_dict["peer_file_url"] = self.get_download_url_from_submission(peer_sub)
else: else:
path = 'openassessmentblock/peer/oa_peer_turbo_mode_waiting.html' path = 'openassessmentblock/peer/oa_peer_turbo_mode_waiting.html'
...@@ -250,7 +250,7 @@ class PeerAssessmentMixin(object): ...@@ -250,7 +250,7 @@ class PeerAssessmentMixin(object):
path = 'openassessmentblock/peer/oa_peer_assessment.html' path = 'openassessmentblock/peer/oa_peer_assessment.html'
context_dict["peer_submission"] = create_submission_dict(peer_sub, self.prompts) context_dict["peer_submission"] = create_submission_dict(peer_sub, self.prompts)
# Determine if file upload is supported for this XBlock. # 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) 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 # Sets the XBlock boolean to signal to Message that it WAS NOT able to grab a submission
self.no_peers = False self.no_peers = False
......
...@@ -63,6 +63,12 @@ VALID_ASSESSMENT_TYPES = [ ...@@ -63,6 +63,12 @@ VALID_ASSESSMENT_TYPES = [
u'student-training' 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. # Schema definition for an update from the Studio JavaScript editor.
EDITOR_UPDATE_SCHEMA = Schema({ EDITOR_UPDATE_SCHEMA = Schema({
...@@ -76,7 +82,12 @@ EDITOR_UPDATE_SCHEMA = Schema({ ...@@ -76,7 +82,12 @@ EDITOR_UPDATE_SCHEMA = Schema({
Required('feedback_default_text'): utf8_validator, Required('feedback_default_text'): utf8_validator,
Required('submission_start'): Any(datetime_validator, None), Required('submission_start'): Any(datetime_validator, None),
Required('submission_due'): 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('allow_latex'): bool,
Required('leaderboard_show'): int, Required('leaderboard_show'): int,
Required('assessments'): [ Required('assessments'): [
......
...@@ -90,8 +90,8 @@ class SelfAssessmentMixin(object): ...@@ -90,8 +90,8 @@ class SelfAssessmentMixin(object):
context["estimated_time"] = "20 minutes" # TODO: Need to configure this. context["estimated_time"] = "20 minutes" # TODO: Need to configure this.
context["self_submission"] = create_submission_dict(submission, self.prompts) context["self_submission"] = create_submission_dict(submission, self.prompts)
# Determine if file upload is supported for this XBlock. # Determine if file upload is supported for this XBlock and what kind of files can be uploaded.
context["allow_file_upload"] = self.allow_file_upload context["file_upload_type"] = self.file_upload_type
context['self_file_url'] = self.get_download_url_from_submission(submission) context['self_file_url'] = self.get_download_url_from_submission(submission)
path = 'openassessmentblock/self/oa_self_assessment.html' path = 'openassessmentblock/self/oa_self_assessment.html'
......
...@@ -253,7 +253,7 @@ class StaffAreaMixin(object): ...@@ -253,7 +253,7 @@ class StaffAreaMixin(object):
file_key = submission['answer']['file_key'] file_key = submission['answer']['file_key']
try: 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: except file_exceptions.FileUploadError:
# Log the error, but do not prevent the rest of the student info # Log the error, but do not prevent the rest of the student info
# from being displayed. # from being displayed.
......
if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}if(typeof window.ngetgext==="undefined"){window.ngettext=function(singular_text,plural_text,n){if(n>1){return plural_text}else{return singular_text}}}if(typeof window.Logger==="undefined"){window.Logger={log:function(){}}}if(typeof window.MathJax==="undefined"){window.MathJax={Hub:{Typeset:function(){},Queue:function(){}}}}if(typeof OpenAssessment.Server==="undefined"||!OpenAssessment.Server){OpenAssessment.Server=function(runtime,element){this.runtime=runtime;this.element=element};var jsonContentType="application/json; charset=utf-8";OpenAssessment.Server.prototype={url:function(handler){return this.runtime.handlerUrl(this.element,handler)},render:function(component){var view=this;var url=this.url("render_"+component);return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html"}).done(function(data){defer.resolveWith(view,[data])}).fail(function(){defer.rejectWith(view,[gettext("This section could not be loaded.")])})}).promise()},renderLatex:function(element){element.filter(".allow--latex").each(function(){MathJax.Hub.Queue(["Typeset",MathJax.Hub,this])})},renderContinuedPeer:function(){var view=this;var url=this.url("render_peer_assessment");return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html",data:{continue_grading:true}}).done(function(data){defer.resolveWith(view,[data])}).fail(function(){defer.rejectWith(view,[gettext("This section could not be loaded.")])})}).promise()},studentInfo:function(student_username){var url=this.url("render_student_info");return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html",data:{student_username:student_username}}).done(function(data){defer.resolveWith(this,[data])}).fail(function(){defer.rejectWith(this,[gettext("This section could not be loaded.")])})}).promise()},submit:function(submission){var url=this.url("submit");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({submission:submission}),contentType:jsonContentType}).done(function(data){var success=data[0];if(success){var studentId=data[1];var attemptNum=data[2];defer.resolveWith(this,[studentId,attemptNum])}else{var errorNum=data[1];var errorMsg=data[2];defer.rejectWith(this,[errorNum,errorMsg])}}).fail(function(){defer.rejectWith(this,["AJAX",gettext("This response could not be submitted.")])})}).promise()},save:function(submission){var url=this.url("save_submission");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({submission:submission}),contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This response could not be saved.")])})}).promise()},submitFeedbackOnAssessment:function(text,options){var url=this.url("submit_feedback");var payload=JSON.stringify({feedback_text:text,feedback_options:options});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This feedback could not be submitted.")])})}).promise()},peerAssess:function(optionsSelected,criterionFeedback,overallFeedback,uuid){var url=this.url("peer_assess");var payload=JSON.stringify({options_selected:optionsSelected,criterion_feedback:criterionFeedback,overall_feedback:overallFeedback,submission_uuid:uuid});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})}).promise()},selfAssess:function(optionsSelected,criterionFeedback,overallFeedback){var url=this.url("self_assess");var payload=JSON.stringify({options_selected:optionsSelected,criterion_feedback:criterionFeedback,overall_feedback:overallFeedback});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},trainingAssess:function(optionsSelected){var url=this.url("training_assess");var payload=JSON.stringify({options_selected:optionsSelected});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.corrections])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},scheduleTraining:function(){var url=this.url("schedule_training");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""',contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},rescheduleUnfinishedTasks:function(){var url=this.url("reschedule_unfinished_tasks");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""',contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("One or more rescheduling tasks failed.")])})})},updateEditorContext:function(kwargs){var url=this.url("update_editor_context");var payload=JSON.stringify({prompts:kwargs.prompts,feedback_prompt:kwargs.feedbackPrompt,feedback_default_text:kwargs.feedback_default_text,title:kwargs.title,submission_start:kwargs.submissionStart,submission_due:kwargs.submissionDue,criteria:kwargs.criteria,assessments:kwargs.assessments,editor_assessments_order:kwargs.editorAssessmentsOrder,allow_file_upload:kwargs.imageSubmissionEnabled,allow_latex:kwargs.latexEnabled,leaderboard_show:kwargs.leaderboardNum});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This problem could not be saved.")])})}).promise()},checkReleased:function(){var url=this.url("check_released");var payload='""';return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.is_released])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("The server could not be contacted.")])})}).promise()},getUploadUrl:function(contentType){var url=this.url("upload_url");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({contentType:contentType}),contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("Could not retrieve upload url.")])})}).promise()},getDownloadUrl:function(){var url=this.url("download_url");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({}),contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("Could not retrieve download url.")])})}).promise()},cancelSubmission:function(submissionUUID,comments){var url=this.url("cancel_submission");var payload=JSON.stringify({submission_uuid:submissionUUID,comments:comments});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("The submission could not be removed from the grading pool.")])})}).promise()}}}if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}if(typeof window.ngetgext==="undefined"){window.ngettext=function(singular_text,plural_text,n){if(n>1){return plural_text}else{return singular_text}}}if(typeof window.Logger==="undefined"){window.Logger={log:function(){}}}if(typeof window.MathJax==="undefined"){window.MathJax={Hub:{Typeset:function(){},Queue:function(){}}}}OpenAssessment.BaseView=function(runtime,element,server){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.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);this.gradeView=new OpenAssessment.GradeView(this.element,this.server,this);this.leaderboardView=new OpenAssessment.LeaderboardView(this.element,this.server,this);this.messageView=new OpenAssessment.MessageView(this.element,this.server,this);this.staffAreaView=new OpenAssessment.StaffAreaView(this.element,this.server,this)};OpenAssessment.BaseView.prototype={scrollToTop:function(){if($.scrollTo instanceof Function){$(window).scrollTo($("#openassessment__steps",this.element),800,{offset:-50})}},setUpCollapseExpand:function(parentSel){parentSel.on("click",".ui-toggle-visibility__control",function(eventData){var sel=$(eventData.target).closest(".ui-toggle-visibility");sel.toggleClass("is--collapsed")})},load:function(){this.responseView.load();this.loadAssessmentModules();this.staffAreaView.load()},loadAssessmentModules:function(){this.trainingView.load();this.peerView.load();this.selfView.load();this.gradeView.load();this.leaderboardView.load()},loadMessageView:function(){this.messageView.load()},toggleActionError:function(type,msg){var element=this.element;var container=null;if(type==="save"){container=".response__submission__actions"}else if(type==="submit"||type==="peer"||type==="self"||type==="student-training"){container=".step__actions"}else if(type==="feedback_assess"){container=".submission__feedback__actions"}else if(type==="upload"){container="#upload__error"}if(container===null){if(msg!==null){console.log(msg)}}else{var msgHtml=msg===null?"":msg;$(container+" .message__content",element).html("<p>"+msgHtml+"</p>");$(container,element).toggleClass("has--error",msg!==null)}},showLoadError:function(step){var container="#openassessment__"+step;$(container).toggleClass("has--error",true);$(container+" .step__status__value i").removeClass().addClass("ico icon-warning-sign");$(container+" .step__status__value .copy").html(gettext("Unable to Load"))}};function OpenAssessmentBlock(runtime,element){var server=new OpenAssessment.Server(runtime,element);var view=new OpenAssessment.BaseView(runtime,element,server);view.load()}OpenAssessment.FileUploader=function(){this.upload=function(url,file){return $.Deferred(function(defer){$.ajax({url:url,type:"PUT",data:file,async:false,processData:false,contentType:file.type}).done(function(){Logger.log("openassessment.upload_file",{fileName:file.name,fileSize:file.size,fileType:file.type});defer.resolve()}).fail(function(data,textStatus){defer.rejectWith(this,[textStatus])})}).promise()}};OpenAssessment.GradeView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.GradeView.prototype={load:function(){var view=this;var baseView=this.baseView;this.server.render("grade").done(function(html){$("#openassessment__grade",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__grade",view.element));view.installHandlers()}).fail(function(errMsg){baseView.showLoadError("grade",errMsg)})},installHandlers:function(){var sel=$("#openassessment__grade",this.element);this.baseView.setUpCollapseExpand(sel);var view=this;sel.find("#feedback__submit").click(function(eventObject){eventObject.preventDefault();view.submitFeedbackOnAssessment()})},feedbackText:function(text){if(typeof text==="undefined"){return $("#feedback__remarks__value",this.element).val()}else{$("#feedback__remarks__value",this.element).val(text)}},feedbackOptions:function(options){var view=this;if(typeof options==="undefined"){return $.map($(".feedback__overall__value:checked",view.element),function(element){return $(element).val()})}else{$(".feedback__overall__value",this.element).prop("checked",false);$.each(options,function(index,opt){$("#feedback__overall__value--"+opt,view.element).prop("checked",true)})}},setHidden:function(sel,hidden){sel.toggleClass("is--hidden",hidden);sel.attr("aria-hidden",hidden?"true":"false")},isHidden:function(sel){return sel.hasClass("is--hidden")&&sel.attr("aria-hidden")==="true"},feedbackState:function(newState){var containerSel=$(".submission__feedback__content",this.element);var instructionsSel=containerSel.find(".submission__feedback__instructions");var fieldsSel=containerSel.find(".submission__feedback__fields");var actionsSel=containerSel.find(".submission__feedback__actions");var transitionSel=containerSel.find(".transition__status");var messageSel=containerSel.find(".message--complete");if(typeof newState==="undefined"){var isSubmitting=containerSel.hasClass("is--transitioning")&&containerSel.hasClass("is--submitting")&&!this.isHidden(transitionSel)&&this.isHidden(messageSel)&&this.isHidden(instructionsSel)&&this.isHidden(fieldsSel)&&this.isHidden(actionsSel);var hasSubmitted=containerSel.hasClass("is--submitted")&&this.isHidden(transitionSel)&&!this.isHidden(messageSel)&&this.isHidden(instructionsSel)&&this.isHidden(fieldsSel)&&this.isHidden(actionsSel);var isOpen=!containerSel.hasClass("is--submitted")&&!containerSel.hasClass("is--transitioning")&&!containerSel.hasClass("is--submitting")&&this.isHidden(transitionSel)&&this.isHidden(messageSel)&&!this.isHidden(instructionsSel)&&!this.isHidden(fieldsSel)&&!this.isHidden(actionsSel);if(isOpen){return"open"}else if(isSubmitting){return"submitting"}else if(hasSubmitted){return"submitted"}else{throw"Invalid feedback state"}}else{if(newState==="open"){containerSel.toggleClass("is--transitioning",false);containerSel.toggleClass("is--submitting",false);containerSel.toggleClass("is--submitted",false);this.setHidden(instructionsSel,false);this.setHidden(fieldsSel,false);this.setHidden(actionsSel,false);this.setHidden(transitionSel,true);this.setHidden(messageSel,true)}else if(newState==="submitting"){containerSel.toggleClass("is--transitioning",true);containerSel.toggleClass("is--submitting",true);containerSel.toggleClass("is--submitted",false);this.setHidden(instructionsSel,true);this.setHidden(fieldsSel,true);this.setHidden(actionsSel,true);this.setHidden(transitionSel,false);this.setHidden(messageSel,true)}else if(newState==="submitted"){containerSel.toggleClass("is--transitioning",false);containerSel.toggleClass("is--submitting",false);containerSel.toggleClass("is--submitted",true);this.setHidden(instructionsSel,true);this.setHidden(fieldsSel,true);this.setHidden(actionsSel,true);this.setHidden(transitionSel,true);this.setHidden(messageSel,false)}}},submitFeedbackOnAssessment:function(){var view=this;var baseView=this.baseView;$("#feedback__submit",this.element).toggleClass("is--disabled",true);view.feedbackState("submitting");this.server.submitFeedbackOnAssessment(this.feedbackText(),this.feedbackOptions()).done(function(){view.feedbackState("submitted")}).fail(function(errMsg){baseView.toggleActionError("feedback_assess",errMsg)})}};OpenAssessment.LeaderboardView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.LeaderboardView.prototype={load:function(){var view=this;var baseView=this.baseView;this.server.render("leaderboard").done(function(html){$("#openassessment__leaderboard",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__leaderboard",view.element))}).fail(function(errMsg){baseView.showLoadError("leaderboard",errMsg)})}};OpenAssessment.MessageView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.MessageView.prototype={load:function(){var view=this;var baseView=this.baseView;this.server.render("message").done(function(html){$("#openassessment__message",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__message",view.element))}).fail(function(errMsg){baseView.showLoadError("message",errMsg)})}};OpenAssessment.PeerView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.PeerView.prototype={load:function(){var view=this;this.server.render("peer_assessment").done(function(html){$("#openassessment__peer-assessment",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__peer-assessment",view.element));view.installHandlers(false)}).fail(function(){view.baseView.showLoadError("peer-assessment")});view.baseView.loadMessageView()},loadContinuedAssessment:function(){var view=this;view.continueAssessmentEnabled(false);this.server.renderContinuedPeer().done(function(html){$("#openassessment__peer-assessment",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__peer-assessment",view.element));view.installHandlers(true)}).fail(function(){view.baseView.showLoadError("peer-assessment");view.continueAssessmentEnabled(true)})},continueAssessmentEnabled:function(enabled){var button=$("#peer-assessment__continue__grading",this.element);if(typeof enabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!enabled)}},installHandlers:function(isContinuedAssessment){var sel=$("#openassessment__peer-assessment",this.element);var view=this;this.baseView.setUpCollapseExpand(sel);var rubricSelector=$("#peer-assessment--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(view.peerSubmitEnabled,view))}sel.find("#peer-assessment--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();if(!isContinuedAssessment){view.peerAssess()}else{view.continuedPeerAssess()}});sel.find("#peer-assessment__continue__grading").click(function(eventObject){eventObject.preventDefault();view.loadContinuedAssessment()})},peerSubmitEnabled:function(enabled){var button=$("#peer-assessment--001__assessment__submit",this.element);if(typeof enabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!enabled)}},peerAssess:function(){var view=this;var baseView=view.baseView;this.peerAssessRequest(function(){baseView.loadAssessmentModules();baseView.scrollToTop()})},continuedPeerAssess:function(){var view=this;var gradeView=this.baseView.gradeView;var baseView=view.baseView;view.peerAssessRequest(function(){view.loadContinuedAssessment();gradeView.load();baseView.scrollToTop()})},peerAssessRequest:function(successFunction){var view=this;var uuid=$("#openassessment__peer-assessment").data("submission-uuid");view.baseView.toggleActionError("peer",null);view.peerSubmitEnabled(false);this.server.peerAssess(this.rubric.optionsSelected(),this.rubric.criterionFeedback(),this.rubric.overallFeedback(),uuid).done(successFunction).fail(function(errMsg){view.baseView.toggleActionError("peer",errMsg);view.peerSubmitEnabled(true)})}};OpenAssessment.ResponseView=function(element,server,fileUploader,baseView){this.element=element;this.server=server;this.fileUploader=fileUploader;this.baseView=baseView;this.savedResponse=[];this.files=null;this.imageType=null;this.lastChangeTime=Date.now();this.errorOnLastSave=false;this.autoSaveTimerId=null};OpenAssessment.ResponseView.prototype={AUTO_SAVE_POLL_INTERVAL:2e3,AUTO_SAVE_WAIT:3e4,MAX_FILE_SIZE:5242880,load:function(){var view=this;this.server.render("submission").done(function(html){$("#openassessment__response",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__response",view.element));view.installHandlers();view.setAutoSaveEnabled(true)}).fail(function(){view.baseView.showLoadError("response")})},installHandlers:function(){var sel=$("#openassessment__response",this.element);var view=this;this.baseView.setUpCollapseExpand(sel);this.savedResponse=this.response();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)};sel.find("input[type=file]").on("change",handlePrepareUpload);sel.find("#submission__preview__item").hide();sel.find("#step--response__submit").click(function(eventObject){eventObject.preventDefault();view.submit()});sel.find("#submission__save").click(function(eventObject){eventObject.preventDefault();view.save()});sel.find("#submission__preview").click(function(eventObject){eventObject.preventDefault();var preview_text=sel.find(".submission__answer__part__text__value").val();var preview_container=sel.find("#preview_content");preview_container.html(preview_text.replace(/\r\n|\r|\n/g,"<br />"));sel.find("#submission__preview__item").show();MathJax.Hub.Queue(["Typeset",MathJax.Hub,preview_container[0]])});sel.find("#file__upload").click(function(eventObject){eventObject.preventDefault();$(".submission__answer__display__image",view.element).removeClass("is--hidden");view.fileUpload()})},setAutoSaveEnabled:function(enabled){if(enabled){if(this.autoSaveTimerId===null){this.autoSaveTimerId=setInterval($.proxy(this.autoSave,this),this.AUTO_SAVE_POLL_INTERVAL)}}else{if(this.autoSaveTimerId!==null){clearInterval(this.autoSaveTimerId)}}},submitEnabled:function(enabled){var sel=$("#step--response__submit",this.element);if(typeof enabled==="undefined"){return!sel.hasClass("is--disabled")}else{sel.toggleClass("is--disabled",!enabled)}},saveEnabled:function(enabled){var sel=$("#submission__save",this.element);if(typeof enabled==="undefined"){return!sel.hasClass("is--disabled")}else{sel.toggleClass("is--disabled",!enabled)}},previewEnabled:function(enabled){var sel=$("#submission__preview",this.element);if(typeof enabled==="undefined"){return!sel.hasClass("is--disabled")}else{sel.toggleClass("is--disabled",!enabled)}},saveStatus:function(msg){var sel=$("#response__save_status h3",this.element);if(typeof msg==="undefined"){return sel.text()}else{var label=gettext("Status of Your Response");sel.html('<span class="sr">'+label+":"+"</span>\n"+msg)}},unsavedWarningEnabled:function(enabled){if(typeof enabled==="undefined"){return window.onbeforeunload!==null}else{if(enabled){window.onbeforeunload=function(){return gettext("If you leave this page without saving or submitting your response, you'll lose any work you've done on the response.")}}else{window.onbeforeunload=null}}},response:function(texts){var sel=$(".submission__answer__part__text__value",this.element);if(typeof texts==="undefined"){return sel.map(function(){return $.trim($(this).val())}).get()}else{sel.map(function(index){$(this).val(texts[index])})}},responseChanged:function(){var savedResponse=this.savedResponse;return this.response().some(function(element,index){return element!==savedResponse[index]})},autoSave:function(){var timeSinceLastChange=Date.now()-this.lastChangeTime;if(this.responseChanged()&&timeSinceLastChange>this.AUTO_SAVE_WAIT&&!this.errorOnLastSave){this.save()}},handleResponseChanged:function(){var isNotBlank=!this.response().every(function(element){return $.trim(element)===""});this.submitEnabled(isNotBlank);if(this.responseChanged()){this.saveEnabled(isNotBlank);this.previewEnabled(isNotBlank);this.saveStatus(gettext("This response has not been saved."));this.unsavedWarningEnabled(true)}this.lastChangeTime=Date.now()},save:function(){this.errorOnLastSave=false;this.saveStatus(gettext("Saving..."));this.baseView.toggleActionError("save",null);this.unsavedWarningEnabled(false);var view=this;var savedResponse=this.response();this.server.save(savedResponse).done(function(){view.savedResponse=savedResponse;var currentResponse=view.response();var currentResponseIsEmpty=currentResponse.every(function(element){return element===""});view.submitEnabled(!currentResponseIsEmpty);var currentResponseEqualsSaved=currentResponse.every(function(element,index){return element===savedResponse[index]});if(currentResponseEqualsSaved){view.saveEnabled(false);view.saveStatus(gettext("This response has been saved but not submitted."))}}).fail(function(errMsg){view.saveStatus(gettext("Error"));view.baseView.toggleActionError("save",errMsg);view.errorOnLastSave=true})},submit:function(){this.submitEnabled(false);var view=this;var baseView=this.baseView;this.confirmSubmission().pipe(function(){var submission=view.response();baseView.toggleActionError("response",null);return view.server.submit(submission)}).done($.proxy(view.moveToNextStep,view)).fail(function(errCode,errMsg){if(errCode==="ENOMULTI"){view.moveToNextStep()}else{if(errMsg){baseView.toggleActionError("submit",errMsg)}view.submitEnabled(true)}})},moveToNextStep:function(){this.load();this.baseView.loadAssessmentModules();this.unsavedWarningEnabled(false)},confirmSubmission:function(){var msg=gettext("You're about to submit your response for this assignment. After you submit this response, you can't change it or submit a new response.");return $.Deferred(function(defer){if(confirm(msg)){defer.resolve()}else{defer.reject()}})},prepareUpload:function(files){this.files=null;this.imageType=files[0].type;if(files[0].size>this.MAX_FILE_SIZE){this.baseView.toggleActionError("upload",gettext("File size must be 5MB or less."))}else if(this.imageType.substring(0,6)!=="image/"){this.baseView.toggleActionError("upload",gettext("File must be an image."))}else{this.baseView.toggleActionError("upload",null);this.files=files}$("#file__upload").toggleClass("is--disabled",this.files===null)},fileUpload:function(){var view=this;var fileUpload=$("#file__upload");fileUpload.addClass("is--disabled");var handleError=function(errMsg){view.baseView.toggleActionError("upload",errMsg);fileUpload.removeClass("is--disabled")};this.server.getUploadUrl(view.imageType).done(function(url){var image=view.files[0];view.fileUploader.upload(url,image).done(function(){view.imageUrl();view.baseView.toggleActionError("upload",null)}).fail(handleError)}).fail(handleError)},imageUrl:function(){var view=this;var image=$("#submission__answer__image",view.element);view.server.getDownloadUrl().done(function(url){image.attr("src",url);return url})}};OpenAssessment.Rubric=function(element){this.element=element};OpenAssessment.Rubric.prototype={criterionFeedback:function(criterionFeedback){var selector="textarea.answer__value";var feedback={};$(selector,this.element).each(function(index,sel){if(typeof criterionFeedback!=="undefined"){$(sel).val(criterionFeedback[sel.name]);feedback[sel.name]=criterionFeedback[sel.name]}else{feedback[sel.name]=$(sel).val()}});return feedback},overallFeedback:function(overallFeedback){var selector="#assessment__rubric__question--feedback__value";if(typeof overallFeedback==="undefined"){return $(selector,this.element).val()}else{$(selector,this.element).val(overallFeedback)}},optionsSelected:function(optionsSelected){var selector="input[type=radio]";if(typeof optionsSelected==="undefined"){var options={};$(selector+":checked",this.element).each(function(index,sel){options[sel.name]=sel.value});return options}else{$(selector,this.element).prop("checked",false);$(selector,this.element).each(function(index,sel){if(optionsSelected.hasOwnProperty(sel.name)){if(sel.value===optionsSelected[sel.name]){$(sel).prop("checked",true)}}})}},canSubmitCallback:function(callback){var rubric=this;callback(rubric.canSubmit());$(this.element).on("change keyup drop paste",function(){callback(rubric.canSubmit())})},canSubmit:function(){var numChecked=$("input[type=radio]:checked",this.element).length;var numAvailable=$(".field--radio.assessment__rubric__question.has--options",this.element).length;var completedRequiredComments=true;$("textarea[required]",this.element).each(function(){var trimmedText=$.trim($(this).val());if(trimmedText===""){completedRequiredComments=false}});return numChecked===numAvailable&&completedRequiredComments},showCorrections:function(corrections){var selector="input[type=radio]";var hasErrors=false;$(selector,this.element).each(function(index,sel){var listItem=$(sel).parents(".assessment__rubric__question");if(corrections.hasOwnProperty(sel.name)){hasErrors=true;listItem.find(".message--incorrect").removeClass("is--hidden");listItem.find(".message--correct").addClass("is--hidden")}else{listItem.find(".message--correct").removeClass("is--hidden");listItem.find(".message--incorrect").addClass("is--hidden")}});return hasErrors}};OpenAssessment.SelfView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.SelfView.prototype={load:function(){var view=this;this.server.render("self_assessment").done(function(html){$("#openassessment__self-assessment",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__self-assessment",view.element));view.installHandlers()}).fail(function(){view.showLoadError("self-assessment")})},installHandlers:function(){var view=this;var sel=$("#openassessment__self-assessment",view.element);this.baseView.setUpCollapseExpand(sel);var rubricSelector=$("#self-assessment--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(this.selfSubmitEnabled,this))}sel.find("#self-assessment--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();view.selfAssess()})},selfSubmitEnabled:function(enabled){var button=$("#self-assessment--001__assessment__submit",this.element);if(typeof enabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!enabled)}},selfAssess:function(){var view=this;var baseView=this.baseView;baseView.toggleActionError("self",null);view.selfSubmitEnabled(false);this.server.selfAssess(this.rubric.optionsSelected(),this.rubric.criterionFeedback(),this.rubric.overallFeedback()).done(function(){baseView.loadAssessmentModules();baseView.scrollToTop()}).fail(function(errMsg){baseView.toggleActionError("self",errMsg);view.selfSubmitEnabled(true)})}};!function(OpenAssessment){"use strict";OpenAssessment.StaffAreaView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.StaffAreaView.prototype={load:function(){var view=this;if($("#openassessment__staff-area",view.element).length>0){this.server.render("staff_area").done(function(html){$("#openassessment__staff-area",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__staff-area",view.element));view.installHandlers()}).fail(function(){view.baseView.showLoadError("staff_area")})}},loadStudentInfo:function(){var view=this;var sel=$("#openassessment__staff-tools",this.element);var student_username=sel.find("#openassessment__student_username").val();this.server.studentInfo(student_username).done(function(html){$("#openassessment__student-info",view.element).replaceWith(html);var selCancelSub=$("#openassessment__staff-info__cancel__submission",view.element);selCancelSub.on("click","#submit_cancel_submission",function(eventObject){eventObject.preventDefault(); if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}if(typeof window.ngetgext==="undefined"){window.ngettext=function(singular_text,plural_text,n){if(n>1){return plural_text}else{return singular_text}}}if(typeof window.Logger==="undefined"){window.Logger={log:function(){}}}if(typeof window.MathJax==="undefined"){window.MathJax={Hub:{Typeset:function(){},Queue:function(){}}}}if(typeof OpenAssessment.Server==="undefined"||!OpenAssessment.Server){OpenAssessment.Server=function(runtime,element){this.runtime=runtime;this.element=element};var jsonContentType="application/json; charset=utf-8";OpenAssessment.Server.prototype={url:function(handler){return this.runtime.handlerUrl(this.element,handler)},render:function(component){var view=this;var url=this.url("render_"+component);return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html"}).done(function(data){defer.resolveWith(view,[data])}).fail(function(){defer.rejectWith(view,[gettext("This section could not be loaded.")])})}).promise()},renderLatex:function(element){element.filter(".allow--latex").each(function(){MathJax.Hub.Queue(["Typeset",MathJax.Hub,this])})},renderContinuedPeer:function(){var view=this;var url=this.url("render_peer_assessment");return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html",data:{continue_grading:true}}).done(function(data){defer.resolveWith(view,[data])}).fail(function(){defer.rejectWith(view,[gettext("This section could not be loaded.")])})}).promise()},studentInfo:function(student_username){var url=this.url("render_student_info");return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html",data:{student_username:student_username}}).done(function(data){defer.resolveWith(this,[data])}).fail(function(){defer.rejectWith(this,[gettext("This section could not be loaded.")])})}).promise()},submit:function(submission){var url=this.url("submit");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({submission:submission}),contentType:jsonContentType}).done(function(data){var success=data[0];if(success){var studentId=data[1];var attemptNum=data[2];defer.resolveWith(this,[studentId,attemptNum])}else{var errorNum=data[1];var errorMsg=data[2];defer.rejectWith(this,[errorNum,errorMsg])}}).fail(function(){defer.rejectWith(this,["AJAX",gettext("This response could not be submitted.")])})}).promise()},save:function(submission){var url=this.url("save_submission");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({submission:submission}),contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This response could not be saved.")])})}).promise()},submitFeedbackOnAssessment:function(text,options){var url=this.url("submit_feedback");var payload=JSON.stringify({feedback_text:text,feedback_options:options});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This feedback could not be submitted.")])})}).promise()},peerAssess:function(optionsSelected,criterionFeedback,overallFeedback,uuid){var url=this.url("peer_assess");var payload=JSON.stringify({options_selected:optionsSelected,criterion_feedback:criterionFeedback,overall_feedback:overallFeedback,submission_uuid:uuid});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})}).promise()},selfAssess:function(optionsSelected,criterionFeedback,overallFeedback){var url=this.url("self_assess");var payload=JSON.stringify({options_selected:optionsSelected,criterion_feedback:criterionFeedback,overall_feedback:overallFeedback});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},trainingAssess:function(optionsSelected){var url=this.url("training_assess");var payload=JSON.stringify({options_selected:optionsSelected});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.corrections])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},scheduleTraining:function(){var url=this.url("schedule_training");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""',contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},rescheduleUnfinishedTasks:function(){var url=this.url("reschedule_unfinished_tasks");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""',contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("One or more rescheduling tasks failed.")])})})},updateEditorContext:function(kwargs){var url=this.url("update_editor_context");var payload=JSON.stringify({prompts:kwargs.prompts,feedback_prompt:kwargs.feedbackPrompt,feedback_default_text:kwargs.feedback_default_text,title:kwargs.title,submission_start:kwargs.submissionStart,submission_due:kwargs.submissionDue,criteria:kwargs.criteria,assessments:kwargs.assessments,editor_assessments_order:kwargs.editorAssessmentsOrder,file_upload_type:kwargs.fileUploadType,white_listed_file_types:kwargs.fileTypeWhiteList,allow_latex:kwargs.latexEnabled,leaderboard_show:kwargs.leaderboardNum});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This problem could not be saved.")])})}).promise()},checkReleased:function(){var url=this.url("check_released");var payload='""';return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.is_released])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("The server could not be contacted.")])})}).promise()},getUploadUrl:function(contentType,filename){var url=this.url("upload_url");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({contentType:contentType,filename:filename}),contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("Could not retrieve upload url.")])})}).promise()},getDownloadUrl:function(){var url=this.url("download_url");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({}),contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("Could not retrieve download url.")])})}).promise()},cancelSubmission:function(submissionUUID,comments){var url=this.url("cancel_submission");var payload=JSON.stringify({submission_uuid:submissionUUID,comments:comments});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("The submission could not be removed from the grading pool.")])})}).promise()}}}if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}if(typeof window.ngetgext==="undefined"){window.ngettext=function(singular_text,plural_text,n){if(n>1){return plural_text}else{return singular_text}}}if(typeof window.Logger==="undefined"){window.Logger={log:function(){}}}if(typeof window.MathJax==="undefined"){window.MathJax={Hub:{Typeset:function(){},Queue:function(){}}}}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,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);this.gradeView=new OpenAssessment.GradeView(this.element,this.server,this);this.leaderboardView=new OpenAssessment.LeaderboardView(this.element,this.server,this);this.messageView=new OpenAssessment.MessageView(this.element,this.server,this);this.staffAreaView=new OpenAssessment.StaffAreaView(this.element,this.server,this)};OpenAssessment.BaseView.prototype={scrollToTop:function(){if($.scrollTo instanceof Function){$(window).scrollTo($("#openassessment__steps",this.element),800,{offset:-50})}},setUpCollapseExpand:function(parentSel){parentSel.on("click",".ui-toggle-visibility__control",function(eventData){var sel=$(eventData.target).closest(".ui-toggle-visibility");sel.toggleClass("is--collapsed")})},load:function(){this.responseView.load();this.loadAssessmentModules();this.staffAreaView.load()},loadAssessmentModules:function(){this.trainingView.load();this.peerView.load();this.selfView.load();this.gradeView.load();this.leaderboardView.load()},loadMessageView:function(){this.messageView.load()},toggleActionError:function(type,msg){var element=this.element;var container=null;if(type==="save"){container=".response__submission__actions"}else if(type==="submit"||type==="peer"||type==="self"||type==="student-training"){container=".step__actions"}else if(type==="feedback_assess"){container=".submission__feedback__actions"}else if(type==="upload"){container="#upload__error"}if(container===null){if(msg!==null){console.log(msg)}}else{var msgHtml=msg===null?"":msg;$(container+" .message__content",element).html("<p>"+msgHtml+"</p>");$(container,element).toggleClass("has--error",msg!==null)}},showLoadError:function(step){var container="#openassessment__"+step;$(container).toggleClass("has--error",true);$(container+" .step__status__value i").removeClass().addClass("ico icon-warning-sign");$(container+" .step__status__value .copy").html(gettext("Unable to Load"))}};function OpenAssessmentBlock(runtime,element,data){var server=new OpenAssessment.Server(runtime,element);var view=new OpenAssessment.BaseView(runtime,element,server,data);view.load()}OpenAssessment.FileUploader=function(){this.upload=function(url,file){return $.Deferred(function(defer){$.ajax({url:url,type:"PUT",data:file,async:false,processData:false,contentType:file.type}).done(function(){Logger.log("openassessment.upload_file",{fileName:file.name,fileSize:file.size,fileType:file.type});defer.resolve()}).fail(function(data,textStatus){defer.rejectWith(this,[textStatus])})}).promise()}};OpenAssessment.GradeView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.GradeView.prototype={load:function(){var view=this;var baseView=this.baseView;this.server.render("grade").done(function(html){$("#openassessment__grade",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__grade",view.element));view.installHandlers()}).fail(function(errMsg){baseView.showLoadError("grade",errMsg)})},installHandlers:function(){var sel=$("#openassessment__grade",this.element);this.baseView.setUpCollapseExpand(sel);var view=this;sel.find("#feedback__submit").click(function(eventObject){eventObject.preventDefault();view.submitFeedbackOnAssessment()})},feedbackText:function(text){if(typeof text==="undefined"){return $("#feedback__remarks__value",this.element).val()}else{$("#feedback__remarks__value",this.element).val(text)}},feedbackOptions:function(options){var view=this;if(typeof options==="undefined"){return $.map($(".feedback__overall__value:checked",view.element),function(element){return $(element).val()})}else{$(".feedback__overall__value",this.element).prop("checked",false);$.each(options,function(index,opt){$("#feedback__overall__value--"+opt,view.element).prop("checked",true)})}},setHidden:function(sel,hidden){sel.toggleClass("is--hidden",hidden);sel.attr("aria-hidden",hidden?"true":"false")},isHidden:function(sel){return sel.hasClass("is--hidden")&&sel.attr("aria-hidden")==="true"},feedbackState:function(newState){var containerSel=$(".submission__feedback__content",this.element);var instructionsSel=containerSel.find(".submission__feedback__instructions");var fieldsSel=containerSel.find(".submission__feedback__fields");var actionsSel=containerSel.find(".submission__feedback__actions");var transitionSel=containerSel.find(".transition__status");var messageSel=containerSel.find(".message--complete");if(typeof newState==="undefined"){var isSubmitting=containerSel.hasClass("is--transitioning")&&containerSel.hasClass("is--submitting")&&!this.isHidden(transitionSel)&&this.isHidden(messageSel)&&this.isHidden(instructionsSel)&&this.isHidden(fieldsSel)&&this.isHidden(actionsSel);var hasSubmitted=containerSel.hasClass("is--submitted")&&this.isHidden(transitionSel)&&!this.isHidden(messageSel)&&this.isHidden(instructionsSel)&&this.isHidden(fieldsSel)&&this.isHidden(actionsSel);var isOpen=!containerSel.hasClass("is--submitted")&&!containerSel.hasClass("is--transitioning")&&!containerSel.hasClass("is--submitting")&&this.isHidden(transitionSel)&&this.isHidden(messageSel)&&!this.isHidden(instructionsSel)&&!this.isHidden(fieldsSel)&&!this.isHidden(actionsSel);if(isOpen){return"open"}else if(isSubmitting){return"submitting"}else if(hasSubmitted){return"submitted"}else{throw"Invalid feedback state"}}else{if(newState==="open"){containerSel.toggleClass("is--transitioning",false);containerSel.toggleClass("is--submitting",false);containerSel.toggleClass("is--submitted",false);this.setHidden(instructionsSel,false);this.setHidden(fieldsSel,false);this.setHidden(actionsSel,false);this.setHidden(transitionSel,true);this.setHidden(messageSel,true)}else if(newState==="submitting"){containerSel.toggleClass("is--transitioning",true);containerSel.toggleClass("is--submitting",true);containerSel.toggleClass("is--submitted",false);this.setHidden(instructionsSel,true);this.setHidden(fieldsSel,true);this.setHidden(actionsSel,true);this.setHidden(transitionSel,false);this.setHidden(messageSel,true)}else if(newState==="submitted"){containerSel.toggleClass("is--transitioning",false);containerSel.toggleClass("is--submitting",false);containerSel.toggleClass("is--submitted",true);this.setHidden(instructionsSel,true);this.setHidden(fieldsSel,true);this.setHidden(actionsSel,true);this.setHidden(transitionSel,true);this.setHidden(messageSel,false)}}},submitFeedbackOnAssessment:function(){var view=this;var baseView=this.baseView;$("#feedback__submit",this.element).toggleClass("is--disabled",true);view.feedbackState("submitting");this.server.submitFeedbackOnAssessment(this.feedbackText(),this.feedbackOptions()).done(function(){view.feedbackState("submitted")}).fail(function(errMsg){baseView.toggleActionError("feedback_assess",errMsg)})}};OpenAssessment.LeaderboardView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.LeaderboardView.prototype={load:function(){var view=this;var baseView=this.baseView;this.server.render("leaderboard").done(function(html){$("#openassessment__leaderboard",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__leaderboard",view.element))}).fail(function(errMsg){baseView.showLoadError("leaderboard",errMsg)})}};OpenAssessment.MessageView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.MessageView.prototype={load:function(){var view=this;var baseView=this.baseView;this.server.render("message").done(function(html){$("#openassessment__message",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__message",view.element))}).fail(function(errMsg){baseView.showLoadError("message",errMsg)})}};OpenAssessment.PeerView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.PeerView.prototype={load:function(){var view=this;this.server.render("peer_assessment").done(function(html){$("#openassessment__peer-assessment",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__peer-assessment",view.element));view.installHandlers(false)}).fail(function(){view.baseView.showLoadError("peer-assessment")});view.baseView.loadMessageView()},loadContinuedAssessment:function(){var view=this;view.continueAssessmentEnabled(false);this.server.renderContinuedPeer().done(function(html){$("#openassessment__peer-assessment",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__peer-assessment",view.element));view.installHandlers(true)}).fail(function(){view.baseView.showLoadError("peer-assessment");view.continueAssessmentEnabled(true)})},continueAssessmentEnabled:function(enabled){var button=$("#peer-assessment__continue__grading",this.element);if(typeof enabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!enabled)}},installHandlers:function(isContinuedAssessment){var sel=$("#openassessment__peer-assessment",this.element);var view=this;this.baseView.setUpCollapseExpand(sel);var rubricSelector=$("#peer-assessment--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(view.peerSubmitEnabled,view))}sel.find("#peer-assessment--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();if(!isContinuedAssessment){view.peerAssess()}else{view.continuedPeerAssess()}});sel.find("#peer-assessment__continue__grading").click(function(eventObject){eventObject.preventDefault();view.loadContinuedAssessment()})},peerSubmitEnabled:function(enabled){var button=$("#peer-assessment--001__assessment__submit",this.element);if(typeof enabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!enabled)}},peerAssess:function(){var view=this;var baseView=view.baseView;this.peerAssessRequest(function(){baseView.loadAssessmentModules();baseView.scrollToTop()})},continuedPeerAssess:function(){var view=this;var gradeView=this.baseView.gradeView;var baseView=view.baseView;view.peerAssessRequest(function(){view.loadContinuedAssessment();gradeView.load();baseView.scrollToTop()})},peerAssessRequest:function(successFunction){var view=this;var uuid=$("#openassessment__peer-assessment").data("submission-uuid");view.baseView.toggleActionError("peer",null);view.peerSubmitEnabled(false);this.server.peerAssess(this.rubric.optionsSelected(),this.rubric.criterionFeedback(),this.rubric.overallFeedback(),uuid).done(successFunction).fail(function(errMsg){view.baseView.toggleActionError("peer",errMsg);view.peerSubmitEnabled(true)})}};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.fileType=null;this.lastChangeTime=Date.now();this.errorOnLastSave=false;this.autoSaveTimerId=null;this.data=data;this.fileUploaded=false};OpenAssessment.ResponseView.prototype={AUTO_SAVE_POLL_INTERVAL:2e3,AUTO_SAVE_WAIT:3e4,MAX_FILE_SIZE:5242880,load:function(){var view=this;this.server.render("submission").done(function(html){$("#openassessment__response",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__response",view.element));view.installHandlers();view.setAutoSaveEnabled(true)}).fail(function(){view.baseView.showLoadError("response")})},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")}this.baseView.setUpCollapseExpand(sel);this.savedResponse=this.response();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,uploadType)};sel.find("input[type=file]").on("change",handlePrepareUpload);sel.find("#submission__preview__item").hide();sel.find("#step--response__submit").click(function(eventObject){eventObject.preventDefault();view.submit()});sel.find("#submission__save").click(function(eventObject){eventObject.preventDefault();view.save()});sel.find("#submission__preview").click(function(eventObject){eventObject.preventDefault();var preview_text=sel.find(".submission__answer__part__text__value").val();var preview_container=sel.find("#preview_content");preview_container.html(preview_text.replace(/\r\n|\r|\n/g,"<br />"));sel.find("#submission__preview__item").show();MathJax.Hub.Queue(["Typeset",MathJax.Hub,preview_container[0]])});sel.find("#file__upload").click(function(eventObject){eventObject.preventDefault();$(".submission__answer__display__file",view.element).removeClass("is--hidden");view.fileUpload()})},setAutoSaveEnabled:function(enabled){if(enabled){if(this.autoSaveTimerId===null){this.autoSaveTimerId=setInterval($.proxy(this.autoSave,this),this.AUTO_SAVE_POLL_INTERVAL)}}else{if(this.autoSaveTimerId!==null){clearInterval(this.autoSaveTimerId)}}},submitEnabled:function(enabled){var sel=$("#step--response__submit",this.element);if(typeof enabled==="undefined"){return!sel.hasClass("is--disabled")}else{sel.toggleClass("is--disabled",!enabled)}},saveEnabled:function(enabled){var sel=$("#submission__save",this.element);if(typeof enabled==="undefined"){return!sel.hasClass("is--disabled")}else{sel.toggleClass("is--disabled",!enabled)}},previewEnabled:function(enabled){var sel=$("#submission__preview",this.element);if(typeof enabled==="undefined"){return!sel.hasClass("is--disabled")}else{sel.toggleClass("is--disabled",!enabled)}},saveStatus:function(msg){var sel=$("#response__save_status h3",this.element);if(typeof msg==="undefined"){return sel.text()}else{var label=gettext("Status of Your Response");sel.html('<span class="sr">'+label+":"+"</span>\n"+msg)}},unsavedWarningEnabled:function(enabled){if(typeof enabled==="undefined"){return window.onbeforeunload!==null}else{if(enabled){window.onbeforeunload=function(){return gettext("If you leave this page without saving or submitting your response, you'll lose any work you've done on the response.")}}else{window.onbeforeunload=null}}},response:function(texts){var sel=$(".submission__answer__part__text__value",this.element);if(typeof texts==="undefined"){return sel.map(function(){return $.trim($(this).val())}).get()}else{sel.map(function(index){$(this).val(texts[index])})}},responseChanged:function(){var savedResponse=this.savedResponse;return this.response().some(function(element,index){return element!==savedResponse[index]})},autoSave:function(){var timeSinceLastChange=Date.now()-this.lastChangeTime;if(this.responseChanged()&&timeSinceLastChange>this.AUTO_SAVE_WAIT&&!this.errorOnLastSave){this.save()}},handleResponseChanged:function(){var isNotBlank=!this.response().every(function(element){return $.trim(element)===""});this.submitEnabled(isNotBlank);if(this.responseChanged()){this.saveEnabled(isNotBlank);this.previewEnabled(isNotBlank);this.saveStatus(gettext("This response has not been saved."));this.unsavedWarningEnabled(true)}this.lastChangeTime=Date.now()},save:function(){this.errorOnLastSave=false;this.saveStatus(gettext("Saving..."));this.baseView.toggleActionError("save",null);this.unsavedWarningEnabled(false);var view=this;var savedResponse=this.response();this.server.save(savedResponse).done(function(){view.savedResponse=savedResponse;var currentResponse=view.response();var currentResponseIsEmpty=currentResponse.every(function(element){return element===""});view.submitEnabled(!currentResponseIsEmpty);var currentResponseEqualsSaved=currentResponse.every(function(element,index){return element===savedResponse[index]});if(currentResponseEqualsSaved){view.saveEnabled(false);view.saveStatus(gettext("This response has been saved but not submitted."))}}).fail(function(errMsg){view.saveStatus(gettext("Error"));view.baseView.toggleActionError("save",errMsg);view.errorOnLastSave=true})},submit:function(){this.submitEnabled(false);var view=this;var baseView=this.baseView;var fileDefer=$.Deferred();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()}fileDefer.pipe(function(){return view.confirmSubmission().pipe(function(){var submission=view.response();baseView.toggleActionError("response",null);return view.server.submit(submission)})}).done($.proxy(view.moveToNextStep,view)).fail(function(errCode,errMsg){if(errCode==="ENOMULTI"){view.moveToNextStep()}else{if(errMsg){baseView.toggleActionError("submit",errMsg)}view.submitEnabled(true)}})},moveToNextStep:function(){this.load();this.baseView.loadAssessmentModules();this.unsavedWarningEnabled(false)},confirmSubmission:function(){var msg=gettext("You're about to submit your response for this assignment. After you submit this response, you can't change it or submit a new response.");return $.Deferred(function(defer){if(confirm(msg)){defer.resolve()}else{defer.reject()}})},prepareUpload:function(files,uploadType){this.files=null;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."))}else if(uploadType==="image"&&this.data.ALLOWED_IMAGE_MIME_TYPES.indexOf(this.fileType)===-1){this.baseView.toggleActionError("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);this.files=files}$("#file__upload").toggleClass("is--disabled",this.files===null)},fileUpload:function(){var view=this;var fileUpload=$("#file__upload");fileUpload.addClass("is--disabled");var handleError=function(errMsg){view.baseView.toggleActionError("upload",errMsg);fileUpload.removeClass("is--disabled")};return this.server.getUploadUrl(view.fileType,view.files[0].name).done(function(url){var file=view.files[0];view.fileUploader.upload(url,file).done(function(){view.fileUrl();view.baseView.toggleActionError("upload",null);view.fileUploaded=true}).fail(handleError)}).fail(handleError)},fileUrl:function(){var view=this;var file=$("#submission__answer__file",view.element);view.server.getDownloadUrl().done(function(url){if(file.prop("tagName")==="IMG"){file.attr("src",url)}else{file.attr("href",url)}return url})}};OpenAssessment.Rubric=function(element){this.element=element};OpenAssessment.Rubric.prototype={criterionFeedback:function(criterionFeedback){var selector="textarea.answer__value";var feedback={};$(selector,this.element).each(function(index,sel){if(typeof criterionFeedback!=="undefined"){$(sel).val(criterionFeedback[sel.name]);feedback[sel.name]=criterionFeedback[sel.name]}else{feedback[sel.name]=$(sel).val()}});return feedback},overallFeedback:function(overallFeedback){var selector="#assessment__rubric__question--feedback__value";if(typeof overallFeedback==="undefined"){return $(selector,this.element).val()}else{$(selector,this.element).val(overallFeedback)}},optionsSelected:function(optionsSelected){var selector="input[type=radio]";if(typeof optionsSelected==="undefined"){var options={};$(selector+":checked",this.element).each(function(index,sel){options[sel.name]=sel.value});return options}else{$(selector,this.element).prop("checked",false);$(selector,this.element).each(function(index,sel){if(optionsSelected.hasOwnProperty(sel.name)){if(sel.value===optionsSelected[sel.name]){$(sel).prop("checked",true)}}})}},canSubmitCallback:function(callback){var rubric=this;callback(rubric.canSubmit());$(this.element).on("change keyup drop paste",function(){callback(rubric.canSubmit())})},canSubmit:function(){var numChecked=$("input[type=radio]:checked",this.element).length;var numAvailable=$(".field--radio.assessment__rubric__question.has--options",this.element).length;var completedRequiredComments=true;$("textarea[required]",this.element).each(function(){var trimmedText=$.trim($(this).val());if(trimmedText===""){completedRequiredComments=false}});return numChecked===numAvailable&&completedRequiredComments},showCorrections:function(corrections){var selector="input[type=radio]";var hasErrors=false;$(selector,this.element).each(function(index,sel){var listItem=$(sel).parents(".assessment__rubric__question");if(corrections.hasOwnProperty(sel.name)){hasErrors=true;listItem.find(".message--incorrect").removeClass("is--hidden");listItem.find(".message--correct").addClass("is--hidden")}else{listItem.find(".message--correct").removeClass("is--hidden");listItem.find(".message--incorrect").addClass("is--hidden")}});return hasErrors}};OpenAssessment.SelfView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.SelfView.prototype={load:function(){var view=this;this.server.render("self_assessment").done(function(html){$("#openassessment__self-assessment",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__self-assessment",view.element));view.installHandlers()}).fail(function(){view.showLoadError("self-assessment")})},installHandlers:function(){var view=this;var sel=$("#openassessment__self-assessment",view.element);this.baseView.setUpCollapseExpand(sel);var rubricSelector=$("#self-assessment--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(this.selfSubmitEnabled,this))}sel.find("#self-assessment--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();view.selfAssess()})},selfSubmitEnabled:function(enabled){var button=$("#self-assessment--001__assessment__submit",this.element);if(typeof enabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!enabled)
view.cancelSubmission($(this).data("submission-uuid"))});var handleChange=function(eventData){view.handleCommentChanged(eventData)};selCancelSub.find("#staff-info__cancel-submission__comments").on("change keyup drop paste",handleChange)}).fail(function(){view.showLoadError("student_info")})},installHandlers:function(){var $staffArea=$("#openassessment__staff-area",this.element);var toolsElement=$("#openassessment__staff-tools",$staffArea);var infoElement=$("#openassessment__student-info",$staffArea);var view=this;if(toolsElement.length<=0){return}this.baseView.setUpCollapseExpand(toolsElement,function(){});this.baseView.setUpCollapseExpand(infoElement,function(){});$staffArea.find(".ui-staff__button").click(function(eventObject){var $button=$(eventObject.currentTarget),panelID=$button.data("panel"),$panel=$staffArea.find("#"+panelID).first();if($button.hasClass("is--active")){$button.removeClass("is--active");$panel.addClass("is--hidden")}else{$staffArea.find(".ui-staff__button").removeClass("is--active");$button.addClass("is--active");$staffArea.find(".wrapper--ui-staff").addClass("is--hidden");$panel.removeClass("is--hidden")}});$staffArea.find(".ui-staff_close_button").click(function(eventObject){var $button=$(eventObject.currentTarget),$panel=$button.closest(".wrapper--ui-staff");$staffArea.find(".ui-staff__button").removeClass("is--active");$panel.addClass("is--hidden")});toolsElement.find("#openassessment_student_info_form").submit(function(eventObject){eventObject.preventDefault();view.loadStudentInfo()});toolsElement.find("#submit_student_username").click(function(eventObject){eventObject.preventDefault();view.loadStudentInfo()});toolsElement.find("#schedule_training").click(function(eventObject){eventObject.preventDefault();view.scheduleTraining()});toolsElement.find("#reschedule_unfinished_tasks").click(function(eventObject){eventObject.preventDefault();view.rescheduleUnfinishedTasks()})},scheduleTraining:function(){var view=this;this.server.scheduleTraining().done(function(msg){$("#schedule_training_message",view.element).text(msg)}).fail(function(errMsg){$("#schedule_training_message",view.element).text(errMsg)})},rescheduleUnfinishedTasks:function(){var view=this;this.server.rescheduleUnfinishedTasks().done(function(msg){$("#reschedule_unfinished_tasks_message",view.element).text(msg)}).fail(function(errMsg){$("#reschedule_unfinished_tasks_message",view.element).text(errMsg)})},cancelSubmission:function(submissionUUID){this.cancelSubmissionEnabled(false);var view=this;var sel=$("#openassessment__student-info",this.element);var comments=sel.find("#staff-info__cancel-submission__comments").val();this.server.cancelSubmission(submissionUUID,comments).done(function(msg){$(".cancel-submission-error").html("");$("#openassessment__staff-info__cancel__submission",view.element).html(msg)}).fail(function(errMsg){$(".cancel-submission-error").html(errMsg)})},cancelSubmissionEnabled:function(enabled){var sel=$("#submit_cancel_submission",this.element);if(typeof enabled==="undefined"){return!sel.hasClass("is--disabled")}else{sel.toggleClass("is--disabled",!enabled)}},comment:function(text){var sel=$("#staff-info__cancel-submission__comments",this.element);if(typeof text==="undefined"){return sel.val()}else{sel.val(text)}},handleCommentChanged:function(){var isBlank=$.trim(this.comment())!=="";this.cancelSubmissionEnabled(isBlank)}}}(OpenAssessment);OpenAssessment.StudentTrainingView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.StudentTrainingView.prototype={load:function(){var view=this;this.server.render("student_training").done(function(html){$("#openassessment__student-training",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__student-training",view.element));view.installHandlers()}).fail(function(){view.baseView.showLoadError("student-training")})},installHandlers:function(){var sel=$("#openassessment__student-training",this.element);var view=this;this.baseView.setUpCollapseExpand(sel);var rubricSelector=$("#student-training--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(this.assessButtonEnabled,this))}sel.find("#student-training--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();view.assess()})},assess:function(){this.assessButtonEnabled(false);var options={};if(this.rubric!==null){options=this.rubric.optionsSelected()}var view=this;var baseView=this.baseView;this.server.trainingAssess(options).done(function(corrections){var incorrect=$("#openassessment__student-training--incorrect",view.element);var instructions=$("#openassessment__student-training--instructions",view.element);if(!view.rubric.showCorrections(corrections)){view.load();baseView.loadAssessmentModules();incorrect.addClass("is--hidden");instructions.removeClass("is--hidden")}else{instructions.addClass("is--hidden");incorrect.removeClass("is--hidden")}baseView.scrollToTop()}).fail(function(errMsg){baseView.toggleActionError("student-training",errMsg);view.assessButtonEnabled(true)})},assessButtonEnabled:function(isEnabled){var button=$("#student-training--001__assessment__submit",this.element);if(typeof isEnabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!isEnabled)}}}; }},selfAssess:function(){var view=this;var baseView=this.baseView;baseView.toggleActionError("self",null);view.selfSubmitEnabled(false);this.server.selfAssess(this.rubric.optionsSelected(),this.rubric.criterionFeedback(),this.rubric.overallFeedback()).done(function(){baseView.loadAssessmentModules();baseView.scrollToTop()}).fail(function(errMsg){baseView.toggleActionError("self",errMsg);view.selfSubmitEnabled(true)})}};!function(OpenAssessment){"use strict";OpenAssessment.StaffAreaView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.StaffAreaView.prototype={load:function(){var view=this;if($("#openassessment__staff-area",view.element).length>0){this.server.render("staff_area").done(function(html){$("#openassessment__staff-area",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__staff-area",view.element));view.installHandlers()}).fail(function(){view.baseView.showLoadError("staff_area")})}},loadStudentInfo:function(){var view=this;var sel=$("#openassessment__staff-tools",this.element);var student_username=sel.find("#openassessment__student_username").val();this.server.studentInfo(student_username).done(function(html){$("#openassessment__student-info",view.element).replaceWith(html);var selCancelSub=$("#openassessment__staff-info__cancel__submission",view.element);selCancelSub.on("click","#submit_cancel_submission",function(eventObject){eventObject.preventDefault();view.cancelSubmission($(this).data("submission-uuid"))});var handleChange=function(eventData){view.handleCommentChanged(eventData)};selCancelSub.find("#staff-info__cancel-submission__comments").on("change keyup drop paste",handleChange)}).fail(function(){view.showLoadError("student_info")})},installHandlers:function(){var $staffArea=$("#openassessment__staff-area",this.element);var toolsElement=$("#openassessment__staff-tools",$staffArea);var infoElement=$("#openassessment__student-info",$staffArea);var view=this;if(toolsElement.length<=0){return}this.baseView.setUpCollapseExpand(toolsElement,function(){});this.baseView.setUpCollapseExpand(infoElement,function(){});$staffArea.find(".ui-staff__button").click(function(eventObject){var $button=$(eventObject.currentTarget),panelID=$button.data("panel"),$panel=$staffArea.find("#"+panelID).first();if($button.hasClass("is--active")){$button.removeClass("is--active");$panel.addClass("is--hidden")}else{$staffArea.find(".ui-staff__button").removeClass("is--active");$button.addClass("is--active");$staffArea.find(".wrapper--ui-staff").addClass("is--hidden");$panel.removeClass("is--hidden")}});$staffArea.find(".ui-staff_close_button").click(function(eventObject){var $button=$(eventObject.currentTarget),$panel=$button.closest(".wrapper--ui-staff");$staffArea.find(".ui-staff__button").removeClass("is--active");$panel.addClass("is--hidden")});toolsElement.find("#openassessment_student_info_form").submit(function(eventObject){eventObject.preventDefault();view.loadStudentInfo()});toolsElement.find("#submit_student_username").click(function(eventObject){eventObject.preventDefault();view.loadStudentInfo()});toolsElement.find("#schedule_training").click(function(eventObject){eventObject.preventDefault();view.scheduleTraining()});toolsElement.find("#reschedule_unfinished_tasks").click(function(eventObject){eventObject.preventDefault();view.rescheduleUnfinishedTasks()})},scheduleTraining:function(){var view=this;this.server.scheduleTraining().done(function(msg){$("#schedule_training_message",view.element).text(msg)}).fail(function(errMsg){$("#schedule_training_message",view.element).text(errMsg)})},rescheduleUnfinishedTasks:function(){var view=this;this.server.rescheduleUnfinishedTasks().done(function(msg){$("#reschedule_unfinished_tasks_message",view.element).text(msg)}).fail(function(errMsg){$("#reschedule_unfinished_tasks_message",view.element).text(errMsg)})},cancelSubmission:function(submissionUUID){this.cancelSubmissionEnabled(false);var view=this;var sel=$("#openassessment__student-info",this.element);var comments=sel.find("#staff-info__cancel-submission__comments").val();this.server.cancelSubmission(submissionUUID,comments).done(function(msg){$(".cancel-submission-error").html("");$("#openassessment__staff-info__cancel__submission",view.element).html(msg)}).fail(function(errMsg){$(".cancel-submission-error").html(errMsg)})},cancelSubmissionEnabled:function(enabled){var sel=$("#submit_cancel_submission",this.element);if(typeof enabled==="undefined"){return!sel.hasClass("is--disabled")}else{sel.toggleClass("is--disabled",!enabled)}},comment:function(text){var sel=$("#staff-info__cancel-submission__comments",this.element);if(typeof text==="undefined"){return sel.val()}else{sel.val(text)}},handleCommentChanged:function(){var isBlank=$.trim(this.comment())!=="";this.cancelSubmissionEnabled(isBlank)}}}(OpenAssessment);OpenAssessment.StudentTrainingView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.StudentTrainingView.prototype={load:function(){var view=this;this.server.render("student_training").done(function(html){$("#openassessment__student-training",view.element).replaceWith(html);view.server.renderLatex($("#openassessment__student-training",view.element));view.installHandlers()}).fail(function(){view.baseView.showLoadError("student-training")})},installHandlers:function(){var sel=$("#openassessment__student-training",this.element);var view=this;this.baseView.setUpCollapseExpand(sel);var rubricSelector=$("#student-training--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(this.assessButtonEnabled,this))}sel.find("#student-training--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();view.assess()})},assess:function(){this.assessButtonEnabled(false);var options={};if(this.rubric!==null){options=this.rubric.optionsSelected()}var view=this;var baseView=this.baseView;this.server.trainingAssess(options).done(function(corrections){var incorrect=$("#openassessment__student-training--incorrect",view.element);var instructions=$("#openassessment__student-training--instructions",view.element);if(!view.rubric.showCorrections(corrections)){view.load();baseView.loadAssessmentModules();incorrect.addClass("is--hidden");instructions.removeClass("is--hidden")}else{instructions.addClass("is--hidden");incorrect.removeClass("is--hidden")}baseView.scrollToTop()}).fail(function(errMsg){baseView.toggleActionError("student-training",errMsg);view.assessButtonEnabled(true)})},assessButtonEnabled:function(isEnabled){var button=$("#student-training--001__assessment__submit",this.element);if(typeof isEnabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!isEnabled)}}};
\ No newline at end of file \ No newline at end of file
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. ...@@ -5,6 +5,23 @@ Tests for OpenAssessment response (submission) view.
describe("OpenAssessment.ResponseView", function() { describe("OpenAssessment.ResponseView", function() {
var FAKE_URL = "http://www.example.com"; 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() { var StubServer = function() {
...@@ -75,6 +92,7 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -75,6 +92,7 @@ describe("OpenAssessment.ResponseView", function() {
var baseView = null; var baseView = null;
var server = null; var server = null;
var fileUploader = null; var fileUploader = null;
var data = null;
// View under test // View under test
var view = null; var view = null;
...@@ -103,10 +121,16 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -103,10 +121,16 @@ describe("OpenAssessment.ResponseView", function() {
server.renderLatex = jasmine.createSpy('renderLatex'); server.renderLatex = jasmine.createSpy('renderLatex');
fileUploader = new StubFileUploader(); fileUploader = new StubFileUploader();
baseView = new StubBaseView(); 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 // Create and install the view
var el = $('#openassessment-base').get(0); 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(); view.installHandlers();
// Stub the confirmation step // Stub the confirmation step
...@@ -419,21 +443,59 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -419,21 +443,59 @@ describe("OpenAssessment.ResponseView", function() {
it("selects too large of a file", function() { it("selects too large of a file", function() {
spyOn(baseView, 'toggleActionError').and.callThrough(); spyOn(baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'image/jpg', size: 6000000, name: 'huge-picture.jpg', data: ''}]; var files = [{type: 'image/jpeg', size: 6000000, name: 'huge-picture.jpg', data: ''}];
view.prepareUpload(files); view.prepareUpload(files, 'image');
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File size must be 5MB or less.'); 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(); spyOn(baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'bogus/jpg', size: 1024, name: 'picture.exe', data: ''}]; view.data.FILE_TYPE_WHITE_LIST = ['exe'];
view.prepareUpload(files); var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}];
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File must be an image.'); 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() { it("uploads a arbitrary type file using a one-time URL", function() {
var files = [{type: 'image/jpg', size: 1024, name: 'picture.jpg', data: ''}]; var files = [{type: 'text/html', size: 1024, name: 'index.html', data: ''}];
view.prepareUpload(files); view.prepareUpload(files, 'custom');
view.fileUpload(); view.fileUpload();
expect(fileUploader.uploadArgs.url).toEqual(FAKE_URL); expect(fileUploader.uploadArgs.url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs.data).toEqual(files[0]); expect(fileUploader.uploadArgs.data).toEqual(files[0]);
...@@ -445,8 +507,8 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -445,8 +507,8 @@ describe("OpenAssessment.ResponseView", function() {
spyOn(baseView, 'toggleActionError').and.callThrough(); spyOn(baseView, 'toggleActionError').and.callThrough();
// Attempt to upload a file // Attempt to upload a file
var files = [{type: 'image/jpg', size: 1024, name: 'picture.jpg', data: ''}]; var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files); view.prepareUpload(files, 'image');
view.fileUpload(); view.fileUpload();
// Expect an error to be displayed // Expect an error to be displayed
...@@ -459,8 +521,8 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -459,8 +521,8 @@ describe("OpenAssessment.ResponseView", function() {
spyOn(baseView, 'toggleActionError').and.callThrough(); spyOn(baseView, 'toggleActionError').and.callThrough();
// Attempt to upload a file // Attempt to upload a file
var files = [{type: 'image/jpg', size: 1024, name: 'picture.jpg', data: ''}]; var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files); view.prepareUpload(files, 'image');
view.fileUpload(); view.fileUpload();
// Expect an error to be displayed // Expect an error to be displayed
......
...@@ -271,7 +271,9 @@ describe("OpenAssessment.Server", function() { ...@@ -271,7 +271,9 @@ describe("OpenAssessment.Server", function() {
criteria: CRITERIA, criteria: CRITERIA,
assessments: ASSESSMENTS, assessments: ASSESSMENTS,
editorAssessmentsOrder: EDITOR_ASSESSMENTS_ORDER, editorAssessmentsOrder: EDITOR_ASSESSMENTS_ORDER,
imageSubmissionEnabled: true, fileUploadType: "image",
fileTypeWhiteList: ['pdf', 'doc'],
latexEnabled: true,
leaderboardNum: 15 leaderboardNum: 15
}); });
expect($.ajax).toHaveBeenCalledWith({ expect($.ajax).toHaveBeenCalledWith({
...@@ -286,7 +288,9 @@ describe("OpenAssessment.Server", function() { ...@@ -286,7 +288,9 @@ describe("OpenAssessment.Server", function() {
criteria: CRITERIA, criteria: CRITERIA,
assessments: ASSESSMENTS, assessments: ASSESSMENTS,
editor_assessments_order: EDITOR_ASSESSMENTS_ORDER, 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 leaderboard_show: 15
}), }),
contentType : jsonContentType contentType : jsonContentType
......
...@@ -40,6 +40,7 @@ describe("OpenAssessment.StudioView", function() { ...@@ -40,6 +40,7 @@ describe("OpenAssessment.StudioView", function() {
var server = null; var server = null;
var view = null; var view = null;
var data = null;
var EXPECTED_SERVER_DATA = { var EXPECTED_SERVER_DATA = {
title: "The most important of all questions.", title: "The most important of all questions.",
...@@ -47,7 +48,7 @@ describe("OpenAssessment.StudioView", function() { ...@@ -47,7 +48,7 @@ describe("OpenAssessment.StudioView", function() {
feedbackPrompt: "", feedbackPrompt: "",
submissionStart: "2014-01-02T12:15", submissionStart: "2014-01-02T12:15",
submissionDue: "2014-10-01T04:53", submissionDue: "2014-10-01T04:53",
imageSubmissionEnabled: false, fileUploadType: "",
leaderboardNum: 12, leaderboardNum: 12,
criteria: [ criteria: [
{ {
...@@ -126,13 +127,17 @@ describe("OpenAssessment.StudioView", function() { ...@@ -126,13 +127,17 @@ describe("OpenAssessment.StudioView", function() {
// Create the stub server // Create the stub server
server = new StubServer(); server = new StubServer();
// mock data sent from backend
data = {
FILE_EXT_BLACK_LIST: ['exe','app']
};
// Mock the runtime // Mock the runtime
spyOn(runtime, 'notify'); spyOn(runtime, 'notify');
// Create the object under test // Create the object under test
var el = $('#openassessment-editor').get(0); 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() { it("sends the editor context to the server", function() {
...@@ -149,7 +154,7 @@ describe("OpenAssessment.StudioView", function() { ...@@ -149,7 +154,7 @@ describe("OpenAssessment.StudioView", function() {
expect(server.receivedData.feedbackPrompt).toEqual(EXPECTED_SERVER_DATA.feedbackPrompt); expect(server.receivedData.feedbackPrompt).toEqual(EXPECTED_SERVER_DATA.feedbackPrompt);
expect(server.receivedData.submissionStart).toEqual(EXPECTED_SERVER_DATA.submissionStart); expect(server.receivedData.submissionStart).toEqual(EXPECTED_SERVER_DATA.submissionStart);
expect(server.receivedData.submissionDue).toEqual(EXPECTED_SERVER_DATA.submissionDue); 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); expect(server.receivedData.leaderboardNum).toEqual(EXPECTED_SERVER_DATA.leaderboardNum);
// Criteria // Criteria
......
var StubNotifier = function() {
this.receivedNotifications = [];
this.notificationFired = function(name, data) {
this.receivedNotifications.push({
name: name,
data: data
});
};
};
describe("OpenAssessment.DatetimeControl", function() { describe("OpenAssessment.DatetimeControl", function() {
var datetimeControl = null; var datetimeControl = null;
...@@ -86,16 +96,6 @@ describe("OpenAssessment.DatetimeControl", function() { ...@@ -86,16 +96,6 @@ describe("OpenAssessment.DatetimeControl", function() {
describe("OpenAssessment.ToggleControl", 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 notifier = null;
var toggleControl = null; var toggleControl = null;
...@@ -159,3 +159,137 @@ describe("OpenAssessment.ToggleControl", function() { ...@@ -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() { ...@@ -44,6 +44,7 @@ describe("OpenAssessment.EditSettingsView", function() {
var view = null; var view = null;
var assessmentViews = null; var assessmentViews = null;
var data = null;
// The Peer and Self Editor ID's // The Peer and Self Editor ID's
var PEER = "oa_peer_assessment_editor"; var PEER = "oa_peer_assessment_editor";
...@@ -62,9 +63,14 @@ describe("OpenAssessment.EditSettingsView", function() { ...@@ -62,9 +63,14 @@ describe("OpenAssessment.EditSettingsView", function() {
assessmentViews[AI] = new StubView("ai-assessment", "Example Based assessment description"); assessmentViews[AI] = new StubView("ai-assessment", "Example Based assessment description");
assessmentViews[TRAINING] = new StubView("student-training", "Student Training description"); assessmentViews[TRAINING] = new StubView("student-training", "Student Training description");
// mock data from backend
data = {
FILE_EXT_BLACK_LIST: ['exe','app']
};
// Create the view // Create the view
var element = $("#oa_basic_settings_editor").get(0); 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.submissionStart("2014-01-01", "00:00");
view.submissionDue("2014-03-04", "00:00"); view.submissionDue("2014-03-04", "00:00");
}); });
...@@ -84,11 +90,23 @@ describe("OpenAssessment.EditSettingsView", function() { ...@@ -84,11 +90,23 @@ describe("OpenAssessment.EditSettingsView", function() {
expect(view.submissionDue()).toEqual("2014-05-02T12:34"); expect(view.submissionDue()).toEqual("2014-05-02T12:34");
}); });
it("sets and loads the image enabled state", function() { it("sets and loads the file upload state", function() {
view.imageSubmissionEnabled(true); view.fileUploadType('image');
expect(view.imageSubmissionEnabled()).toBe(true); expect(view.fileUploadType()).toBe('image');
view.imageSubmissionEnabled(false); view.fileUploadType('pdf-and-image');
expect(view.imageSubmissionEnabled()).toBe(false); 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() { it("sets and loads the leaderboard number", function() {
...@@ -219,4 +237,21 @@ describe("OpenAssessment.EditSettingsView", function() { ...@@ -219,4 +237,21 @@ describe("OpenAssessment.EditSettingsView", function() {
// to mark anything as invalid // to mark anything as invalid
expect(assessmentViews[PEER].validate).not.toHaveBeenCalled(); 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: ...@@ -5,17 +5,18 @@ Args:
runtime (Runtime): an XBlock runtime instance. runtime (Runtime): an XBlock runtime instance.
element (DOM element): The DOM element representing this XBlock. element (DOM element): The DOM element representing this XBlock.
server (OpenAssessment.Server): The interface to the XBlock server. server (OpenAssessment.Server): The interface to the XBlock server.
data (Object): The data object passed from XBlock backend.
Returns: Returns:
OpenAssessment.BaseView OpenAssessment.BaseView
**/ **/
OpenAssessment.BaseView = function(runtime, element, server) { OpenAssessment.BaseView = function(runtime, element, server, data) {
this.runtime = runtime; this.runtime = runtime;
this.element = element; this.element = element;
this.server = server; this.server = server;
this.fileUploader = new OpenAssessment.FileUploader(); 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.trainingView = new OpenAssessment.StudentTrainingView(this.element, this.server, this);
this.selfView = new OpenAssessment.SelfView(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); this.peerView = new OpenAssessment.PeerView(this.element, this.server, this);
...@@ -151,11 +152,11 @@ OpenAssessment.BaseView.prototype = { ...@@ -151,11 +152,11 @@ OpenAssessment.BaseView.prototype = {
/* XBlock JavaScript entry point for OpenAssessmentXBlock. */ /* XBlock JavaScript entry point for OpenAssessmentXBlock. */
/* jshint unused:false */ /* jshint unused:false */
function OpenAssessmentBlock(runtime, element) { function OpenAssessmentBlock(runtime, element, data) {
/** /**
Render views within the base view on page load. Render views within the base view on page load.
**/ **/
var server = new OpenAssessment.Server(runtime, element); 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(); view.load();
} }
...@@ -4,22 +4,26 @@ Interface for response (submission) view. ...@@ -4,22 +4,26 @@ Interface for response (submission) view.
Args: Args:
element (DOM element): The DOM element representing the XBlock. element (DOM element): The DOM element representing the XBlock.
server (OpenAssessment.Server): The interface to the XBlock server. server (OpenAssessment.Server): The interface to the XBlock server.
fileUploader (OpenAssessment.FileUploader): File uploader instance.
baseView (OpenAssessment.BaseView): Container view. baseView (OpenAssessment.BaseView): Container view.
data (Object): The data object passed from XBlock backend.
Returns: Returns:
OpenAssessment.ResponseView OpenAssessment.ResponseView
**/ **/
OpenAssessment.ResponseView = function(element, server, fileUploader, baseView) { OpenAssessment.ResponseView = function(element, server, fileUploader, baseView, data) {
this.element = element; this.element = element;
this.server = server; this.server = server;
this.fileUploader = fileUploader; this.fileUploader = fileUploader;
this.baseView = baseView; this.baseView = baseView;
this.savedResponse = []; this.savedResponse = [];
this.files = null; this.files = null;
this.imageType = null; this.fileType = null;
this.lastChangeTime = Date.now(); this.lastChangeTime = Date.now();
this.errorOnLastSave = false; this.errorOnLastSave = false;
this.autoSaveTimerId = null; this.autoSaveTimerId = null;
this.data = data;
this.fileUploaded = false;
}; };
...@@ -59,6 +63,10 @@ OpenAssessment.ResponseView.prototype = { ...@@ -59,6 +63,10 @@ OpenAssessment.ResponseView.prototype = {
installHandlers: function() { installHandlers: function() {
var sel = $('#openassessment__response', this.element); var sel = $('#openassessment__response', this.element);
var view = this; 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 // Install a click handler for collapse/expand
this.baseView.setUpCollapseExpand(sel); this.baseView.setUpCollapseExpand(sel);
...@@ -68,9 +76,9 @@ OpenAssessment.ResponseView.prototype = { ...@@ -68,9 +76,9 @@ OpenAssessment.ResponseView.prototype = {
var handleChange = function() { view.handleResponseChanged(); }; var handleChange = function() { view.handleResponseChanged(); };
sel.find('.submission__answer__part__text__value').on('change keyup drop paste', handleChange); 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); 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(); sel.find('#submission__preview__item').hide();
// Install a click handler for submission // Install a click handler for submission
...@@ -111,7 +119,7 @@ OpenAssessment.ResponseView.prototype = { ...@@ -111,7 +119,7 @@ OpenAssessment.ResponseView.prototype = {
function(eventObject) { function(eventObject) {
// Override default form submission // Override default form submission
eventObject.preventDefault(); eventObject.preventDefault();
$('.submission__answer__display__image', view.element).removeClass('is--hidden'); $('.submission__answer__display__file', view.element).removeClass('is--hidden');
view.fileUpload(); view.fileUpload();
} }
); );
...@@ -396,19 +404,36 @@ OpenAssessment.ResponseView.prototype = { ...@@ -396,19 +404,36 @@ OpenAssessment.ResponseView.prototype = {
var view = this; var view = this;
var baseView = this.baseView; 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() fileDefer
// 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() { .pipe(function() {
var submission = view.response(); return view.confirmSubmission()
baseView.toggleActionError('response', null); // On confirmation, send the submission to the server
// The callback returns a promise so we can attach
// Send the submission to the server, returning the promise. // additional callbacks after the confirmation.
return view.server.submit(submission); // 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 // If the submission was submitted successfully, move to the next step
...@@ -463,25 +488,46 @@ OpenAssessment.ResponseView.prototype = { ...@@ -463,25 +488,46 @@ OpenAssessment.ResponseView.prototype = {
/** /**
When selecting a file for upload, do some quick client-side validation 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 to ensure that it is an image, a PDF or other allowed types, and is not
size. larger than the maximum file size.
Args: Args:
files (list): A collection of files used for upload. This function assumes 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 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.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) { if (files[0].size > this.MAX_FILE_SIZE) {
this.baseView.toggleActionError( 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( 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 { } else {
this.baseView.toggleActionError('upload', null); this.baseView.toggleActionError('upload', null);
...@@ -511,13 +557,14 @@ OpenAssessment.ResponseView.prototype = { ...@@ -511,13 +557,14 @@ OpenAssessment.ResponseView.prototype = {
// completed, execute a sequential AJAX call to upload to the returned // completed, execute a sequential AJAX call to upload to the returned
// URL. This request requires appropriate CORS configuration for AJAX // URL. This request requires appropriate CORS configuration for AJAX
// PUT requests on the server. // PUT requests on the server.
this.server.getUploadUrl(view.imageType).done( return this.server.getUploadUrl(view.fileType, view.files[0].name).done(
function(url) { function(url) {
var image = view.files[0]; var file = view.files[0];
view.fileUploader.upload(url, image) view.fileUploader.upload(url, file)
.done(function() { .done(function() {
view.imageUrl(); view.fileUrl();
view.baseView.toggleActionError('upload', null); view.baseView.toggleActionError('upload', null);
view.fileUploaded = true;
}) })
.fail(handleError); .fail(handleError);
} }
...@@ -525,13 +572,17 @@ OpenAssessment.ResponseView.prototype = { ...@@ -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 view = this;
var image = $('#submission__answer__image', view.element); var file = $('#submission__answer__file', view.element);
view.server.getDownloadUrl().done(function(url) { 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; return url;
}); });
} }
......
...@@ -436,7 +436,9 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) { ...@@ -436,7 +436,9 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
submissionDue (ISO-formatted datetime string or null): The date the submission is due. submissionDue (ISO-formatted datetime string or null): The date the submission is due.
criteria (list of object literals): The rubric criteria. criteria (list of object literals): The rubric criteria.
assessments (list of object literals): The assessments the student will be evaluated on. 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. latexEnabled: TRUE if latex rendering is enabled.
leaderboardNum (int): The number of scores to show in the leaderboard. leaderboardNum (int): The number of scores to show in the leaderboard.
...@@ -457,7 +459,8 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) { ...@@ -457,7 +459,8 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
criteria: kwargs.criteria, criteria: kwargs.criteria,
assessments: kwargs.assessments, assessments: kwargs.assessments,
editor_assessments_order: kwargs.editorAssessmentsOrder, 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, allow_latex: kwargs.latexEnabled,
leaderboard_show: kwargs.leaderboardNum leaderboard_show: kwargs.leaderboardNum
}); });
...@@ -509,19 +512,20 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) { ...@@ -509,19 +512,20 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
Args: Args:
contentType (str): The Content Type for the file being uploaded. contentType (str): The Content Type for the file being uploaded.
filename (str): The name of the file to be uploaded.
Returns: Returns:
A presigned upload URL from the specified service used for uploading A presigned upload URL from the specified service used for uploading
files. files.
**/ **/
getUploadUrl: function(contentType) { getUploadUrl: function(contentType, filename) {
var url = this.url('upload_url'); var url = this.url('upload_url');
return $.Deferred(function(defer) { return $.Deferred(function(defer) {
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: url, url: url,
data: JSON.stringify({contentType: contentType}), data: JSON.stringify({contentType: contentType, filename: filename}),
contentType: jsonContentType contentType: jsonContentType
}).done(function(data) { }).done(function(data) {
if (data.success) { defer.resolve(data.url); } if (data.success) { defer.resolve(data.url); }
......
...@@ -6,15 +6,17 @@ ...@@ -6,15 +6,17 @@
runtime (Runtime): an XBlock runtime instance. runtime (Runtime): an XBlock runtime instance.
element (DOM element): The DOM element representing this XBlock. element (DOM element): The DOM element representing this XBlock.
server (OpenAssessment.Server): The interface to the XBlock server. server (OpenAssessment.Server): The interface to the XBlock server.
data (Object literal): The data object passed from XBlock backend.
Returns: Returns:
OpenAssessment.StudioView OpenAssessment.StudioView
**/ **/
OpenAssessment.StudioView = function(runtime, element, server) { OpenAssessment.StudioView = function(runtime, element, server, data) {
this.element = element; this.element = element;
this.runtime = runtime; this.runtime = runtime;
this.server = server; this.server = server;
this.data = data;
// Resize the editing modal // Resize the editing modal
this.fixModalHeight(); this.fixModalHeight();
...@@ -55,7 +57,7 @@ OpenAssessment.StudioView = function(runtime, element, server) { ...@@ -55,7 +57,7 @@ OpenAssessment.StudioView = function(runtime, element, server) {
assessmentLookupDictionary[exampleBasedAssessmentView.getID()] = exampleBasedAssessmentView; assessmentLookupDictionary[exampleBasedAssessmentView.getID()] = exampleBasedAssessmentView;
this.settingsView = new OpenAssessment.EditSettingsView( 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 // Initialize the rubric tab view
...@@ -198,7 +200,8 @@ OpenAssessment.StudioView.prototype = { ...@@ -198,7 +200,8 @@ OpenAssessment.StudioView.prototype = {
submissionStart: view.settingsView.submissionStart(), submissionStart: view.settingsView.submissionStart(),
submissionDue: view.settingsView.submissionDue(), submissionDue: view.settingsView.submissionDue(),
assessments: view.settingsView.assessmentsDescription(), assessments: view.settingsView.assessmentsDescription(),
imageSubmissionEnabled: view.settingsView.imageSubmissionEnabled(), fileUploadType: view.settingsView.fileUploadType(),
fileTypeWhiteList: view.settingsView.fileTypeWhiteList(),
latexEnabled: view.settingsView.latexEnabled(), latexEnabled: view.settingsView.latexEnabled(),
leaderboardNum: view.settingsView.leaderboardNum(), leaderboardNum: view.settingsView.leaderboardNum(),
editorAssessmentsOrder: view.settingsView.editorAssessmentsOrder() editorAssessmentsOrder: view.settingsView.editorAssessmentsOrder()
...@@ -274,11 +277,11 @@ OpenAssessment.StudioView.prototype = { ...@@ -274,11 +277,11 @@ OpenAssessment.StudioView.prototype = {
/* XBlock entry point for Studio view */ /* XBlock entry point for Studio view */
/* jshint unused:false */ /* jshint unused:false */
function OpenAssessmentEditor(runtime, element) { function OpenAssessmentEditor(runtime, element, data) {
/** /**
Initialize the editing interface on page load. Initialize the editing interface on page load.
**/ **/
var server = new OpenAssessment.Server(runtime, element); 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 = { ...@@ -280,4 +280,137 @@ OpenAssessment.DatetimeControl.prototype = {
return errors; 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). ...@@ -4,12 +4,13 @@ Editing interface for OpenAssessment settings (including assessments).
Args: Args:
element (DOM element): The DOM element representing this view. element (DOM element): The DOM element representing this view.
assessmentViews (object literal): Mapping of CSS IDs to view objects. assessmentViews (object literal): Mapping of CSS IDs to view objects.
data (Object literal): The data object passed from XBlock backend.
Returns: Returns:
OpenAssessment.EditSettingsView OpenAssessment.EditSettingsView
**/ **/
OpenAssessment.EditSettingsView = function(element, assessmentViews) { OpenAssessment.EditSettingsView = function(element, assessmentViews, data) {
this.settingsElement = element; this.settingsElement = element;
this.assessmentsElement = $(element).siblings('#openassessment_assessment_module_settings_editors').get(0); this.assessmentsElement = $(element).siblings('#openassessment_assessment_module_settings_editors').get(0);
this.assessmentViews = assessmentViews; this.assessmentViews = assessmentViews;
...@@ -27,11 +28,42 @@ OpenAssessment.EditSettingsView = function(element, assessmentViews) { ...@@ -27,11 +28,42 @@ OpenAssessment.EditSettingsView = function(element, assessmentViews) {
"#openassessment_submission_due_time" "#openassessment_submission_due_time"
).install(); ).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( this.leaderboardIntField = new OpenAssessment.IntField(
$("#openassessment_leaderboard_editor", this.element), $("#openassessment_leaderboard_editor", this.element),
{ min: 0, max: 100 } { 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(); this.initializeSortableAssessments();
}; };
...@@ -122,28 +154,43 @@ OpenAssessment.EditSettingsView.prototype = { ...@@ -122,28 +154,43 @@ OpenAssessment.EditSettingsView.prototype = {
}, },
/** /**
Enable / disable image submission. Get or set upload file type.
Args: Args:
isEnabled (boolean, optional): If provided, enable/disable image submission. uploadType (string, optional): If provided, enable specified upload type submission.
Returns: Returns:
boolean string (image, file or custom)
**/ **/
imageSubmissionEnabled: function(isEnabled) { fileUploadType: function(uploadType) {
var sel = $("#openassessment_submission_image_editor", this.settingsElement); var sel = $("#openassessment_submission_upload_selector", this.settingsElement);
if (isEnabled !== undefined) { if (uploadType !== undefined) {
if (isEnabled) { sel.val("1"); } sel.val(uploadType);
else { sel.val("0"); }
} }
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. Enable / disable latex rendering.
Args: Args:
isEnabled(boolean, optional): if provided enable/disable latex rendering isEnabled(boolean, optional): if provided enable/disable latex rendering
Returns: Returns:
boolean boolean
...@@ -255,6 +302,15 @@ OpenAssessment.EditSettingsView.prototype = { ...@@ -255,6 +302,15 @@ OpenAssessment.EditSettingsView.prototype = {
isValid = (this.startDatetimeControl.validate() && isValid); isValid = (this.startDatetimeControl.validate() && isValid);
isValid = (this.dueDatetimeControl.validate() && isValid); isValid = (this.dueDatetimeControl.validate() && isValid);
isValid = (this.leaderboardIntField.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 // Validate each of the *enabled* assessment views
$.each(this.assessmentViews, function() { $.each(this.assessmentViews, function() {
...@@ -286,6 +342,9 @@ OpenAssessment.EditSettingsView.prototype = { ...@@ -286,6 +342,9 @@ OpenAssessment.EditSettingsView.prototype = {
if (this.leaderboardIntField.validationErrors().length > 0) { if (this.leaderboardIntField.validationErrors().length > 0) {
errors.push("Leaderboard number is invalid"); errors.push("Leaderboard number is invalid");
} }
if (this.fileTypeWhiteListInputField.validationErrors().length > 0) {
errors = errors.concat(this.fileTypeWhiteListInputField.validationErrors());
}
$.each(this.assessmentViews, function() { $.each(this.assessmentViews, function() {
errors = errors.concat(this.validationErrors()); errors = errors.concat(this.validationErrors());
...@@ -301,8 +360,9 @@ OpenAssessment.EditSettingsView.prototype = { ...@@ -301,8 +360,9 @@ OpenAssessment.EditSettingsView.prototype = {
this.startDatetimeControl.clearValidationErrors(); this.startDatetimeControl.clearValidationErrors();
this.dueDatetimeControl.clearValidationErrors(); this.dueDatetimeControl.clearValidationErrors();
this.leaderboardIntField.clearValidationErrors(); this.leaderboardIntField.clearValidationErrors();
this.fileTypeWhiteListInputField.clearValidationErrors();
$.each(this.assessmentViews, function() { $.each(this.assessmentViews, function() {
this.clearValidationErrors(); 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> <title>
My favourite pet My favourite pet
</title> </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> <title>
Global Poverty Global Poverty
</title> </title>
......
...@@ -73,7 +73,10 @@ class StudioMixin(object): ...@@ -73,7 +73,10 @@ class StudioMixin(object):
else: else:
# TODO: switch to add_javascript_url once XBlock resources are loaded from the CDN # 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.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 return fragment
def editor_context(self): def editor_context(self):
...@@ -115,7 +118,7 @@ class StudioMixin(object): ...@@ -115,7 +118,7 @@ class StudioMixin(object):
if not criteria: if not criteria:
criteria = self.DEFAULT_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 configured for the xblock, use the default text
feedback_default_text = copy.deepcopy(self.rubric_feedback_default_text) feedback_default_text = copy.deepcopy(self.rubric_feedback_default_text)
if not feedback_default_text: if not feedback_default_text:
...@@ -130,7 +133,8 @@ class StudioMixin(object): ...@@ -130,7 +133,8 @@ class StudioMixin(object):
'criteria': criteria, 'criteria': criteria,
'feedbackprompt': self.rubric_feedback_prompt, 'feedbackprompt': self.rubric_feedback_prompt,
'feedback_default_text': feedback_default_text, '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, 'allow_latex': self.allow_latex,
'leaderboard_show': self.leaderboard_show, 'leaderboard_show': self.leaderboard_show,
'editor_assessments_order': [ 'editor_assessments_order': [
...@@ -228,7 +232,8 @@ class StudioMixin(object): ...@@ -228,7 +232,8 @@ class StudioMixin(object):
self.rubric_feedback_default_text = data['feedback_default_text'] self.rubric_feedback_default_text = data['feedback_default_text']
self.submission_start = data['submission_start'] self.submission_start = data['submission_start']
self.submission_due = data['submission_due'] 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.allow_latex = bool(data['allow_latex'])
self.leaderboard_show = data['leaderboard_show'] self.leaderboard_show = data['leaderboard_show']
......
...@@ -29,6 +29,23 @@ class SubmissionMixin(object): ...@@ -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 @XBlock.json_handler
def submit(self, data, suffix=''): def submit(self, data, suffix=''):
"""Place the submission text into Openassessment system """Place the submission text into Openassessment system
...@@ -172,7 +189,7 @@ class SubmissionMixin(object): ...@@ -172,7 +189,7 @@ class SubmissionMixin(object):
# so that later we can add additional response fields. # so that later we can add additional response fields.
student_sub_dict = prepare_submission_for_serialization(student_sub_data) 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() student_sub_dict['file_key'] = self._get_student_item_key()
submission = api.create_submission(student_item_dict, student_sub_dict) submission = api.create_submission(student_item_dict, student_sub_dict)
self.create_workflow(submission["uuid"]) self.create_workflow(submission["uuid"])
...@@ -203,13 +220,25 @@ class SubmissionMixin(object): ...@@ -203,13 +220,25 @@ class SubmissionMixin(object):
A URL to be used to upload content associated with this submission. A URL to be used to upload content associated with this submission.
""" """
if "contentType" not in data: if 'contentType' not in data or 'filename' not in data:
return {'success': False, 'msg': self._(u"Must specify contentType.")} return {'success': False, 'msg': self._(u"There was an error uploading your file.")}
content_type = data['contentType'] 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/'): if self.file_upload_type == 'pdf-and-image' and content_type not in self.ALLOWED_FILE_MIME_TYPES:
return {'success': False, 'msg': self._(u"contentType must be an image.")} 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: try:
key = self._get_student_item_key() key = self._get_student_item_key()
url = file_upload_api.get_upload_url(key, content_type) url = file_upload_api.get_upload_url(key, content_type)
...@@ -249,8 +278,9 @@ class SubmissionMixin(object): ...@@ -249,8 +278,9 @@ class SubmissionMixin(object):
A string representation of the key. A string representation of the key.
""" """
student_item_dict = self.get_student_item_dict()
return u"{student_id}/{course_id}/{item_id}".format( 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): def get_download_url_from_submission(self, submission):
...@@ -308,7 +338,8 @@ class SubmissionMixin(object): ...@@ -308,7 +338,8 @@ class SubmissionMixin(object):
Returns: Returns:
unicode 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 @XBlock.handler
def render_submission(self, data, suffix=''): def render_submission(self, data, suffix=''):
...@@ -354,13 +385,15 @@ class SubmissionMixin(object): ...@@ -354,13 +385,15 @@ class SubmissionMixin(object):
if due_date < DISTANT_FUTURE: if due_date < DISTANT_FUTURE:
context["submission_due"] = due_date 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['allow_latex'] = self.allow_latex
context['has_peer'] = 'peer-assessment' in self.assessment_steps context['has_peer'] = 'peer-assessment' in self.assessment_steps
context['has_self'] = 'self-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() 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 not workflow and problem_closed:
if reason == 'due': 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 @@ ...@@ -21,7 +21,7 @@
"submission_due": "4014-02-27T09:46:28", "submission_due": "4014-02-27T09:46:28",
"submission_start": "4014-02-10T09:46:28", "submission_start": "4014-02-10T09:46:28",
"title": "My new title.", "title": "My new title.",
"allow_file_upload": false, "file_upload_type": null,
"assessments": [ "assessments": [
{ {
"name": "peer-assessment", "name": "peer-assessment",
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"title": "My new title.", "title": "My new title.",
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"assessments": [ "assessments": [
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
"no_prompt": { "no_prompt": {
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
"no_feedback_prompt": { "no_feedback_prompt": {
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -131,7 +131,7 @@ ...@@ -131,7 +131,7 @@
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -182,7 +182,7 @@ ...@@ -182,7 +182,7 @@
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -235,7 +235,7 @@ ...@@ -235,7 +235,7 @@
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -288,7 +288,7 @@ ...@@ -288,7 +288,7 @@
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -333,7 +333,7 @@ ...@@ -333,7 +333,7 @@
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -377,7 +377,7 @@ ...@@ -377,7 +377,7 @@
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -400,7 +400,7 @@ ...@@ -400,7 +400,7 @@
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -444,7 +444,7 @@ ...@@ -444,7 +444,7 @@
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -474,7 +474,7 @@ ...@@ -474,7 +474,7 @@
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -513,7 +513,7 @@ ...@@ -513,7 +513,7 @@
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -557,7 +557,7 @@ ...@@ -557,7 +557,7 @@
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -596,11 +596,11 @@ ...@@ -596,11 +596,11 @@
"expected_error": "error updating xblock configuration" "expected_error": "error updating xblock configuration"
}, },
"allow_file_upload_must_be_boolean": { "invalid_file_upload_type": {
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"allow_file_upload": 6, "file_upload_type": "invalid",
"allow_latex": false, "allow_latex": false,
"criteria": [ "criteria": [
{ {
...@@ -714,7 +714,7 @@ ...@@ -714,7 +714,7 @@
"submission_due": "4014-02-27T09:46", "submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46", "submission_start": "4014-02-10T09:46",
"title": "My new title.", "title": "My new title.",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"assessments": [ "assessments": [
...@@ -771,7 +771,7 @@ ...@@ -771,7 +771,7 @@
"submission_due": "4014-02-27T09:46", "submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46", "submission_start": "4014-02-10T09:46",
"title": "My new title.", "title": "My new title.",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"assessments": [ "assessments": [
...@@ -826,7 +826,7 @@ ...@@ -826,7 +826,7 @@
"submission_due": "4014-02-27T09:46", "submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46", "submission_start": "4014-02-10T09:46",
"title": "My new title.", "title": "My new title.",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"assessments": [ "assessments": [
...@@ -884,7 +884,7 @@ ...@@ -884,7 +884,7 @@
"submission_due": "4014-02-27T09:46", "submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46", "submission_start": "4014-02-10T09:46",
"title": "My new title.", "title": "My new title.",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"assessments": [ "assessments": [
...@@ -942,7 +942,7 @@ ...@@ -942,7 +942,7 @@
"submission_due": "4014-02-27T09:46", "submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46", "submission_start": "4014-02-10T09:46",
"title": "My new title.", "title": "My new title.",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"assessments": [ "assessments": [
...@@ -994,7 +994,7 @@ ...@@ -994,7 +994,7 @@
"submission_due": "4014-02-27T09:46", "submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46", "submission_start": "4014-02-10T09:46",
"title": "My new title.", "title": "My new title.",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"assessments": [ "assessments": [
...@@ -1053,7 +1053,7 @@ ...@@ -1053,7 +1053,7 @@
"submission_due": "4014-02-27T09:46", "submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46", "submission_start": "4014-02-10T09:46",
"title": "My new title.", "title": "My new title.",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"assessments": [ "assessments": [
...@@ -1104,7 +1104,7 @@ ...@@ -1104,7 +1104,7 @@
"submission_due": "4014-02-27T09:46", "submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46", "submission_start": "4014-02-10T09:46",
"title": "My new title.", "title": "My new title.",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"assessments": [ "assessments": [
...@@ -1156,7 +1156,7 @@ ...@@ -1156,7 +1156,7 @@
"submission_due": "4014-02-27T09:46", "submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46", "submission_start": "4014-02-10T09:46",
"title": "My new title.", "title": "My new title.",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"assessments": [ "assessments": [
...@@ -1184,7 +1184,7 @@ ...@@ -1184,7 +1184,7 @@
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -1235,7 +1235,7 @@ ...@@ -1235,7 +1235,7 @@
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -1286,7 +1286,7 @@ ...@@ -1286,7 +1286,7 @@
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
...@@ -1360,7 +1360,7 @@ ...@@ -1360,7 +1360,7 @@
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}], "prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt", "feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"allow_file_upload": false, "file_upload_type": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"criteria": [ "criteria": [
......
<openassessment leaderboard_show="3" allow_file_upload="True"> <openassessment leaderboard_show="3" file_upload_type="image">
<title>Open Assessment Test</title> <title>Open Assessment Test</title>
<prompts> <prompts>
<prompt> <prompt>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -80,7 +80,7 @@ ...@@ -80,7 +80,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -150,7 +150,7 @@ ...@@ -150,7 +150,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -291,7 +291,7 @@ ...@@ -291,7 +291,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -358,7 +358,7 @@ ...@@ -358,7 +358,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -428,7 +428,7 @@ ...@@ -428,7 +428,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -496,7 +496,7 @@ ...@@ -496,7 +496,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -560,7 +560,7 @@ ...@@ -560,7 +560,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 2, "order_num": 2,
...@@ -643,7 +643,7 @@ ...@@ -643,7 +643,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -785,7 +785,7 @@ ...@@ -785,7 +785,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -856,7 +856,7 @@ ...@@ -856,7 +856,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -927,7 +927,7 @@ ...@@ -927,7 +927,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -990,7 +990,7 @@ ...@@ -990,7 +990,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -1101,7 +1101,7 @@ ...@@ -1101,7 +1101,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -1244,7 +1244,7 @@ ...@@ -1244,7 +1244,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -1343,12 +1343,78 @@ ...@@ -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", "title": "Foo",
"prompt": "Test prompt", "prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt", "rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...", "rubric_feedback_default_text": "Test default text...",
"allow_file_upload": true, "file_upload_type": "image",
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -1385,7 +1451,7 @@ ...@@ -1385,7 +1451,7 @@
} }
], ],
"expected_xml": [ "expected_xml": [
"<openassessment allow_file_upload=\"True\">", "<openassessment file_upload_type=\"image\">",
"<title>Foo</title>", "<title>Foo</title>",
"<assessments>", "<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=\"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 @@ ...@@ -1417,7 +1483,7 @@
"due": null, "due": null,
"submission_start": null, "submission_start": null,
"submission_due": null, "submission_due": null,
"allow_file_upload": null, "file_upload_type": null,
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
...@@ -1482,7 +1548,7 @@ ...@@ -1482,7 +1548,7 @@
"prompt": "Test prompt", "prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt", "rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "", "rubric_feedback_default_text": "",
"allow_file_upload": true, "file_upload_type": "image",
"start": null, "start": null,
"due": null, "due": null,
"submission_start": null, "submission_start": null,
...@@ -1523,7 +1589,7 @@ ...@@ -1523,7 +1589,7 @@
} }
], ],
"expected_xml": [ "expected_xml": [
"<openassessment allow_file_upload=\"True\">", "<openassessment file_upload_type=\"image\">",
"<title>Foo</title>", "<title>Foo</title>",
"<assessments>", "<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=\"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 @@ ...@@ -676,9 +676,31 @@
] ]
}, },
"file_upload": { "file_upload_type_none": {
"xml": [ "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>", "<title>Foo</title>",
"<assessments>", "<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=\"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 @@ ...@@ -695,7 +717,7 @@
"</rubric>", "</rubric>",
"</openassessment>" "</openassessment>"
], ],
"allow_file_upload": true "file_upload_type": "image"
}, },
"leaderboard": { "leaderboard": {
......
...@@ -31,7 +31,8 @@ ...@@ -31,7 +31,8 @@
"submission_due": "4014-02-27T09:46", "submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46", "submission_start": "4014-02-10T09:46",
"title": "My new title.", "title": "My new title.",
"allow_file_upload": false, "file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"assessments": [ "assessments": [
...@@ -83,7 +84,8 @@ ...@@ -83,7 +84,8 @@
"submission_due": "4014-02-27T09:46", "submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46", "submission_start": "4014-02-10T09:46",
"title": "ɯʎ uǝʍ ʇıʇןǝ", "title": "ɯʎ uǝʍ ʇıʇןǝ",
"allow_file_upload": false, "file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"assessments": [ "assessments": [
...@@ -135,7 +137,8 @@ ...@@ -135,7 +137,8 @@
"submission_due": "4014-02-27T09:46", "submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46", "submission_start": "4014-02-10T09:46",
"title": "My new title.", "title": "My new title.",
"allow_file_upload": false, "file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"assessments": [ "assessments": [
...@@ -198,7 +201,167 @@ ...@@ -198,7 +201,167 @@
"feedback_default_text": "Feedback default text", "feedback_default_text": "Feedback default text",
"submission_due": "4014-02-27T09:46", "submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09: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, "allow_latex": false,
"leaderboard_show": 0, "leaderboard_show": 0,
"title": "My new title.", "title": "My new title.",
......
...@@ -368,7 +368,7 @@ class TestPeerAssessmentRender(XBlockHandlerTestCase): ...@@ -368,7 +368,7 @@ class TestPeerAssessmentRender(XBlockHandlerTestCase):
'must_grade': 5, 'must_grade': 5,
'review_num': 1, 'review_num': 1,
'peer_submission': create_submission_dict(submission, xblock.prompts), 'peer_submission': create_submission_dict(submission, xblock.prompts),
'allow_file_upload': False, 'file_upload_type': None,
'peer_file_url': '', 'peer_file_url': '',
'submit_button_text': 'submit your assessment & move to response #2', 'submit_button_text': 'submit your assessment & move to response #2',
'allow_latex': False, 'allow_latex': False,
...@@ -538,7 +538,7 @@ class TestPeerAssessmentRender(XBlockHandlerTestCase): ...@@ -538,7 +538,7 @@ class TestPeerAssessmentRender(XBlockHandlerTestCase):
'must_grade': 5, 'must_grade': 5,
'peer_due': dt.datetime(2000, 1, 1).replace(tzinfo=pytz.utc), 'peer_due': dt.datetime(2000, 1, 1).replace(tzinfo=pytz.utc),
'peer_submission': create_submission_dict(submission, xblock.prompts), 'peer_submission': create_submission_dict(submission, xblock.prompts),
'allow_file_upload': False, 'file_upload_type': None,
'peer_file_url': '', 'peer_file_url': '',
'review_num': 1, 'review_num': 1,
'rubric_criteria': xblock.rubric_criteria, 'rubric_criteria': xblock.rubric_criteria,
......
...@@ -266,7 +266,7 @@ class TestSelfAssessmentRender(XBlockHandlerTestCase): ...@@ -266,7 +266,7 @@ class TestSelfAssessmentRender(XBlockHandlerTestCase):
'rubric_criteria': xblock.rubric_criteria, 'rubric_criteria': xblock.rubric_criteria,
'estimated_time': '20 minutes', 'estimated_time': '20 minutes',
'self_submission': submission, 'self_submission': submission,
'allow_file_upload': False, 'file_upload_type': None,
'self_file_url': '', 'self_file_url': '',
'allow_latex': False, 'allow_latex': False,
}, },
......
...@@ -337,7 +337,7 @@ class TestCourseStaff(XBlockHandlerTestCase): ...@@ -337,7 +337,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
file_api.get_download_url.assert_called_with("test_key") file_api.get_download_url.assert_called_with("test_key")
# Check the context passed to the template # Check the context passed to the template
self.assertEquals('http://www.example.com/image.jpeg', context['submission']['image_url']) self.assertEquals('http://www.example.com/image.jpeg', context['submission']['file_url'])
# Check the fully rendered template # Check the fully rendered template
payload = urllib.urlencode({"student_username": "Bob"}) payload = urllib.urlencode({"student_username": "Bob"})
...@@ -368,7 +368,7 @@ class TestCourseStaff(XBlockHandlerTestCase): ...@@ -368,7 +368,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
# Expect that the page still renders, but without the image url # Expect that the page still renders, but without the image url
self.assertIn('submission', context) self.assertIn('submission', context)
self.assertNotIn('image_url', context['submission']) self.assertNotIn('file_url', context['submission'])
# Check the fully rendered template # Check the fully rendered template
payload = urllib.urlencode({"student_username": "Bob"}) payload = urllib.urlencode({"student_username": "Bob"})
......
...@@ -23,7 +23,8 @@ class StudioViewTest(XBlockHandlerTestCase): ...@@ -23,7 +23,8 @@ class StudioViewTest(XBlockHandlerTestCase):
"feedback_default_text": "Test feedback default text", "feedback_default_text": "Test feedback default text",
"submission_start": "4014-02-10T09:46", "submission_start": "4014-02-10T09:46",
"submission_due": "4014-02-27T09:46", "submission_due": "4014-02-27T09:46",
"allow_file_upload": False, "file_upload_type": None,
"white_listed_file_types": '',
"allow_latex": False, "allow_latex": False,
"leaderboard_show": 4, "leaderboard_show": 4,
"assessments": [{"name": "self-assessment"}], "assessments": [{"name": "self-assessment"}],
...@@ -151,7 +152,6 @@ class StudioViewTest(XBlockHandlerTestCase): ...@@ -151,7 +152,6 @@ class StudioViewTest(XBlockHandlerTestCase):
self.assertTrue(resp['success'], msg=resp.get('msg')) self.assertTrue(resp['success'], msg=resp.get('msg'))
self.assertEqual(xblock.editor_assessments_order, data['editor_assessments_order']) self.assertEqual(xblock.editor_assessments_order, data['editor_assessments_order'])
@scenario('data/basic_scenario.xml') @scenario('data/basic_scenario.xml')
def test_update_editor_context_saves_assessment_order_with_ai(self, xblock): def test_update_editor_context_saves_assessment_order_with_ai(self, xblock):
# Update the XBlock with a different editor assessment order # Update the XBlock with a different editor assessment order
......
...@@ -5,11 +5,16 @@ Test submission to the OpenAssessment XBlock. ...@@ -5,11 +5,16 @@ Test submission to the OpenAssessment XBlock.
import json import json
import datetime as dt import datetime as dt
import boto
from boto.s3.key import Key
from django.test.utils import override_settings
from mock import patch, Mock from mock import patch, Mock
from moto import mock_s3
import pytz import pytz
from submissions import api as sub_api from submissions import api as sub_api
from submissions.api import SubmissionRequestError, SubmissionInternalError from submissions.api import SubmissionRequestError, SubmissionInternalError
from openassessment.fileupload import api
from openassessment.workflow import api as workflow_api from openassessment.workflow import api as workflow_api
from openassessment.xblock.openassessmentblock import OpenAssessmentBlock from openassessment.xblock.openassessmentblock import OpenAssessmentBlock
...@@ -19,7 +24,6 @@ from .base import XBlockHandlerTestCase, scenario ...@@ -19,7 +24,6 @@ from .base import XBlockHandlerTestCase, scenario
class SubmissionTest(XBlockHandlerTestCase): class SubmissionTest(XBlockHandlerTestCase):
SUBMISSION = json.dumps({ SUBMISSION = json.dumps({
"submission": ["This is my answer to the first question!", "This is my answer to the second question!"] "submission": ["This is my answer to the first question!", "This is my answer to the second question!"]
}) })
...@@ -33,7 +37,8 @@ class SubmissionTest(XBlockHandlerTestCase): ...@@ -33,7 +37,8 @@ class SubmissionTest(XBlockHandlerTestCase):
def test_submit_answer_too_long(self, xblock): def test_submit_answer_too_long(self, xblock):
# Maximum answer length is 100K, once the answer has been JSON-encoded # Maximum answer length is 100K, once the answer has been JSON-encoded
long_submission = json.dumps({ long_submission = json.dumps({
"submission": ["This is my answer to the first question!" * 100000, "This is my answer to the second question!"] "submission": ["This is my answer to the first question!" * 100000,
"This is my answer to the second question!"]
}) })
resp = self.request(xblock, 'submit', long_submission, response_format='json') resp = self.request(xblock, 'submit', long_submission, response_format='json')
self.assertFalse(resp[0]) self.assertFalse(resp[0])
...@@ -72,7 +77,6 @@ class SubmissionTest(XBlockHandlerTestCase): ...@@ -72,7 +77,6 @@ class SubmissionTest(XBlockHandlerTestCase):
# In Studio preview mode, the runtime sets the user ID to None # In Studio preview mode, the runtime sets the user ID to None
@scenario('data/basic_scenario.xml', user_id=None) @scenario('data/basic_scenario.xml', user_id=None)
def test_cannot_submit_in_preview_mode(self, xblock): def test_cannot_submit_in_preview_mode(self, xblock):
# The Studio runtime apparently provides an anonymous student ID, # The Studio runtime apparently provides an anonymous student ID,
# even though we're running in Preview mode. We should check the scope id # even though we're running in Preview mode. We should check the scope id
# to determine whether we're in Preview mode or not. # to determine whether we're in Preview mode or not.
...@@ -98,6 +102,66 @@ class SubmissionTest(XBlockHandlerTestCase): ...@@ -98,6 +102,66 @@ class SubmissionTest(XBlockHandlerTestCase):
expected_prompt = u"<p><br />Line 1</p><p>Line 2</p><p>Line 3<br /></p>" expected_prompt = u"<p><br />Line 1</p><p>Line 2</p><p>Line 3<br /></p>"
self.assertIn(expected_prompt, resp) self.assertIn(expected_prompt, resp)
@mock_s3
@override_settings(
AWS_ACCESS_KEY_ID='foobar',
AWS_SECRET_ACCESS_KEY='bizbaz',
FILE_UPLOAD_STORAGE_BUCKET_NAME="mybucket"
)
@scenario('data/file_upload_scenario.xml')
def test_upload_url(self, xblock):
""" Test generate correct upload URL """
xblock.xmodule_runtime = Mock(
course_id='test_course',
anonymous_student_id='test_student',
)
resp = self.request(xblock, 'upload_url', json.dumps({"contentType": "image/jpeg",
"filename": "test.jpg"}), response_format='json')
self.assertTrue(resp['success'])
self.assertTrue(resp['url'].startswith(
'https://mybucket.s3.amazonaws.com/submissions_attachments/test_student/test_course/' + xblock.scope_ids.usage_id
))
@mock_s3
@override_settings(
AWS_ACCESS_KEY_ID='foobar',
AWS_SECRET_ACCESS_KEY='bizbaz',
FILE_UPLOAD_STORAGE_BUCKET_NAME="mybucket"
)
@scenario('data/file_upload_scenario.xml')
def test_download_url(self, xblock):
""" Test generate correct download URL with existing file. should create a file and get the download URL """
conn = boto.connect_s3()
bucket = conn.create_bucket('mybucket')
key = Key(bucket)
key.key = "submissions_attachments/test_student/test_course/" + xblock.scope_ids.usage_id
key.set_contents_from_string("How d'ya do?")
download_url = api.get_download_url("test_student/test_course/" + xblock.scope_ids.usage_id)
xblock.xmodule_runtime = Mock(
course_id='test_course',
anonymous_student_id='test_student',
)
resp = self.request(xblock, 'download_url', json.dumps(dict()), response_format='json')
self.assertTrue(resp['success'])
self.assertEqual(download_url, resp['url'])
@mock_s3
@override_settings(
AWS_ACCESS_KEY_ID='foobar',
AWS_SECRET_ACCESS_KEY='bizbaz',
FILE_UPLOAD_STORAGE_BUCKET_NAME="mybucket"
)
@scenario('data/file_upload_scenario.xml')
def test_download_url_non_existing_file(self, xblock):
""" Test generate a download URL for non-existing file, should return empty string """
resp = self.request(xblock, 'download_url', json.dumps(dict()), response_format='json')
self.assertTrue(resp['success'])
self.assertEqual(u'', resp['url'])
class SubmissionRenderTest(XBlockHandlerTestCase): class SubmissionRenderTest(XBlockHandlerTestCase):
""" """
...@@ -114,7 +178,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase): ...@@ -114,7 +178,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context( self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response_unavailable.html', xblock, 'openassessmentblock/response/oa_response_unavailable.html',
{ {
'allow_file_upload': False, 'file_upload_type': None,
'submission_start': dt.datetime(4999, 4, 1).replace(tzinfo=pytz.utc), 'submission_start': dt.datetime(4999, 4, 1).replace(tzinfo=pytz.utc),
'has_peer': True, 'has_peer': True,
'has_self': True, 'has_self': True,
...@@ -137,7 +201,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase): ...@@ -137,7 +201,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
xblock, 'openassessmentblock/response/oa_response_submitted.html', xblock, 'openassessmentblock/response/oa_response_submitted.html',
{ {
'student_submission': create_submission_dict(submission, xblock.prompts), 'student_submission': create_submission_dict(submission, xblock.prompts),
'allow_file_upload': False, 'file_upload_type': None,
'has_peer': True, 'has_peer': True,
'has_self': True, 'has_self': True,
'allow_latex': False, 'allow_latex': False,
...@@ -149,7 +213,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase): ...@@ -149,7 +213,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context( self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response.html', xblock, 'openassessmentblock/response/oa_response.html',
{ {
'allow_file_upload': False, 'file_upload_type': None,
'saved_response': create_submission_dict({ 'saved_response': create_submission_dict({
'answer': prepare_submission_for_serialization( 'answer': prepare_submission_for_serialization(
("", "") ("", "")
...@@ -169,7 +233,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase): ...@@ -169,7 +233,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context( self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response.html', xblock, 'openassessmentblock/response/oa_response.html',
{ {
'allow_file_upload': False, 'file_upload_type': None,
'saved_response': create_submission_dict({ 'saved_response': create_submission_dict({
'answer': prepare_submission_for_serialization( 'answer': prepare_submission_for_serialization(
("", "") ("", "")
...@@ -193,7 +257,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase): ...@@ -193,7 +257,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context( self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response.html', xblock, 'openassessmentblock/response/oa_response.html',
{ {
'allow_file_upload': False, 'file_upload_type': None,
'saved_response': create_submission_dict({ 'saved_response': create_submission_dict({
'answer': prepare_submission_for_serialization( 'answer': prepare_submission_for_serialization(
('A man must have a code', 'A man must have an umbrella too.') ('A man must have a code', 'A man must have an umbrella too.')
...@@ -218,7 +282,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase): ...@@ -218,7 +282,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context( self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response.html', xblock, 'openassessmentblock/response/oa_response.html',
{ {
'allow_file_upload': False, 'file_upload_type': None,
'saved_response': create_submission_dict({ 'saved_response': create_submission_dict({
'answer': prepare_submission_for_serialization( 'answer': prepare_submission_for_serialization(
('An old format response.',) ('An old format response.',)
...@@ -232,6 +296,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase): ...@@ -232,6 +296,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
'allow_latex': False, 'allow_latex': False,
} }
) )
@scenario('data/submission_open.xml', user_id="Bob") @scenario('data/submission_open.xml', user_id="Bob")
def test_open_submitted(self, xblock): def test_open_submitted(self, xblock):
submission = xblock.create_submission( submission = xblock.create_submission(
...@@ -243,7 +308,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase): ...@@ -243,7 +308,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
{ {
'submission_due': dt.datetime(2999, 5, 6).replace(tzinfo=pytz.utc), 'submission_due': dt.datetime(2999, 5, 6).replace(tzinfo=pytz.utc),
'student_submission': create_submission_dict(submission, xblock.prompts), 'student_submission': create_submission_dict(submission, xblock.prompts),
'allow_file_upload': False, 'file_upload_type': None,
'has_peer': True, 'has_peer': True,
'has_self': True, 'has_self': True,
'allow_latex': False, 'allow_latex': False,
...@@ -274,7 +339,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase): ...@@ -274,7 +339,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context( self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response_cancelled.html', xblock, 'openassessmentblock/response/oa_response_cancelled.html',
{ {
'allow_file_upload': False, 'file_upload_type': None,
'allow_latex': False, 'allow_latex': False,
'has_peer': True, 'has_peer': True,
'has_self': True, 'has_self': True,
...@@ -307,7 +372,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase): ...@@ -307,7 +372,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
'student_submission': {"answer": {"parts": [ 'student_submission': {"answer": {"parts": [
{"prompt": {'description': 'One prompt.'}, "text": "An old format response."} {"prompt": {'description': 'One prompt.'}, "text": "An old format response."}
]}}, ]}},
'allow_file_upload': False, 'file_upload_type': None,
'has_peer': True, 'has_peer': True,
'has_self': True, 'has_self': True,
'allow_latex': False, 'allow_latex': False,
...@@ -319,7 +384,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase): ...@@ -319,7 +384,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context( self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response_closed.html', xblock, 'openassessmentblock/response/oa_response_closed.html',
{ {
'allow_file_upload': False, 'file_upload_type': None,
'submission_due': dt.datetime(2014, 4, 5).replace(tzinfo=pytz.utc), 'submission_due': dt.datetime(2014, 4, 5).replace(tzinfo=pytz.utc),
'has_peer': False, 'has_peer': False,
'has_self': True, 'has_self': True,
...@@ -338,7 +403,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase): ...@@ -338,7 +403,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
{ {
'submission_due': dt.datetime(2014, 4, 5).replace(tzinfo=pytz.utc), 'submission_due': dt.datetime(2014, 4, 5).replace(tzinfo=pytz.utc),
'student_submission': create_submission_dict(submission, xblock.prompts), 'student_submission': create_submission_dict(submission, xblock.prompts),
'allow_file_upload': False, 'file_upload_type': None,
'has_peer': False, 'has_peer': False,
'has_self': True, 'has_self': True,
'allow_latex': False, 'allow_latex': False,
...@@ -364,7 +429,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase): ...@@ -364,7 +429,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
{ {
'submission_due': dt.datetime(2999, 5, 6).replace(tzinfo=pytz.utc), 'submission_due': dt.datetime(2999, 5, 6).replace(tzinfo=pytz.utc),
'student_submission': create_submission_dict(submission, xblock.prompts), 'student_submission': create_submission_dict(submission, xblock.prompts),
'allow_file_upload': False, 'file_upload_type': None,
'has_peer': True, 'has_peer': True,
'has_self': True, 'has_self': True,
'allow_latex': False, 'allow_latex': False,
...@@ -390,7 +455,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase): ...@@ -390,7 +455,7 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
{ {
'submission_due': dt.datetime(2014, 4, 5).replace(tzinfo=pytz.utc), 'submission_due': dt.datetime(2014, 4, 5).replace(tzinfo=pytz.utc),
'student_submission': create_submission_dict(submission, xblock.prompts), 'student_submission': create_submission_dict(submission, xblock.prompts),
'allow_file_upload': False, 'file_upload_type': None,
'has_peer': False, 'has_peer': False,
'has_self': True, 'has_self': True,
'allow_latex': False, 'allow_latex': False,
......
...@@ -122,7 +122,8 @@ class TestSerializeContent(TestCase): ...@@ -122,7 +122,8 @@ class TestSerializeContent(TestCase):
self.oa_block.rubric_criteria = data.get('criteria', copy.deepcopy(self.BASIC_CRITERIA)) 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.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.allow_latex = data.get('allow_latex')
self.oa_block.leaderboard_show = data.get('leaderboard_show', 0) self.oa_block.leaderboard_show = data.get('leaderboard_show', 0)
...@@ -492,7 +493,8 @@ class TestParseFromXml(TestCase): ...@@ -492,7 +493,8 @@ class TestParseFromXml(TestCase):
'submission_due', 'submission_due',
'criteria', 'criteria',
'assessments', 'assessments',
'allow_file_upload', 'file_upload_type',
'white_listed_file_types',
'allow_latex', 'allow_latex',
'leaderboard_show' 'leaderboard_show'
] ]
......
...@@ -686,9 +686,13 @@ def serialize_content_to_xml(oa_block, root): ...@@ -686,9 +686,13 @@ def serialize_content_to_xml(oa_block, root):
if oa_block.leaderboard_show: if oa_block.leaderboard_show:
root.set('leaderboard_show', unicode(oa_block.leaderboard_show)) root.set('leaderboard_show', unicode(oa_block.leaderboard_show))
# Allow file upload # Set File upload settings
if oa_block.allow_file_upload is not None: if oa_block.file_upload_type:
root.set('allow_file_upload', unicode(oa_block.allow_file_upload)) 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: if oa_block.allow_latex is not None:
root.set('allow_latex', unicode(oa_block.allow_latex)) root.set('allow_latex', unicode(oa_block.allow_latex))
...@@ -815,10 +819,18 @@ def parse_from_xml(root): ...@@ -815,10 +819,18 @@ def parse_from_xml(root):
if 'submission_due' in root.attrib: if 'submission_due' in root.attrib:
submission_due = parse_date(unicode(root.attrib['submission_due']), name="submission due date") 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: if 'allow_file_upload' in root.attrib:
allow_file_upload = _parse_boolean(unicode(root.attrib['allow_file_upload'])) 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 allow_latex = False
if 'allow_latex' in root.attrib: if 'allow_latex' in root.attrib:
allow_latex = _parse_boolean(unicode(root.attrib['allow_latex'])) allow_latex = _parse_boolean(unicode(root.attrib['allow_latex']))
...@@ -865,6 +877,8 @@ def parse_from_xml(root): ...@@ -865,6 +877,8 @@ def parse_from_xml(root):
'submission_start': submission_start, 'submission_start': submission_start,
'submission_due': submission_due, 'submission_due': submission_due,
'allow_file_upload': allow_file_upload, 'allow_file_upload': allow_file_upload,
'file_upload_type': file_upload_type,
'white_listed_file_types': white_listed_file_types,
'allow_latex': allow_latex, 'allow_latex': allow_latex,
'leaderboard_show': leaderboard_show 'leaderboard_show': leaderboard_show
} }
......
...@@ -51,6 +51,10 @@ class OpenAssessmentPage(PageObject): ...@@ -51,6 +51,10 @@ class OpenAssessmentPage(PageObject):
with self.handle_alert(): with self.handle_alert():
self.q(css=".action--submit").first.click() 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): class SubmissionPage(OpenAssessmentPage):
""" """
...@@ -90,6 +94,23 @@ class SubmissionPage(OpenAssessmentPage): ...@@ -90,6 +94,23 @@ class SubmissionPage(OpenAssessmentPage):
self.q(css="button#submission__preview").click() self.q(css="button#submission__preview").click()
self.wait_for_element_visibility("#preview_content .MathJax", "Verify Preview Latex expression") 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 @property
def latex_preview_button_is_disabled(self): def latex_preview_button_is_disabled(self):
""" """
...@@ -111,6 +132,26 @@ class SubmissionPage(OpenAssessmentPage): ...@@ -111,6 +132,26 @@ class SubmissionPage(OpenAssessmentPage):
""" """
return self.q(css=".step--response.is--complete").is_present() 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): class AssessmentPage(OpenAssessmentPage):
""" """
......
...@@ -60,6 +60,9 @@ class OpenAssessmentTest(WebAppTest): ...@@ -60,6 +60,9 @@ class OpenAssessmentTest(WebAppTest):
'student_training': 'student_training':
u'courses/{test_course_id}/courseware/' u'courses/{test_course_id}/courseware/'
u'676026889c884ac1827688750871c825/5663e9b038434636977a4226d668fe02/'.format(test_course_id=TEST_COURSE_ID), 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." SUBMISSION = u"This is a test submission."
...@@ -282,6 +285,33 @@ class StaffAreaTest(OpenAssessmentTest): ...@@ -282,6 +285,33 @@ class StaffAreaTest(OpenAssessmentTest):
self.assertEqual(self.staff_area_page.visible_staff_panels, []) 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__": if __name__ == "__main__":
# Configure the screenshot directory # 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