Commit 32dadd8d by Eric Fischer Committed by GitHub

Merge pull request #964 from CredoReference/multiple-file-uploads-pr

Multiple file uploads support
parents ded09bd7 7568a700
......@@ -105,7 +105,8 @@ def create_assessment(
Args:
submission_uuid (str): The unique identifier for the submission being assessed.
user_id (str): The ID of the user creating the assessment. This must match the ID of the user who made the submission.
user_id (str): The ID of the user creating the assessment.
This must match the ID of the user who made the submission.
options_selected (dict): Mapping of rubric criterion names to option values selected.
criterion_feedback (dict): Dictionary mapping criterion names to the
free-form text feedback the user gave for the criterion.
......
......@@ -18,3 +18,10 @@ def get_download_url(key):
Returns the url at which the file that corresponds to the key can be downloaded.
"""
return backends.get_backend().get_download_url(key)
def remove_file(key):
"""
Remove file from the storage
"""
return backends.get_backend().remove_file(key)
......@@ -90,6 +90,19 @@ class BaseBackend(object):
"""
raise NotImplementedError
@abc.abstractmethod
def remove_file(self, key):
"""
Remove file from the storage
Args:
key (str): A unique identifier used to identify the data requested for remove.
Returns:
True if file was successfully removed or False is file was not removed or was not was not found.
"""
raise NotImplementedError
def _retrieve_parameters(self, key):
"""
Simple utility function to validate settings and arguments before compiling
......
from .base import BaseBackend
from .. import exceptions
......@@ -42,6 +41,10 @@ class Backend(BaseBackend):
make_download_url_available(self._get_key_name(key), self.DOWNLOAD_URL_TIMEOUT)
return self._get_url(key)
def remove_file(self, key):
from openassessment.fileupload.views_filesystem import safe_remove, get_file_path
return safe_remove(get_file_path(self._get_key_name(key)))
def _get_url(self, key):
key_name = self._get_key_name(key)
url = reverse("openassessment-filesystem-storage", kwargs={'key': key_name})
......
......@@ -28,7 +28,6 @@ class Backend(BaseBackend):
)
raise FileUploadInternalError(ex)
def get_download_url(self, key):
bucket_name, key_name = self._retrieve_parameters(key)
try:
......@@ -42,6 +41,18 @@ class Backend(BaseBackend):
)
raise FileUploadInternalError(ex)
def remove_file(self, key):
bucket_name, key_name = self._retrieve_parameters(key)
conn = _connect_to_s3()
bucket = conn.get_bucket(bucket_name)
s3_key = bucket.get_key(key_name)
if s3_key:
bucket.delete_key(s3_key)
return True
else:
return False
def _connect_to_s3():
"""Connect to s3
......
......@@ -63,6 +63,24 @@ class Backend(BaseBackend):
)
raise FileUploadInternalError(ex)
def remove_file(self, key):
bucket_name, key_name = self._retrieve_parameters(key)
key, url = get_settings()
try:
temp_url = swiftclient.utils.generate_temp_url(
path='%s/%s/%s' % (url.path, bucket_name, key_name),
key=key,
method='DELETE',
seconds=self.DOWNLOAD_URL_TIMEOUT)
remove_url = '%s://%s%s' % (url.scheme, url.netloc, temp_url)
response = requests.delete(remove_url)
return response.status_code == 204
except Exception as ex:
logger.exception(
u"An internal exception occurred while removing object on swift storage."
)
raise FileUploadInternalError(ex)
def get_settings():
"""
......
......@@ -57,6 +57,23 @@ class TestFileUploadService(TestCase):
downloadUrl = api.get_download_url("foo")
self.assertIn("https://mybucket.s3.amazonaws.com/submissions_attachments/foo", downloadUrl)
@mock_s3
@override_settings(
AWS_ACCESS_KEY_ID='foobar',
AWS_SECRET_ACCESS_KEY='bizbaz',
FILE_UPLOAD_STORAGE_BUCKET_NAME="mybucket"
)
def test_remove_file(self):
conn = boto.connect_s3()
bucket = conn.create_bucket('mybucket')
key = Key(bucket)
key.key = "submissions_attachments/foo"
key.set_contents_from_string("Test")
result = api.remove_file("foo")
self.assertTrue(result)
result = api.remove_file("foo")
self.assertFalse(result)
@raises(exceptions.FileUploadInternalError)
def test_get_upload_url_no_bucket(self):
api.get_upload_url("foo", "bar")
......@@ -280,6 +297,15 @@ class TestFileUploadServiceWithFilesystemBackend(TestCase):
self.assertEqual(200, upload_response.status_code)
self.assertEqual(200, download_response.status_code)
def test_remove_file(self):
self.set_key(u"noël.jpg")
upload_url = self.backend.get_upload_url(self.key, self.content_type)
self.client.put(upload_url, data=self.content.read(), content_type=self.content_type)
result = self.backend.remove_file(self.key)
self.assertTrue(result)
result = self.backend.remove_file(self.key)
self.assertFalse(result)
@override_settings(
ORA2_FILEUPLOAD_BACKEND='swift',
......
......@@ -114,6 +114,8 @@ def safe_remove(path):
"""
if os.path.exists(path):
os.remove(path)
return True
return False
def get_file_path(key):
......
......@@ -101,32 +101,57 @@
</div>
<p class="setting-help">{% trans "The date and time when learners can no longer submit responses." %}</p>
</li>
<li id="openassessment_submission_text_response_wrapper" class="field comp-setting-entry">
<div class="wrapper-comp-setting">
<label for="openassessment_submission_text_response" class="setting-label">{% trans "Text Response"%}</label>
<select id="openassessment_submission_text_response" class="input setting-input" name="text response">
{% for option_key, option_name in necessity_options.items %}
<option value="{{ option_key }}" {% if option_key == text_response %} selected="true" {% endif %}>{{ option_name }}</option>
{% endfor %}
</select>
</div>
<p class="setting-help">
{% trans "Specify whether learners must include a text based response to this problem's prompt." %}
</p>
</li>
<li id="openassessment_submission_file_wrapper" class="field comp-setting-entry">
<div class="wrapper-comp-setting">
<label for="openassessment_submission_upload_selector" class="setting-label">{% trans "Allow File Upload"%}</label>
<select id="openassessment_submission_upload_selector" class="input setting-input" name="upload submission">
<option value="">{% trans "None"%}</option>
<option value="image" {% if file_upload_type == "image" %} selected="true" {% endif %}>{% trans "Image File"%}</option>
<option value="pdf-and-image" {% if file_upload_type == "pdf-and-image" %} selected="true" {% endif %}>{% trans "PDF or Image File"%}</option>
<option value="custom" {% if file_upload_type == "custom" %} selected="true" {% endif %}>{% trans "Custom File Types"%}</option>
<label for="openassessment_submission_file_upload_response" class="setting-label">{% trans "File Uploads Response"%}</label>
<select id="openassessment_submission_file_upload_response" class="input setting-input" name="text response">
{% for option_key, option_name in necessity_options.items %}
<option value="{{ option_key }}" {% if option_key == file_upload_response %} selected="true" {% endif %}>{{ option_name }}</option>
{% endfor %}
</select>
</div>
<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.)" %}
{% trans "Specify whether learners are able to upload files as a part of their response." %}
</p>
<div id="openassessment_submission_white_listed_file_types_wrapper" class="{% if file_upload_type != "custom" %}is--hidden{% endif %}">
<div id="openassessment_submission_file_upload_type_wrapper" class="{% if not file_upload_response %}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 }}"
/>
<label for="openassessment_submission_upload_selector" class="setting-label">{% trans "File Upload Types"%}</label>
<select id="openassessment_submission_upload_selector" class="input setting-input" name="upload submission">
<option value="pdf-and-image" {% if file_upload_type == "pdf-and-image" %} selected="true" {% endif %}>{% trans "PDF or Image Files"%}</option>
<option value="image" {% if file_upload_type == "image" %} selected="true" {% endif %}>{% trans "Image Files"%}</option>
<option value="custom" {% if file_upload_type == "custom" %} selected="true" {% endif %}>{% trans "Custom File Types"%}</option>
</select>
</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>
{% trans "Specify whether learners can submit files along with their text responses. Select Images to allow JPG, GIF, or PNG files. Select PDF or Images to allow PDF files and images. Select Custom File Types to allow files with extensions that you specify below. (Use the Select Custom File Types 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>
</div>
</li>
<li id="openassessment_submission_latex_wrapper" class="field comp-setting-entry">
......
......@@ -33,7 +33,7 @@
{% include "openassessmentblock/oa_submission_answer.html" with answer=student_submission.answer answer_text_label=translated_label %}
{% trans "Your Upload" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url header=translated_header class_prefix="submission__answer" %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=file_urls header=translated_header class_prefix="submission__answer" including_template="grade_complete" xblock_id=xblock_id %}
</article>
<article class="submission__peer-evaluations step__content__section">
......
......@@ -23,7 +23,7 @@
<div class="leaderboard__answer">
{% trans "Your peer's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=topscore.submission.answer answer_text_label=translated_label %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=topscore.file class_prefix="submission__answer"%}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=topscore.files class_prefix="submission__answer" including_template="leaderboard_show" xblock_id=xblock_id %}
</div>
</li>
{% endfor %}
......
......@@ -11,12 +11,14 @@
{{ part.prompt.description|linebreaks }}
</div>
</article>
{% if part.text %}
<div class="submission__answer__part__text">
<h5 class="submission__answer__part__text__title">{{ answer_text_label }}</h5>
<div class="submission__answer__part__text__value">
{{ part.text|linebreaks }}
</div>
</div>
{% endif %}
</li>
{% endfor %}
</ol>
......
......@@ -10,18 +10,30 @@
</header>
{% endif %}
<div class="{{ class_prefix }}__display__file {% if not file_url %}is--hidden{% endif %} submission__{{ file_upload_type }}__upload" data-upload-type="{{ file_upload_type }}">
{% if file_upload_type == "image" %}
<img class="submission__answer__file 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 }}" class="submission__answer__file 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 the site administrator. If you decide to access it, you do so at your own risk.)" %}</p>
<div class="{{ class_prefix }}__display__file {% if not file_urls %}is--hidden{% endif %} submission__{{ file_upload_type }}__upload" data-upload-type="{{ file_upload_type }}">
<div class="submission__answer__files">
{% for file_url, file_description in file_urls %}
<div class="submission__answer__file__block submission__answer__file__block__{{ forloop.counter0 }}">
{% if file_upload_type == "image" %}
{% if file_description %}
<div class="submission__file__description__label" id="file_description_{{ xblock_id }}_{{ including_template }}_{{ forloop.counter0 }}">{{ file_description }}:</div>
{% endif %}
<div><img class="submission__answer__file submission--image" src="{{ file_url }}"
aria-labelledby="file_description_{{ xblock_id }}_{{ including_template }}_{{ forloop.counter0 }}" /></div>
{% elif file_upload_type == "pdf-and-image" or file_upload_type == "custom" %}
<a href="{{ file_url }}" class="submission__answer__file submission--file" target="_blank">
{% if file_description %}
{{ file_description }}
{% else %}
{% trans "View the files associated with this submission:" %} #{{ forloop.counter }}
{% endif %}
</a>
{% endif %}
</div>
{% endfor %}
</div>
{% if show_warning %}
<p class="submission_file_warning">{% trans "Caution: These files were uploaded by another course learner and have not been verified, screened, approved, reviewed, or endorsed by the site administrator. If you access the files, you do so at your own risk.)" %}</p>
{% endif %}
</div>
{% endif %}
......
......@@ -68,8 +68,8 @@
{% trans "Your peer's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=peer_submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=peer_file_url header=translated_header class_prefix="peer-assessment" show_warning="true" %}
{% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=peer_file_urls header=translated_header class_prefix="peer-assessment" show_warning="true" including_template="peer_assessment" xblock_id=xblock_id %}
</div>
<form class="peer-assessment--001__assessment peer-assessment__assessment" method="post">
......
......@@ -51,8 +51,8 @@
{% trans "Your peer's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=peer_submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=peer_file_url header=translated_header class_prefix="peer-assessment" show_warning="true" %}
{% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=peer_file_urls header=translated_header class_prefix="peer-assessment" show_warning="true" including_template="peer_turbo_mode" xblock_id=xblock_id %}
</div>
<form class="peer-assessment--001__assessment peer-assessment__assessment" method="post">
......
......@@ -72,12 +72,16 @@
</article>
{% if text_response %}
<div class="field field--textarea submission__answer__part__text">
<div class="submission__answer__part__text">
<h5 id="submission__answer__part__text__title__{{ forloop.counter }}__{{ xblock_id }}"
class="submission__answer__part__text__title">
{% trans "Your response" %}
{% if text_response == "required" %}
{% trans "Your response (required)" %}
{% elif text_response == "optional" %}
{% trans "Your response (optional)" %}
{% endif %}
</h5>
</div>
<textarea
......@@ -89,65 +93,72 @@
maxlength="100000"
>{{ part.text }}</textarea>
</div>
{% endif %}
</li>
{% endfor %}
{% if text_response %}
<li class="field">
<div class="response__submission__actions">
<div class="message message--inline message--error message--error-server" tabindex="-1">
<h5 class="message__title">{% trans "We could not save your progress" %}</h5>
<div class="message__content"></div>
</div>
<ul class="list list--actions">
<li class="list--actions__item">
<button type="submit" class="action action--save submission__save" aria-describedby="response__save_status__{{ xblock_id }}" disabled>
{% trans "Save your progress" %}
</button>
<div id="response__save_status__{{ xblock_id }}" class="save__submission__label response__submission__label">
<span class="sr">{% trans "Your Submission Status" %}:</span>
{{ save_status }}
</div>
</li>
{% if allow_latex %}
<li class="list--actions__item">
<button type="submit" class="submission__preview action action--save" aria-describedby="response__preview_explanation__{{ xblock_id }}"
{{submit_enabled|yesno:",disabled" }}>
{% trans "Preview in LaTeX"%}
</button>
<div id="response__preview_explanation__{{ xblock_id }}" class="response__submission__label">
{% trans "Click to preview your submission in LaTeX."%}
</div>
</li>
<li class="submission__preview__item list--actions__item">
<article class="submission__answer__display">
<h5 class="submission__answer__display__title">{% trans "Preview Response"%}</h5>
<div class="submission__answer__display__content">
<p class="preview_content"></p>
</div>
</article>
</li>
{% endif %}
</ul>
</div>
</li>
{% endif %}
{% if file_upload_type %}
<li class="field">
<div class="upload__error">
<div class="message message--inline message--error message--error-server" tabindex="-1">
<h5 class="message__title">{% trans "We could not upload this file" %}</h5>
<h5 class="message__title">{% trans "We could not upload files" %}</h5>
<div class="message__content"></div>
</div>
</div>
<label class="sr" for="submission_answer_upload_{{ xblock_id }}">{% trans "Select a file to upload for this submission." %}</label>
<input type="file" class="submission__answer__upload file--upload" id="submission_answer_upload_{{ xblock_id }}">
<button type="submit" class="file__upload action action--upload" disabled>{% trans "Upload your file" %}</button>
<input type="file" class="submission__answer__upload file--upload" id="submission_answer_upload_{{ xblock_id }}" multiple="">
<button type="submit" class="file__upload action action--upload" disabled>{% trans "Upload files" %}</button>
<div class="files__descriptions"></div>
</li>
{% endif %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url class_prefix="submission__answer"%}
<li class="field">
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=file_urls class_prefix="submission__answer" including_template="response" xblock_id=xblock_id %}
</li>
</ol>
<span class="tip" id="submission__answer__tip__{{ xblock_id }}">{% trans "You may continue to work on your response until you submit it." %}</span>
<div class="response__submission__actions">
<div class="message message--inline message--error message--error-server" tabindex="-1">
<h5 class="message__title">{% trans "We could not save your progress" %}</h5>
<div class="message__content"></div>
</div>
<ul class="list list--actions">
<li class="list--actions__item">
<button type="submit" class="action action--save submission__save" aria-describedby="response__save_status__{{ xblock_id }}" disabled>
{% trans "Save your progress" %}
</button>
<div id="response__save_status__{{ xblock_id }}" class="save__submission__label response__submission__label">
<span class="sr">{% trans "Your Submission Status" %}:</span>
{{ save_status }}
</div>
</li>
{% if allow_latex %}
<li class="list--actions__item">
<button type="submit" class="submission__preview action action--save" aria-describedby="response__preview_explanation__{{ xblock_id }}"
{{submit_enabled|yesno:",disabled" }}>
{% trans "Preview in LaTeX"%}
</button>
<div id="response__preview_explanation__{{ xblock_id }}" class="response__submission__label">
{% trans "Click to preview your submission in LaTeX."%}
</div>
</li>
<li class="submission__preview__item list--actions__item">
<article class="submission__answer__display">
<h5 class="submission__answer__display__title">{% trans "Preview Response"%}</h5>
<div class="submission__answer__display__content">
<p class="preview_content"></p>
</div>
</article>
</li>
{% endif %}
</ul>
</div>
</form>
</div>
......@@ -160,6 +171,8 @@
<ul class="list list--actions">
<li class="list--actions__item">
<button type="submit" class="action action--submit step--response__submit"
text_response="{{text_response}}"
file_upload_response="{{file_upload_response}}"
{{submit_enabled|yesno:",disabled" }}>
{% trans "Submit your response and move to the next step" %}
</button>
......
......@@ -28,8 +28,8 @@
{% trans "Your response" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=student_submission.answer answer_text_label=translated_label %}
{% trans "Your Uploaded File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url header=translated_header class_prefix="submission__answer" %}
{% trans "Your Uploaded Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=file_urls header=translated_header class_prefix="submission__answer" including_template="response_graded" xblock_id=xblock_id %}
</article>
</div>
</div>
......
......@@ -49,8 +49,8 @@
{% trans "Your response" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=student_submission.answer answer_text_label=translated_label %}
{% trans "Your Uploaded File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url header=translated_header class_prefix="submission__answer" %}
{% trans "Your Uploaded Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=file_urls header=translated_header class_prefix="submission__answer" including_template="response_submitted" xblock_id=xblock_id %}
</article>
</div>
</div>
......
......@@ -51,8 +51,8 @@
{% trans "Your response" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=self_submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=self_file_url header=translated_header class_prefix="self-assessment" %}
{% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=self_file_urls header=translated_header class_prefix="self-assessment" including_template="self_assessment" xblock_id=xblock_id %}
</article>
<form class="self-assessment--001__assessment self-assessment__assessment" method="post">
......
......@@ -25,8 +25,8 @@
{% trans "The learner's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=staff_file_url header=translated_header class_prefix="staff-assessment" show_warning="true" %}
{% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=staff_file_urls header=translated_header class_prefix="staff-assessment" show_warning="true" including_template="staff_grade_learners_assessment" xblock_id=xblock_id %}
</div>
<form class="staff-assessment__assessment" method="post">
......
......@@ -24,8 +24,8 @@
{% trans "The learner's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=staff_file_url header=translated_header class_prefix="staff-assessment" show_warning="true" %}
{% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=staff_file_urls header=translated_header class_prefix="staff-assessment" show_warning="true" including_template="staff_override_assessment" xblock_id=xblock_id %}
</div>
<form class="staff-assessment__assessment" method="post">
......
......@@ -46,8 +46,8 @@
{% trans "The learner's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=staff_file_url header=translated_header class_prefix="staff-assessment" show_warning="true" %}
{% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=staff_file_urls header=translated_header class_prefix="staff-assessment" show_warning="true" including_template="student_info" xblock_id=xblock_id %}
</div>
{% endif %}
</div>
......
......@@ -408,7 +408,8 @@ def get_assessment_workflow_cancellation(submission_uuid):
workflow_cancellation = AssessmentWorkflowCancellation.get_latest_workflow_cancellation(submission_uuid)
return AssessmentWorkflowCancellationSerializer(workflow_cancellation).data if workflow_cancellation else None
except DatabaseError:
error_message = u"Error finding assessment workflow cancellation for submission UUID {}.".format(submission_uuid)
error_message = u"Error finding assessment workflow cancellation for submission UUID {}."\
.format(submission_uuid)
logger.exception(error_message)
raise PeerAssessmentInternalError(error_message)
......
......@@ -146,7 +146,7 @@ class GradeMixin(object):
),
'file_upload_type': self.file_upload_type,
'allow_latex': self.allow_latex,
'file_url': self.get_download_url_from_submission(student_submission),
'file_urls': self.get_download_urls_from_submission(student_submission),
'xblock_id': self.get_xblock_id()
}
......
......@@ -8,6 +8,7 @@ from submissions import api as sub_api
from openassessment.assessment.errors import SelfAssessmentError, PeerAssessmentError
from openassessment.fileupload import api as file_upload_api
from openassessment.fileupload.exceptions import FileUploadError
from openassessment.xblock.data_conversion import create_submission_dict
......@@ -73,8 +74,18 @@ class LeaderboardMixin(object):
self.leaderboard_show
)
for score in scores:
if 'file_key' in score['content']:
score['file'] = file_upload_api.get_download_url(score['content']['file_key'])
score['files'] = []
if 'file_keys' in score['content']:
for key in score['content']['file_keys']:
url = ''
try:
url = file_upload_api.get_download_url(key)
except FileUploadError:
pass
if url:
score['files'].append(url)
elif 'file_key' in score['content']:
score['files'].append(file_upload_api.get_download_url(score['content']['file_key']))
if 'text' in score['content'] or 'parts' in score['content']:
submission = {'answer': score.pop('content')}
score['submission'] = create_submission_dict(submission, self.prompts)
......
......@@ -126,6 +126,18 @@ class OpenAssessmentBlock(MessageMixin,
help="ISO-8601 formatted string representing the submission due date."
)
text_response = String(
help="Specify whether learners must include a text based response to this problem's prompt.",
default="required",
scope=Scope.settings
)
file_upload_response_raw = String(
help="Specify whether learners are able to upload files as a part of their response.",
default=None,
scope=Scope.settings
)
allow_file_upload = Boolean(
default=None,
scope=Scope.content,
......@@ -216,6 +228,12 @@ class OpenAssessmentBlock(MessageMixin,
help="Saved response submission for the current user."
)
saved_files_descriptions = String(
default=u"",
scope=Scope.user_state,
help="Saved descriptions for each uploaded file."
)
no_peers = Boolean(
default=False,
scope=Scope.user_state,
......@@ -227,6 +245,24 @@ class OpenAssessmentBlock(MessageMixin,
return self._serialize_opaque_key(self.xmodule_runtime.course_id) # pylint:disable=E1101
@property
def file_upload_response(self):
"""
Backward compatibility for existing block before that were created without
'text_response' and 'file_upload_response_raw' fields.
"""
if not self.file_upload_response_raw and (self.file_upload_type_raw is not None or self.allow_file_upload):
return 'optional'
else:
return self.file_upload_response_raw
@file_upload_response.setter
def file_upload_response(self, value):
"""
Setter for file_upload_response_raw
"""
self.file_upload_response_raw = value if value else None
@property
def file_upload_type(self):
"""
Backward compatibility for existing block before the change from allow_file_upload to file_upload_type_raw.
......@@ -652,6 +688,8 @@ class OpenAssessmentBlock(MessageMixin,
block.submission_due = config['submission_due']
block.title = config['title']
block.prompts = config['prompts']
block.text_response = config['text_response']
block.file_upload_response = config['file_upload_response']
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']
......
......@@ -237,7 +237,7 @@ class PeerAssessmentMixin(object):
# Determine if file upload is supported for this XBlock.
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_urls"] = self.get_download_urls_from_submission(peer_sub)
else:
path = 'openassessmentblock/peer/oa_peer_turbo_mode_waiting.html'
elif reason == 'due' and problem_closed:
......@@ -252,7 +252,7 @@ class PeerAssessmentMixin(object):
context_dict["peer_submission"] = create_submission_dict(peer_sub, self.prompts)
# Determine if file upload is supported for this XBlock.
context_dict["file_upload_type"] = self.file_upload_type
context_dict["peer_file_url"] = self.get_download_url_from_submission(peer_sub)
context_dict["peer_file_urls"] = self.get_download_urls_from_submission(peer_sub)
# Sets the XBlock boolean to signal to Message that it WAS NOT able to grab a submission
self.no_peers = False
else:
......
......@@ -56,6 +56,13 @@ def datetime_validator(value):
raise Invalid(u"Could not parse datetime from value \"{val}\"".format(val=value))
NECESSITY_OPTIONS = [
u'required',
u'optional',
u''
]
VALID_ASSESSMENT_TYPES = [
u'peer-assessment',
u'self-assessment',
......@@ -65,7 +72,6 @@ VALID_ASSESSMENT_TYPES = [
]
VALID_UPLOAD_FILE_TYPES = [
u'',
u'image',
u'pdf-and-image',
u'custom'
......@@ -83,11 +89,10 @@ EDITOR_UPDATE_SCHEMA = Schema({
Required('feedback_default_text'): utf8_validator,
Required('submission_start'): Any(datetime_validator, None),
Required('submission_due'): Any(datetime_validator, None),
Required('text_response', default='required'): Any(All(utf8_validator, In(NECESSITY_OPTIONS)), None),
Required('file_upload_response', default=None): Any(All(utf8_validator, In(NECESSITY_OPTIONS)), None),
'allow_file_upload': bool, # Backwards compatibility.
Required('file_upload_type', default=None): Any(
All(utf8_validator, In(VALID_UPLOAD_FILE_TYPES)),
None
),
Required('file_upload_type', default=None): Any(All(utf8_validator, In(VALID_UPLOAD_FILE_TYPES)), None),
'white_listed_file_types': utf8_validator,
Required('allow_latex'): bool,
Required('leaderboard_show'): int,
......
......@@ -105,7 +105,7 @@ class SelfAssessmentMixin(object):
# Determine if file upload is supported for this XBlock and what kind of files can be uploaded.
context["file_upload_type"] = self.file_upload_type
context['self_file_url'] = self.get_download_url_from_submission(submission)
context['self_file_urls'] = self.get_download_urls_from_submission(submission)
path = 'openassessmentblock/self/oa_self_assessment.html'
else:
......
......@@ -331,7 +331,7 @@ class StaffAreaMixin(object):
if submission:
context["file_upload_type"] = self.file_upload_type
context["staff_file_url"] = self.get_download_url_from_submission(submission)
context["staff_file_urls"] = self.get_download_urls_from_submission(submission)
if self.rubric_feedback_prompt is not None:
context["rubric_feedback_prompt"] = self.rubric_feedback_prompt
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -86,6 +86,9 @@
{
"template": "openassessmentblock/response/oa_response.html",
"context": {
"text_response": "required",
"file_upload_response": "optional",
"file_upload_type": "pdf-and-image",
"saved_response": {
"answer": {
"parts": [
......@@ -96,7 +99,8 @@
},
"save_status": "This response has not been saved.",
"submit_enabled": false,
"submission_due": ""
"submission_due": "",
"file_upload_type": "image"
},
"output": "oa_response.html"
},
......@@ -420,7 +424,14 @@
"title": "The most important of all questions.",
"submission_start": "2014-01-02T12:15",
"submission_due": "2014-10-01T04:53",
"text_response": "required",
"file_upload_response": "",
"leaderboard_show": 12,
"necessity_options": {
"required": "Required",
"optional": "Optional",
"": "None"
},
"criteria": [
{
"name": "criterion_1",
......@@ -502,7 +513,14 @@
"title": "Test title",
"submission_start": "2014-01-1T10:00:00",
"submission_due": "2014-10-1T10:00:00",
"text_response": "required",
"file_upload_response": "",
"leaderboard_show": 12,
"necessity_options": {
"required": "Required",
"optional": "Optional",
"": "None"
},
"criteria": [
{
"name": "criterion_with_two_options",
......@@ -1200,6 +1218,8 @@
{
"template": "openassessmentblock/response/oa_response.html",
"context": {
"text_response": "required",
"file_upload_response": "",
"saved_response": {
"answer": {
"parts": [
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -65,14 +65,14 @@ describe("OpenAssessment.ResponseView", function() {
var errorPromise = $.Deferred(function(defer) { defer.rejectWith(this, ["ERROR"]); }).promise();
this.uploadError = false;
this.uploadArgs = null;
this.uploadArgs = [];
this.upload = function(url, data) {
// Store the args we were passed so we can verify them
this.uploadArgs = {
this.uploadArgs.push({
url: url,
data: data
};
});
// Return a promise indicating success or error
return this.uploadError ? errorPromise : successPromise;
......@@ -135,6 +135,17 @@ describe("OpenAssessment.ResponseView", function() {
else { defer.reject(); }
});
});
spyOn(view, 'removeUploadedFiles').and.callFake(function() {
return $.Deferred(function(defer) {
defer.resolve();
});
});
spyOn(view, 'saveFilesDescriptions').and.callFake(function() {
return $.Deferred(function(defer) {
view.removeFilesDescriptions();
defer.resolve();
});
});
});
afterEach(function() {
......@@ -174,6 +185,59 @@ describe("OpenAssessment.ResponseView", function() {
expect(view.saveStatus()).toContain('This response has not been saved.');
});
it("updates submit/save buttons when response text is optional but file upload is required", function() {
view.textResponse = 'optional';
view.fileUploadResponse = 'required';
expect(view.submitEnabled()).toBe(false);
expect(view.saveEnabled()).toBe(false);
var files = [{type: 'application/pdf', size: 1024, name: 'application.pdf', data: ''}];
view.prepareUpload(files, 'pdf-and-image', ['test description']);
view.uploadFiles();
view.checkSubmissionAbility(true);
expect(view.submitEnabled()).toBe(true);
});
it("updates save buttons when response text is optional and input is empty", function() {
view.textResponse = 'optional';
view.fileUploadResponse = 'required';
expect(view.submitEnabled()).toBe(false);
expect(view.saveEnabled()).toBe(false);
view.response(['Test response 1', ' ']);
view.handleResponseChanged();
expect(view.saveEnabled()).toBe(true);
view.save();
expect(view.submitEnabled()).toBe(false);
expect(view.saveEnabled()).toBe(false);
view.response(['', '']);
view.handleResponseChanged();
expect(view.saveEnabled()).toBe(true);
});
it("doesn't allow to push submit button if response text and file upload are both optional and input fields are empty ", function() {
view.textResponse = 'optional';
view.fileUploadResponse = 'optional';
view.response(['', '']);
view.handleResponseChanged();
expect(view.submitEnabled()).toBe(false);
var files = [{type: 'application/pdf', size: 1024, name: 'application.pdf', data: ''}];
view.prepareUpload(files, 'pdf-and-image', ['test description']);
view.uploadFiles();
view.checkSubmissionAbility(true);
expect(view.submitEnabled()).toBe(true);
});
it("updates submit/save buttons and save status when the user saves a response", function() {
// Response is blank --> save/submit button is disabled
view.response('');
......@@ -434,9 +498,10 @@ describe("OpenAssessment.ResponseView", function() {
it("selects too large of a file", function() {
spyOn(view.baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'image/jpeg', size: 6000000, name: 'huge-picture.jpg', data: ''}];
var files = [{type: 'image/jpeg', size: 12000000, name: 'huge-picture.jpg', data: ''}];
view.prepareUpload(files, 'image');
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File size must be 5MB or less.');
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload',
'File size must be 10MB or less.');
});
it("selects the wrong image file type", function() {
......@@ -471,31 +536,62 @@ describe("OpenAssessment.ResponseView", function() {
view.data.FILE_TYPE_WHITE_LIST = ['exe'];
var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}];
view.prepareUpload(files, 'custom');
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File type is not allowed.');
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload',
'File type is not allowed.');
});
it("selects one small and one large file", function() {
spyOn(view.baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'image/jpeg', size: 1024, name: 'small-picture.jpg', data: ''},
{type: 'image/jpeg', size: 11000000, name: 'huge-picture.jpg', data: ''}];
view.prepareUpload(files, 'image');
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload',
'File size must be 10MB or less.');
});
it("selects three files - one with invalid extension", function() {
spyOn(view.baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'image/jpeg', size: 1024, name: 'small-picture-1.jpg', data: ''},
{type: 'application/exe', size: 1024, name: 'application.exe', data: ''},
{type: 'image/jpeg', size: 1024, name: 'small-picture-2.jpg', data: ''}];
view.prepareUpload(files, 'image');
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload',
'You can upload files with these file types: JPG, PNG or GIF');
});
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]);
view.uploadFiles();
expect(fileUploader.uploadArgs[0].url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs[0].data).toEqual(files[0]);
});
it("uploads two images using a one-time URL", function() {
var files = [{type: 'image/jpeg', size: 1024, name: 'picture1.jpg', data: ''},
{type: 'image/jpeg', size: 1024, name: 'picture2.jpg', data: ''}];
view.prepareUpload(files, 'image', ['text1', 'text2']);
view.uploadFiles();
expect(fileUploader.uploadArgs[0].url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs[0].data).toEqual(files[0]);
expect(fileUploader.uploadArgs[1].url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs[1].data).toEqual(files[1]);
});
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]);
view.prepareUpload(files, 'pdf-and-image', ['text']);
view.uploadFiles();
expect(fileUploader.uploadArgs[0].url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs[0].data).toEqual(files[0]);
});
it("uploads a arbitrary type file using a one-time URL", function() {
var files = [{type: 'text/html', size: 1024, name: 'index.html', data: ''}];
view.prepareUpload(files, 'custom');
view.fileUpload();
expect(fileUploader.uploadArgs.url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs.data).toEqual(files[0]);
view.prepareUpload(files, 'custom', ['text']);
view.uploadFiles();
expect(fileUploader.uploadArgs[0].url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs[0].data).toEqual(files[0]);
});
it("displays an error if a one-time file upload URL cannot be retrieved", function() {
......@@ -505,8 +601,8 @@ describe("OpenAssessment.ResponseView", function() {
// Attempt to upload a file
var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files, 'image');
view.fileUpload();
view.prepareUpload(files, 'image', ['text']);
view.uploadFiles();
// Expect an error to be displayed
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'ERROR');
......@@ -520,9 +616,53 @@ describe("OpenAssessment.ResponseView", function() {
// Attempt to upload a file
var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files, 'image');
view.fileUpload();
view.uploadFiles();
// Expect an error to be displayed
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'ERROR');
});
it("disables the upload button if any file description is not set", function() {
function getFileUploadField() {
return $(view.element).find('.file__upload').first();
}
spyOn(view, 'updateFilesDescriptionsFields').and.callThrough();
var files = [{type: 'image/jpeg', size: 1024, name: 'picture1.jpg', data: ''},
{type: 'image/jpeg', size: 1024, name: 'picture2.jpg', data: ''}];
view.prepareUpload(files, 'image');
expect(getFileUploadField().is(':disabled')).toEqual(true);
expect(view.updateFilesDescriptionsFields).toHaveBeenCalledWith(files, undefined, 'image');
// set the first description field (the second is still empty)
// and check that upload button is disabled
var firstDescriptionField1 = $(view.element).find('.file__description__0').first();
$(firstDescriptionField1).val('test');
view.checkFilesDescriptions();
expect(getFileUploadField().is(':disabled')).toEqual(true);
// set the second description field (now both descriptions are not empty)
// and check that upload button is enabled
var firstDescriptionField2 = $(view.element).find('.file__description__1').first();
$(firstDescriptionField2).val('test2');
view.checkFilesDescriptions();
expect(getFileUploadField().is(':disabled')).toEqual(false);
// remove value in the first upload field
// and check that upload button is disabled
$(firstDescriptionField1).val('');
view.checkFilesDescriptions();
expect(getFileUploadField().is(':disabled')).toEqual(true);
});
it("removes description fields after files upload", function() {
var files = [{type: 'image/jpeg', size: 1024, name: 'picture1.jpg', data: ''},
{type: 'image/jpeg', size: 1024, name: 'picture2.jpg', data: ''}];
view.prepareUpload(files, 'image', ['test1', 'test2']);
expect($(view.element).find('.file__description').length).toEqual(2);
view.uploadFiles();
expect($(view.element).find('.file__description').length).toEqual(0);
});
});
......@@ -167,6 +167,34 @@ describe("OpenAssessment.Server", function() {
});
});
it("removes uploaded files", function() {
stubAjax(true, {'success': true, 'msg': ''});
var success = false;
server.removeUploadedFiles().done(function() { success = true; });
expect(success).toBe(true);
expect($.ajax).toHaveBeenCalledWith({
url: "/remove_all_uploaded_files",
type: "POST",
data: JSON.stringify({}),
contentType : jsonContentType
});
});
it("saves files descriptions", function() {
stubAjax(true, {'success': true, 'msg': ''});
var success = false;
server.saveFilesDescriptions(['test1', 'test2']).done(function() { success = true; });
expect(success).toBe(true);
expect($.ajax).toHaveBeenCalledWith({
url: "/save_files_descriptions",
type: "POST",
data: JSON.stringify({descriptions: ['test1', 'test2']}),
contentType : jsonContentType
});
});
it("sends a peer-assessment to the XBlock", function() {
stubAjax(true, {success: true, msg: ''});
......
......@@ -48,7 +48,7 @@ describe("OpenAssessment.StudioView", function() {
feedbackPrompt: "",
submissionStart: "2014-01-02T12:15",
submissionDue: "2014-10-01T04:53",
fileUploadType: "",
fileUploadType: null,
leaderboardNum: 12,
criteria: [
{
......@@ -80,7 +80,7 @@ describe("OpenAssessment.StudioView", function() {
prompt: "Prompt for criterion with no options",
order_num: 1,
options: [],
feedback: "required",
feedback: "required"
},
{
name: "criterion_3",
......@@ -96,7 +96,7 @@ describe("OpenAssessment.StudioView", function() {
label: "Good",
explanation: "Good explanation"
}
],
]
}
],
assessments: [
......@@ -118,7 +118,7 @@ describe("OpenAssessment.StudioView", function() {
"peer-assessment",
"self-assessment",
"example-based-assessment",
"staff-assessment",
"staff-assessment"
]
};
......
......@@ -93,14 +93,19 @@ describe("OpenAssessment.EditSettingsView", function() {
});
it("sets and loads the file upload state", function() {
view.fileUploadResponseNecessity('optional', true);
view.fileUploadType('image');
expect(view.fileUploadType()).toBe('image');
view.fileUploadType('pdf-and-image');
expect(view.fileUploadType()).toBe('pdf-and-image');
view.fileUploadType('custom');
expect(view.fileUploadType()).toBe('custom');
view.fileUploadType('');
view.fileUploadResponseNecessity('', true);
expect(view.fileUploadType()).toBe('');
view.fileUploadResponseNecessity('required', true);
expect(view.fileUploadType()).toBe('custom');
});
it("sets and loads the file type white list", function() {
......@@ -242,6 +247,8 @@ describe("OpenAssessment.EditSettingsView", function() {
});
it("validates file upload type and white list fields", function() {
view.fileUploadResponseNecessity('optional', true);
view.fileUploadType("image");
expect(view.validate()).toBe(true);
expect(view.validationErrors().length).toBe(0);
......
......@@ -17,13 +17,16 @@ OpenAssessment.ResponseView = function(element, server, fileUploader, baseView,
this.fileUploader = fileUploader;
this.baseView = baseView;
this.savedResponse = [];
this.textResponse = 'required';
this.fileUploadResponse = '';
this.files = null;
this.fileType = null;
this.filesDescriptions = [];
this.filesType = null;
this.lastChangeTime = Date.now();
this.errorOnLastSave = false;
this.autoSaveTimerId = null;
this.data = data;
this.fileUploaded = false;
this.filesUploaded = false;
this.announceStatus = false;
this.isRendering = false;
this.dateFactory = new OpenAssessment.DateTimeFactory(this.element);
......@@ -38,8 +41,8 @@ OpenAssessment.ResponseView.prototype = {
// before we can autosave.
AUTO_SAVE_WAIT: 30000,
// Maximum file size (5 MB) for an attached file.
MAX_FILE_SIZE: 5242880,
// Maximum size (10 MB) for all attached files.
MAX_FILES_SIZE: 10485760,
UNSAVED_WARNING_KEY: "learner-response",
......@@ -92,6 +95,10 @@ OpenAssessment.ResponseView.prototype = {
// keep the preview as display none at first
sel.find('.submission__preview__item').hide();
var submit = $('.step--response__submit', this.element);
this.textResponse = $(submit).attr('text_response');
this.fileUploadResponse = $(submit).attr('file_upload_response');
// Install a click handler for submission
sel.find('.step--response__submit').click(
function(eventObject) {
......@@ -130,8 +137,16 @@ OpenAssessment.ResponseView.prototype = {
function(eventObject) {
// Override default form submission
eventObject.preventDefault();
var previouslyUploadedFiles = sel.find('.submission__answer__file').length ? true : false;
$('.submission__answer__display__file', view.element).removeClass('is--hidden');
view.fileUpload();
if (previouslyUploadedFiles) {
var msg = gettext('After you upload new files all your previously uploaded files will be overwritten. Continue?'); // jscs:ignore maximumLineLength
if (confirm(msg)) {
view.uploadFiles();
}
} else {
view.uploadFiles();
}
}
);
},
......@@ -160,6 +175,56 @@ OpenAssessment.ResponseView.prototype = {
},
/**
* Check that "submit" button could be enabled (or disabled)
*
* Args:
* filesFiledIsNotBlank (boolean): used to avoid race conditions situations
* (if files were successfully uploaded and are not displayed yet but
* after upload last file the submit button should be available to push)
*
*/
checkSubmissionAbility: function(filesFiledIsNotBlank) {
var textFieldsIsNotBlank = !this.response().every(function(element) {
return $.trim(element) === '';
});
filesFiledIsNotBlank = filesFiledIsNotBlank || false;
$('.submission__answer__file', this.element).each(function() {
if (($(this).prop("tagName") === 'IMG') && ($(this).attr('src') !== '')) {
filesFiledIsNotBlank = true;
}
if (($(this).prop("tagName") === 'A') && ($(this).attr('href') !== '')) {
filesFiledIsNotBlank = true;
}
});
var readyToSubmit = true;
if ((this.textResponse === 'required') && !textFieldsIsNotBlank) {
readyToSubmit = false;
}
if ((this.fileUploadResponse === 'required') && !filesFiledIsNotBlank) {
readyToSubmit = false;
}
if ((this.textResponse === 'optional') && (this.fileUploadResponse === 'optional') &&
!textFieldsIsNotBlank && !filesFiledIsNotBlank) {
readyToSubmit = false;
}
this.submitEnabled(readyToSubmit);
},
/**
* Check that "save" button could be enabled (or disabled)
*
*/
checkSaveAbility: function() {
var textFieldsIsNotBlank = !this.response().every(function(element) {
return $.trim(element) === '';
});
return !((this.textResponse === 'required') && !textFieldsIsNotBlank);
},
/**
Enable/disable the submit button.
Check that whether the submit button is enabled.
......@@ -293,17 +358,14 @@ OpenAssessment.ResponseView.prototype = {
the user has entered a response.
**/
handleResponseChanged: function() {
// Enable the save/submit button only for non-blank responses
var isNotBlank = !this.response().every(function(element) {
return $.trim(element) === '';
});
this.submitEnabled(isNotBlank);
this.checkSubmissionAbility();
// Update the save button, save status, and "unsaved changes" warning
// only if the response has changed
if (this.responseChanged()) {
this.saveEnabled(isNotBlank);
this.previewEnabled(isNotBlank);
var saveAbility = this.checkSaveAbility();
this.saveEnabled(saveAbility);
this.previewEnabled(saveAbility);
this.saveStatus(gettext('This response has not been saved.'));
this.baseView.unsavedWarningEnabled(
true,
......@@ -340,12 +402,9 @@ OpenAssessment.ResponseView.prototype = {
// ... but update the UI based on what the user may have entered
// since hitting the save button.
var currentResponse = view.response();
var currentResponseIsEmpty = currentResponse.every(function(element) {
return element === '';
});
view.submitEnabled(!currentResponseIsEmpty);
view.checkSubmissionAbility();
var currentResponse = view.response();
var currentResponseEqualsSaved = currentResponse.every(function(element, index) {
return element === savedResponse[index];
});
......@@ -372,15 +431,19 @@ OpenAssessment.ResponseView.prototype = {
submit: function() {
// Immediately disable the submit button to prevent multiple submission
this.submitEnabled(false);
var view = this;
var baseView = this.baseView;
var fileDefer = $.Deferred();
// check if there is a file selected but not uploaded yet
if (view.files !== null && !view.fileUploaded) {
if (view.files !== null && !view.filesUploaded) {
var msg = gettext('Do you want to upload your file before submitting?');
if (confirm(msg)) {
fileDefer = view.fileUpload();
fileDefer = view.uploadFiles();
if (fileDefer === false) {
return;
}
} else {
view.submitEnabled(true);
return;
......@@ -474,71 +537,262 @@ OpenAssessment.ResponseView.prototype = {
file or custom.
**/
prepareUpload: function(files, uploadType) {
prepareUpload: function(files, uploadType, descriptions) {
this.files = null;
this.fileType = files[0].type;
var ext = files[0].name.split('.').pop().toLowerCase();
this.filesType = uploadType;
this.filesUploaded = false;
var totalSize = 0;
var ext = null;
var fileType = null;
var fileName = '';
var errorCheckerTriggered = false;
var sel = $('.step--response', this.element);
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 {
for (var i = 0; i < files.length; i++) {
totalSize += files[i].size;
ext = files[i].name.split('.').pop().toLowerCase();
fileType = files[i].type;
fileName = files[i].name;
if (totalSize > this.MAX_FILES_SIZE) {
this.baseView.toggleActionError(
'upload',
gettext("File size must be 10MB or less.")
);
errorCheckerTriggered = true;
break;
} else if (uploadType === "image" && this.data.ALLOWED_IMAGE_MIME_TYPES.indexOf(fileType) === -1) {
this.baseView.toggleActionError(
'upload',
gettext("You can upload files with these file types: ") + "JPG, PNG or GIF"
);
errorCheckerTriggered = true;
break;
} else if (uploadType === "pdf-and-image" && this.data.ALLOWED_FILE_MIME_TYPES.indexOf(fileType) === -1) {
this.baseView.toggleActionError(
'upload',
gettext("You can upload files with these file types: ") + "JPG, PNG, GIF or PDF"
);
errorCheckerTriggered = true;
break;
} 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(", ")
);
errorCheckerTriggered = true;
break;
} else if (this.data.FILE_EXT_BLACK_LIST.indexOf(ext) !== -1) {
this.baseView.toggleActionError(
'upload',
gettext("File type is not allowed.")
);
errorCheckerTriggered = true;
break;
}
}
if (!errorCheckerTriggered) {
this.baseView.toggleActionError('upload', null);
this.files = files;
this.updateFilesDescriptionsFields(files, descriptions, uploadType);
}
if (this.files === null) {
sel.find('.file__upload').prop('disabled', true);
}
$(".file__upload").prop('disabled', this.files === null);
},
/**
Manages file uploads for submission attachments. Retrieves a one-time
upload URL from the server, and uses it to upload images to a designated
location.
Render textarea fields to input description for each uploaded file.
*/
/* jshint -W083 */
updateFilesDescriptionsFields: function(files, descriptions, uploadType) {
var filesDescriptions = $(this.element).find('.files__descriptions').first();
var mainDiv = null;
var divLabel = null;
var divTextarea = null;
var divImage = null;
var img = null;
var textarea = null;
var descriptionsExists = true;
this.filesDescriptions = descriptions || [];
$(filesDescriptions).show().html('');
for (var i = 0; i < files.length; i++) {
mainDiv = $('<div/>');
divLabel = $('<div/>');
divLabel.addClass('submission__file__description__label');
divLabel.text(gettext("Describe ") + files[i].name + ' ' + gettext("(required):"));
divLabel.appendTo(mainDiv);
divTextarea = $('<div/>');
divTextarea.addClass('submission__file__description');
textarea = $('<textarea />', {
'aria-label': gettext("Describe ") + files[i].name
});
if ((this.filesDescriptions.indexOf(i) !== -1) && (this.filesDescriptions[i] !== '')) {
textarea.val(this.filesDescriptions[i]);
} else {
descriptionsExists = false;
}
textarea.addClass('file__description file__description__' + i);
textarea.appendTo(divTextarea);
if (uploadType === "image") {
img = $('<img/>', {
src: window.URL.createObjectURL(files[i]),
height: 80,
alt: gettext("Thumbnail view of ") + files[i].name
});
img.onload = function() {
window.URL.revokeObjectURL(this.src);
};
divImage = $('<div/>');
divImage.addClass('submission__img__preview');
img.appendTo(divImage);
divImage.appendTo(mainDiv);
}
divTextarea.appendTo(mainDiv);
mainDiv.appendTo(filesDescriptions);
textarea.on("change keyup drop paste", $.proxy(this, "checkFilesDescriptions"));
}
$(this.element).find('.file__upload').prop('disabled', !descriptionsExists);
},
/**
When user type something in some file description field this function check input
and block/unblock "Upload" button
*/
checkFilesDescriptions: function() {
var isError = false;
var filesDescriptions = [];
$(this.element).find('.file__description').each(function() {
var filesDescriptionVal = $(this).val();
if (filesDescriptionVal) {
filesDescriptions.push(filesDescriptionVal);
} else {
isError = true;
}
});
$(this.element).find('.file__upload').prop('disabled', isError);
if (!isError) {
this.filesDescriptions = filesDescriptions;
}
},
/**
Clear field with files descriptions.
*/
removeFilesDescriptions: function() {
var filesDescriptions = $(this.element).find('.files__descriptions').first();
$(filesDescriptions).hide().html('');
},
/**
Remove previously uploaded files.
*/
removeUploadedFiles: function() {
var view = this;
var sel = $('.step--response', this.element);
return this.server.removeUploadedFiles().done(
function() {
var sel = $('.step--response', view.element);
sel.find('.submission__answer__files').html('');
}
).fail(function(errMsg) {
view.baseView.toggleActionError('upload', errMsg);
sel.find('.file__upload').prop('disabled', false);
});
},
/**
Sends request to server to save all file descriptions.
*/
saveFilesDescriptions: function() {
var view = this;
var sel = $('.step--response', this.element);
return this.server.saveFilesDescriptions(this.filesDescriptions).done(
function() {
view.removeFilesDescriptions();
}
).fail(function(errMsg) {
view.baseView.toggleActionError('upload', errMsg);
sel.find('.file__upload').prop('disabled', false);
});
},
/**
Manages file uploads for submission attachments.
**/
fileUpload: function() {
uploadFiles: function() {
var view = this;
var fileUpload = $(".file__upload");
fileUpload.prop('disabled', true);
var promise = null;
var fileCount = view.files.length;
var sel = $('.step--response', this.element);
sel.find('.file__upload').prop('disabled', true);
promise = view.removeUploadedFiles();
promise = promise.then(function() {
return view.saveFilesDescriptions();
});
$.each(view.files, function(index, file) {
promise = promise.then(function() {
return view.fileUpload(view, file.type, file.name, index, file, fileCount === (index + 1));
});
});
return promise;
},
/**
Retrieves a one-time upload URL from the server, and uses it to upload images
to a designated location.
**/
fileUpload: function(view, filetype, filename, filenum, file, finalUpload) {
var sel = $('.step--response', this.element);
var handleError = function(errMsg) {
view.baseView.toggleActionError('upload', errMsg);
fileUpload.prop('disabled', false);
sel.find('.file__upload').prop('disabled', false);
};
// Call getUploadUrl to get the one-time upload URL for this file. Once
// completed, execute a sequential AJAX call to upload to the returned
// URL. This request requires appropriate CORS configuration for AJAX
// PUT requests on the server.
return this.server.getUploadUrl(view.fileType, view.files[0].name).done(
return view.server.getUploadUrl(filetype, filename, filenum).done(
function(url) {
var file = view.files[0];
view.fileUploader.upload(url, file)
.done(function() {
view.fileUrl();
view.fileUrl(filenum);
view.baseView.toggleActionError('upload', null);
view.fileUploaded = true;
if (finalUpload) {
sel.find('input[type=file]').val('');
view.filesUploaded = true;
view.checkSubmissionAbility(true);
}
})
.fail(handleError);
}
......@@ -547,18 +801,56 @@ OpenAssessment.ResponseView.prototype = {
/**
Set the file URL, or retrieve it.
**/
fileUrl: function() {
fileUrl: function(filenum) {
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);
var sel = $('.step--response', this.element);
view.server.getDownloadUrl(filenum).done(function(url) {
var className = 'submission__answer__file__block__' + filenum;
var file = null;
var img = null;
var fileBlock = null;
var fileBlockExists = sel.find("." + className).length ? true : false;
var div1 = null;
var div2 = null;
var ariaLabelledBy = null;
if (!fileBlockExists) {
fileBlock = $('<div/>');
fileBlock.addClass('submission__answer__file__block ' + className);
fileBlock.appendTo(sel.find('.submission__answer__files').first());
}
if (view.filesType === 'image') {
ariaLabelledBy = 'file_description_' + Math.random().toString(36).substr(2, 9);
div1 = $('<div/>', {
id: ariaLabelledBy
});
div1.addClass('submission__file__description__label');
div1.text(view.filesDescriptions[filenum] + ':');
div1.appendTo(fileBlock);
img = $('<img />');
img.addClass('submission__answer__file submission--image');
img.attr('aria-labelledby', ariaLabelledBy);
img.attr('src', url);
div2 = $('<div/>');
div2.html(img);
div2.appendTo(fileBlock);
} else {
file.attr('href', url);
file = $('<a />', {
href: url,
text: view.filesDescriptions[filenum]
});
file.addClass('submission__answer__file submission--file');
file.attr('target', '_blank');
file.appendTo(fileBlock);
}
return url;
});
}
};
......@@ -442,6 +442,8 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
criteria: options.criteria,
assessments: options.assessments,
editor_assessments_order: options.editorAssessmentsOrder,
text_response: options.textResponse,
file_upload_response: options.fileUploadResponse,
file_upload_type: options.fileUploadType,
white_listed_file_types: options.fileTypeWhiteList,
allow_latex: options.latexEnabled,
......@@ -486,17 +488,18 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
*
* @param {string} contentType The Content Type for the file being uploaded.
* @param {string} filename The name of the file to be uploaded.
* @param {string} filenum The number of the file to be uploaded.
* @returns {promise} A promise which resolves with a presigned upload URL from the
* specified service used for uploading files on success, or with an error message
* upon failure.
*/
getUploadUrl: function(contentType, filename) {
getUploadUrl: function(contentType, filename, filenum) {
var url = this.url('upload_url');
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify({contentType: contentType, filename: filename}),
data: JSON.stringify({contentType: contentType, filename: filename, filenum: filenum}),
contentType: jsonContentType
}).done(function(data) {
if (data.success) { defer.resolve(data.url); }
......@@ -508,16 +511,57 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
},
/**
* Sends request to server to remove all uploaded files.
*/
removeUploadedFiles: function() {
var url = this.url('remove_all_uploaded_files');
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify({}),
contentType: jsonContentType
}).done(function(data) {
if (data.success) { defer.resolve(); }
else { defer.rejectWith(this, [data.msg]); }
}).fail(function() {
defer.rejectWith(this, [gettext('Server error.')]);
});
}).promise();
},
/**
* Sends request to server to save descriptions for each uploaded file.
*/
saveFilesDescriptions: function(descriptions) {
var url = this.url('save_files_descriptions');
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify({descriptions: descriptions}),
contentType: jsonContentType
}).done(function(data) {
if (data.success) { defer.resolve(); }
else { defer.rejectWith(this, [data.msg]); }
}).fail(function() {
defer.rejectWith(this, [gettext('Server error.')]);
});
}).promise();
},
/**
* Get a download url used to download related files for the submission.
*
* @param {string} filenum The number of the file to be downloaded.
* @returns {promise} A promise which resolves with a temporary download URL for
* retrieving documents from s3 on success, or with an error message upon failure.
*/
getDownloadUrl: function() {
getDownloadUrl: function(filenum) {
var url = this.url('download_url');
return $.Deferred(function(defer) {
$.ajax({
type: "POST", url: url, data: JSON.stringify({}), contentType: jsonContentType
type: "POST", url: url, data: JSON.stringify({filenum: filenum}), contentType: jsonContentType
}).done(function(data) {
if (data.success) { defer.resolve(data.url); }
else { defer.rejectWith(this, [data.msg]); }
......
......@@ -195,6 +195,8 @@ OpenAssessment.StudioView.prototype = {
this.runtime.notify('save', {state: 'start'});
var view = this;
var fileUploadType = view.settingsView.fileUploadType();
this.server.updateEditorContext({
prompts: view.promptsView.promptsDefinition(),
feedbackPrompt: view.rubricView.feedbackPrompt(),
......@@ -204,7 +206,9 @@ OpenAssessment.StudioView.prototype = {
submissionStart: view.settingsView.submissionStart(),
submissionDue: view.settingsView.submissionDue(),
assessments: view.settingsView.assessmentsDescription(),
fileUploadType: view.settingsView.fileUploadType(),
textResponse: view.settingsView.textResponseNecessity(),
fileUploadResponse: view.settingsView.fileUploadResponseNecessity(),
fileUploadType: fileUploadType !== '' ? fileUploadType : null,
fileTypeWhiteList: view.settingsView.fileTypeWhiteList(),
latexEnabled: view.settingsView.latexEnabled(),
leaderboardNum: view.settingsView.leaderboardNum(),
......
......@@ -323,13 +323,17 @@ OpenAssessment.SelectControl.prototype = {
},
change: function(selected) {
$.each(this.mapping, function(option, sel) {
if (option === selected) {
sel.removeClass('is--hidden');
} else {
sel.addClass('is--hidden');
}
});
if ($.isFunction(this.mapping)) {
this.mapping(selected);
} else {
$.each(this.mapping, function(option, sel) {
if (option === selected) {
sel.removeClass('is--hidden');
} else {
sel.addClass('is--hidden');
}
});
}
}
};
......
......@@ -11,6 +11,7 @@ Returns:
**/
OpenAssessment.EditSettingsView = function(element, assessmentViews, data) {
var self = this;
this.settingsElement = element;
this.assessmentsElement = $(element).siblings('#openassessment_assessment_module_settings_editors').get(0);
this.assessmentViews = assessmentViews;
......@@ -29,6 +30,21 @@ OpenAssessment.EditSettingsView = function(element, assessmentViews, data) {
).install();
new OpenAssessment.SelectControl(
$("#openassessment_submission_file_upload_response", this.element),
function(selectedValue) {
var el = $("#openassessment_submission_file_upload_type_wrapper", self.element);
if (!selectedValue) {
el.addClass('is--hidden');
} else {
el.removeClass('is--hidden');
}
},
new OpenAssessment.Notifier([
new OpenAssessment.AssessmentToggleListener()
])
).install();
new OpenAssessment.SelectControl(
$("#openassessment_submission_upload_selector", this.element),
{'custom': $("#openassessment_submission_white_listed_file_types_wrapper", this.element)},
new OpenAssessment.Notifier([
......@@ -153,6 +169,44 @@ OpenAssessment.EditSettingsView.prototype = {
},
/**
Get or set text response necessity.
Args:
value (string, optional): If provided, set text response necessity.
Returns:
string ('required', 'optional' or '')
*/
textResponseNecessity: function(value) {
var sel = $("#openassessment_submission_text_response", this.settingsElement);
if (value !== undefined) {
sel.val(value);
}
return sel.val();
},
/**
Get or set file upload necessity.
Args:
value (string, optional): If provided, set file upload necessity.
Returns:
string ('required', 'optional' or '')
*/
fileUploadResponseNecessity: function(value, triggerChange) {
var sel = $("#openassessment_submission_file_upload_response", this.settingsElement);
if (value !== undefined) {
triggerChange = triggerChange || false;
sel.val(value);
if (triggerChange) {
$(sel).trigger("change");
}
}
return sel.val();
},
/**
Get or set upload file type.
Args:
......@@ -163,11 +217,17 @@ OpenAssessment.EditSettingsView.prototype = {
**/
fileUploadType: function(uploadType) {
var sel = $("#openassessment_submission_upload_selector", this.settingsElement);
if (uploadType !== undefined) {
sel.val(uploadType);
var fileUploadTypeWrapper = $("#openassessment_submission_file_upload_type_wrapper", this.settingsElement);
var fileUploadAllowed = !$(fileUploadTypeWrapper).hasClass('is--hidden');
if (fileUploadAllowed) {
var sel = $("#openassessment_submission_upload_selector", this.settingsElement);
if (uploadType !== undefined) {
sel.val(uploadType);
}
return sel.val();
}
return sel.val();
return '';
},
/**
......
......@@ -1048,6 +1048,7 @@
@extend %action-2;
@include text-align(center);
@include float(right);
display: inline-block;
margin: ($baseline-v/2) 0;
box-shadow: none;
......
......@@ -554,6 +554,19 @@
}
}
.submission__file__description__label {
margin-bottom: 5px;
}
.submission__answer__file__block {
margin-bottom: 8px;
}
.submission__img__preview {
float: left;
margin-right: 10px;
}
// --------------------
// response
// --------------------
......@@ -573,9 +586,27 @@
@extend %text-sr;
}
textarea {
@extend %ui-content-longanswer;
min-height: ($baseline-v*10);
.files__descriptions {
display: none;
.submission__file__description {
padding-bottom: 10px;
}
}
.submission__answer__part__text {
textarea {
@extend %ui-content-longanswer;
min-height: ($baseline-v*10);
}
}
.submission__file__description {
textarea {
@extend %ui-content-longanswer;
min-height: ($baseline-v*4);
width: 70%;
}
}
.tip {
......
......@@ -10,6 +10,7 @@ from xml import UpdateFromXmlError
from django.conf import settings
from django.template import Context
from django.template.loader import get_template
from django.utils.translation import ugettext as _
from voluptuous import MultipleInvalid
from xblock.core import XBlock
from xblock.fields import List, Scope
......@@ -43,6 +44,12 @@ class StudioMixin(object):
}
]
NECESSITY_OPTIONS = {
"required": _("Required"),
"optional": _("Optional"),
"": _("None")
}
# Since the XBlock problem definition contains only assessment
# modules that are enabled, we need to keep track of the order
# that the user left assessments in the editor, including
......@@ -135,6 +142,9 @@ class StudioMixin(object):
'criteria': criteria,
'feedbackprompt': self.rubric_feedback_prompt,
'feedback_default_text': feedback_default_text,
'text_response': self.text_response if self.text_response else '',
'file_upload_response': self.file_upload_response if self.file_upload_response else '',
'necessity_options': self.NECESSITY_OPTIONS,
'file_upload_type': self.file_upload_type,
'white_listed_file_types': self.white_listed_file_types_string,
'allow_latex': self.allow_latex,
......@@ -186,6 +196,15 @@ class StudioMixin(object):
logger.exception('editor_assessments_order does not contain all expected assessment types')
return {'success': False, 'msg': self._('Error updating XBlock configuration')}
if not data['text_response'] and not data['file_upload_response']:
return {'success': False, 'msg': self._("Error: both text and file upload responses can't be disabled")}
if not data['text_response'] and data['file_upload_response'] == 'optional':
return {'success': False,
'msg': self._("Error: in case if text response is disabled file upload response must be required")}
if not data['file_upload_response'] and data['text_response'] == 'optional':
return {'success': False,
'msg': self._("Error: in case if file upload response is disabled text response must be required")}
# Backwards compatibility: We used to treat "name" as both a user-facing label
# and a unique identifier for criteria and options.
# Now we treat "name" as a unique identifier, and we've added an additional "label"
......@@ -243,8 +262,14 @@ class StudioMixin(object):
self.rubric_feedback_default_text = data['feedback_default_text']
self.submission_start = data['submission_start']
self.submission_due = data['submission_due']
self.file_upload_type = data['file_upload_type']
self.white_listed_file_types_string = data['white_listed_file_types']
self.text_response = data['text_response']
self.file_upload_response = data['file_upload_response']
if data['file_upload_response']:
self.file_upload_type = data['file_upload_type']
self.white_listed_file_types_string = data['white_listed_file_types']
else:
self.file_upload_type = None
self.white_listed_file_types_string = None
self.allow_latex = bool(data['allow_latex'])
self.leaderboard_show = data['leaderboard_show']
......
......@@ -34,6 +34,8 @@ class SubmissionMixin(object):
ALLOWED_FILE_MIME_TYPES = ['application/pdf'] + ALLOWED_IMAGE_MIME_TYPES
MAX_FILES_COUNT = 20
# 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
......@@ -58,9 +60,9 @@ class SubmissionMixin(object):
Args:
data (dict): Data may contain two attributes: submission and
file_url. submission is the response from the student which
should be stored in the Open Assessment system. file_url is the
path to a related file for the submission. file_url is optional.
file_urls. submission is the response from the student which
should be stored in the Open Assessment system. file_urls is the
path to a related file for the submission. file_urls is optional.
suffix (str): Not used in this handler.
Returns:
......@@ -77,7 +79,7 @@ class SubmissionMixin(object):
status = False
student_sub_data = data['submission']
success, msg = validate_submission(student_sub_data, self.prompts, self._)
success, msg = validate_submission(student_sub_data, self.prompts, self._, self.text_response)
if not success:
return (
False,
......@@ -102,9 +104,14 @@ class SubmissionMixin(object):
status_text = self._(u'Multiple submissions are not allowed.')
if not workflow:
try:
try:
saved_files_descriptions = json.loads(self.saved_files_descriptions)
except ValueError:
saved_files_descriptions = None
submission = self.create_submission(
student_item_dict,
student_sub_data
student_sub_data,
saved_files_descriptions
)
except api.SubmissionRequestError as err:
......@@ -154,7 +161,7 @@ class SubmissionMixin(object):
Args:
data (dict): Data should have a single key 'submission' that contains
the text of the student's response. Optionally, the data could
have a 'file_url' key that is the path to an associated file for
have a 'file_urls' key that is the path to an associated file for
this submission.
suffix (str): Not used.
......@@ -163,7 +170,7 @@ class SubmissionMixin(object):
"""
if 'submission' in data:
student_sub_data = data['submission']
success, msg = validate_submission(student_sub_data, self.prompts, self._)
success, msg = validate_submission(student_sub_data, self.prompts, self._, self.text_response)
if not success:
return {'success': False, 'msg': msg}
try:
......@@ -185,14 +192,69 @@ class SubmissionMixin(object):
else:
return {'success': False, 'msg': self._(u"This response was not submitted.")}
def create_submission(self, student_item_dict, student_sub_data):
@XBlock.json_handler
def save_files_descriptions(self, data, suffix=''):
"""
Save the descriptions for each uploaded file.
Args:
data (dict): Data should have a single key 'descriptions' that contains
the texts for each uploaded file.
suffix (str): Not used.
Returns:
dict: Contains a bool 'success' and unicode string 'msg'.
"""
if 'descriptions' in data:
descriptions = data['descriptions']
if isinstance(descriptions, list) and all(map(lambda description: isinstance(description, basestring), descriptions)):
try:
self.saved_files_descriptions = json.dumps(descriptions)
# Emit analytics event...
self.runtime.publish(
self,
"openassessmentblock.save_files_descriptions",
{"saved_response": self.saved_files_descriptions}
)
except:
return {'success': False, 'msg': self._(u"Files descriptions could not be saved.")}
else:
return {'success': True, 'msg': u''}
return {'success': False, 'msg': self._(u"Files descriptions were not submitted.")}
def create_submission(self, student_item_dict, student_sub_data, files_descriptions=None):
# Store the student's response text in a JSON-encodable dict
# so that later we can add additional response fields.
files_descriptions = files_descriptions if files_descriptions else []
student_sub_dict = prepare_submission_for_serialization(student_sub_data)
if self.file_upload_type:
student_sub_dict['file_key'] = self._get_student_item_key()
student_sub_dict['file_keys'] = []
student_sub_dict['files_descriptions'] = []
for i in range(self.MAX_FILES_COUNT):
key_to_save = ''
file_description = ''
item_key = self._get_student_item_key(i)
try:
url = file_upload_api.get_download_url(item_key)
if url:
key_to_save = item_key
try:
file_description = files_descriptions[i]
except IndexError:
pass
except FileUploadError:
pass
if key_to_save:
student_sub_dict['file_keys'].append(key_to_save)
student_sub_dict['files_descriptions'].append(file_description)
else:
break
submission = api.create_submission(student_item_dict, student_sub_dict)
self.create_workflow(submission["uuid"])
self.submission_uuid = submission["uuid"]
......@@ -213,7 +275,7 @@ class SubmissionMixin(object):
return submission
@XBlock.json_handler
def upload_url(self, data, suffix=''):
def upload_url(self, data, suffix=''): # pylint: disable=unused-argument
"""
Request a URL to be used for uploading content related to this
submission.
......@@ -227,6 +289,7 @@ class SubmissionMixin(object):
content_type = data['contentType']
file_name = data['filename']
file_name_parts = file_name.split('.')
file_num = int(data.get('filenum', 0))
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:
......@@ -242,7 +305,7 @@ class SubmissionMixin(object):
if file_ext in self.FILE_EXT_BLACK_LIST:
return {'success': False, 'msg': self._(u"File type is not allowed.")}
try:
key = self._get_student_item_key()
key = self._get_student_item_key(file_num)
url = file_upload_api.get_upload_url(key, content_type)
return {'success': True, 'url': url}
except FileUploadError:
......@@ -250,7 +313,7 @@ class SubmissionMixin(object):
return {'success': False, 'msg': self._(u"Error retrieving upload URL.")}
@XBlock.json_handler
def download_url(self, data, suffix=''):
def download_url(self, data, suffix=''): # pylint: disable=unused-argument
"""
Request a download URL.
......@@ -258,20 +321,36 @@ class SubmissionMixin(object):
A URL to be used for downloading content related to the submission.
"""
return {'success': True, 'url': self._get_download_url()}
file_num = int(data.get('filenum', 0))
return {'success': True, 'url': self._get_download_url(file_num)}
@XBlock.json_handler
def remove_all_uploaded_files(self, data, suffix=''): # pylint: disable=unused-argument
"""
Removes all uploaded user files.
"""
removed_num = 0
for i in range(self.MAX_FILES_COUNT):
removed = file_upload_api.remove_file(self._get_student_item_key(i))
if removed:
removed_num += 1
else:
break
return {'success': True, 'removed_num': removed_num}
def _get_download_url(self):
def _get_download_url(self, file_num=0):
"""
Internal function for retrieving the download url.
"""
try:
return file_upload_api.get_download_url(self._get_student_item_key())
return file_upload_api.get_download_url(self._get_student_item_key(file_num))
except FileUploadError:
logger.exception("Error retrieving download URL.")
return ''
def _get_student_item_key(self):
def _get_student_item_key(self, num=0):
"""
Simple utility method to generate a common file upload key based on
the student item.
......@@ -281,27 +360,23 @@ class SubmissionMixin(object):
"""
student_item_dict = self.get_student_item_dict()
return u"{student_id}/{course_id}/{item_id}".format(
**student_item_dict
)
num = int(num)
if num > 0:
student_item_dict['num'] = num
return u"{student_id}/{course_id}/{item_id}/{num}".format(
**student_item_dict
)
else:
return u"{student_id}/{course_id}/{item_id}".format(
**student_item_dict
)
def get_download_url_from_submission(self, submission):
def _get_url_by_file_key(self, key):
"""
Returns a download URL for retrieving content within a submission.
Args:
submission (dict): Dictionary containing an answer and a file_key.
The file_key is used to try and retrieve a download url
with related content
Returns:
A URL to related content. If there is no content related to this
key, or if there is no key for the submission, returns an empty
string.
Return download url for some particular file key.
"""
url = ""
key = submission['answer'].get('file_key', '')
url = ''
try:
if key:
url = file_upload_api.get_download_url(key)
......@@ -309,6 +384,43 @@ class SubmissionMixin(object):
logger.exception("Unable to generate download url for file key {}".format(key))
return url
def get_download_urls_from_submission(self, submission):
"""
Returns a download URLs for retrieving content within a submission.
Args:
submission (dict): Dictionary containing an answer and a file_keys.
The file_keys is used to try and retrieve a download urls
with related content
Returns:
List with URLs to related content. If there is no content related to this
key, or if there is no key for the submission, returns an empty
list.
"""
urls = []
if 'file_keys' in submission['answer']:
keys = submission['answer'].get('file_keys', [])
descriptions = submission['answer'].get('files_descriptions', [])
for idx, key in enumerate(keys):
url = self._get_url_by_file_key(key)
if url:
description = ''
try:
description = descriptions[idx]
except IndexError:
pass
urls.append((url, description))
else:
break
elif 'file_key' in submission['answer']:
key = submission['answer'].get('file_key', '')
url = self._get_url_by_file_key(key)
if url:
urls.append((url, ''))
return urls
@staticmethod
def get_user_submission(submission_uuid):
"""Return the most recent submission by user in workflow
......@@ -383,7 +495,10 @@ class SubmissionMixin(object):
context = {
'user_timezone': user_preferences['user_timezone'],
'user_language': user_preferences['user_language'],
"xblock_id": self.get_xblock_id()}
"xblock_id": self.get_xblock_id(),
"text_response": self.text_response,
"file_upload_response": self.file_upload_response,
}
# Due dates can default to the distant future, in which case
# there's effectively no due date.
......@@ -394,8 +509,28 @@ class SubmissionMixin(object):
context['file_upload_type'] = self.file_upload_type
context['allow_latex'] = self.allow_latex
file_urls = None
if self.file_upload_type:
context['file_url'] = self._get_download_url()
try:
saved_files_descriptions = json.loads(self.saved_files_descriptions)
except ValueError:
saved_files_descriptions = []
file_urls = []
for i in range(self.MAX_FILES_COUNT):
file_url = self._get_download_url(i)
file_description = ''
if file_url:
try:
file_description = saved_files_descriptions[i]
except IndexError:
pass
file_urls.append((file_url, file_description))
else:
break
context['file_urls'] = file_urls
if self.file_upload_type == 'custom':
context['white_listed_file_types'] = self.white_listed_file_types
......@@ -422,7 +557,16 @@ class SubmissionMixin(object):
context['saved_response'] = create_submission_dict(saved_response, self.prompts)
context['save_status'] = self.save_status
context['submit_enabled'] = self.saved_response != ''
submit_enabled = True
if self.text_response == 'required' and not self.saved_response:
submit_enabled = False
if self.file_upload_response == 'required' and not file_urls:
submit_enabled = False
if self.text_response == 'optional' and self.file_upload_response == 'optional' \
and not self.saved_response and not file_urls:
submit_enabled = False
context['submit_enabled'] = submit_enabled
path = "openassessmentblock/response/oa_response.html"
elif workflow["status"] == "cancelled":
context["workflow_cancellation"] = self.get_workflow_cancellation_info(self.submission_uuid)
......
<openassessment>
<openassessment text_response="required" file_upload_response="">
<title>Open Assessment Test</title>
<prompts>
<prompt>
......
......@@ -4,6 +4,8 @@
"title": "My new title.",
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -29,6 +31,8 @@
"no_prompt": {
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -79,6 +83,8 @@
"no_feedback_prompt": {
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -131,6 +137,8 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -182,6 +190,8 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -235,6 +245,8 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -288,6 +300,8 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -333,6 +347,8 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -377,6 +393,8 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -400,6 +418,8 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -444,6 +464,8 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -474,6 +496,8 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -513,6 +537,8 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -557,6 +583,8 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -600,6 +628,8 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "required",
"file_upload_response": "optional",
"file_upload_type": "invalid",
"allow_latex": false,
"criteria": [
......@@ -642,6 +672,8 @@
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "required",
"file_upload_response": "optional",
"allow_file_upload": null,
"allow_latex": false,
"criteria": [
......@@ -714,6 +746,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -771,6 +805,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -826,6 +862,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -884,6 +922,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -942,6 +982,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -994,6 +1036,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -1053,6 +1097,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -1104,6 +1150,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -1156,6 +1204,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -1184,6 +1234,8 @@
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -1235,6 +1287,8 @@
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -1286,6 +1340,8 @@
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -1360,6 +1416,8 @@
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
......@@ -1406,5 +1464,170 @@
"editor_assessments_order": ["student-training", "peer-assessment", "self-assessment", "example-based-assessment", "staff-assessment"],
"submission_due": "2014-02-27T09:46",
"submission_start": "2014-02-10T09:46"
},
"text_response_none_and_file_upload_response_none": {
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": null,
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
{
"order_num": 0,
"name": "0",
"label": "Test criterion",
"prompt": "Test criterion prompt",
"options": [
{
"order_num": 0,
"points": 0,
"name": "0",
"label": "No",
"explanation": "No explanation"
},
{
"order_num": 1,
"points": 2,
"name": "1",
"label": "Yes",
"explanation": "Yes explanation"
}
],
"feedback": "optional"
}
],
"title": "My new title.",
"assessments": [
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": null,
"due": null
},
{
"name": "self-assessment",
"start": null,
"due": null
}
],
"editor_assessments_order": ["student-training", "peer-assessment", "self-assessment", "staff-assessment"],
"submission_due": "2012-02-27T09:46",
"submission_start": "2012-02-10T09:46",
"expected_error": "error: both text and file upload responses can't be disabled"
},
"text_response_none_and_file_upload_response_optional": {
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": null,
"file_upload_response": "optional",
"file_upload_type": "pdf-and-image",
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
{
"order_num": 0,
"name": "0",
"label": "Test criterion",
"prompt": "Test criterion prompt",
"options": [
{
"order_num": 0,
"points": 0,
"name": "0",
"label": "No",
"explanation": "No explanation"
},
{
"order_num": 1,
"points": 2,
"name": "1",
"label": "Yes",
"explanation": "Yes explanation"
}
],
"feedback": "optional"
}
],
"title": "My new title.",
"assessments": [
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": null,
"due": null
},
{
"name": "self-assessment",
"start": null,
"due": null
}
],
"editor_assessments_order": ["student-training", "peer-assessment", "self-assessment", "staff-assessment"],
"submission_due": "2012-02-27T09:46",
"submission_start": "2012-02-10T09:46",
"expected_error": "error: in case if text response is disabled file upload response must be required"
},
"text_response_optional_and_file_upload_response_none": {
"feedback_prompt": "Feedback prompt",
"feedback_default_text": "Feedback default text",
"prompts": [{"description": "My new prompt 1."}, {"description": "My new prompt 2."}],
"text_response": "optional",
"file_upload_response": null,
"file_upload_type": null,
"allow_latex": false,
"leaderboard_show": 0,
"criteria": [
{
"order_num": 0,
"name": "0",
"label": "Test criterion",
"prompt": "Test criterion prompt",
"options": [
{
"order_num": 0,
"points": 0,
"name": "0",
"label": "No",
"explanation": "No explanation"
},
{
"order_num": 1,
"points": 2,
"name": "1",
"label": "Yes",
"explanation": "Yes explanation"
}
],
"feedback": "optional"
}
],
"title": "My new title.",
"assessments": [
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": null,
"due": null
},
{
"name": "self-assessment",
"start": null,
"due": null
}
],
"editor_assessments_order": ["student-training", "peer-assessment", "self-assessment", "staff-assessment"],
"submission_due": "2012-02-27T09:46",
"submission_start": "2012-02-10T09:46",
"expected_error": "error: in case if file upload response is disabled text response must be required"
}
}
<openassessment>
<openassessment text_response="required" file_upload_response="optional" file_upload_type="pdf-and-image">
<title>Open Assessment Test</title>
<prompts>
<prompt>
......
{
"simple": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -52,7 +54,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<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\" />",
......@@ -79,6 +81,8 @@
"promptless": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
"start": null,
......@@ -122,7 +126,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<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\" />",
......@@ -148,6 +152,8 @@
"empty_prompt": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -192,7 +198,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<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\" />",
......@@ -218,6 +224,8 @@
"multiple_prompts": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "[{\"description\": \"Test prompt 1.\"}, {\"description\": \"Test prompt 2.\"}]",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -262,7 +270,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<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\" />",
......@@ -289,6 +297,8 @@
"unicode": {
"title": "ƒσσ",
"text_response": "required",
"file_upload_response": null,
"prompt": "Ṫëṡẗ ṗṛöṁṗẗ",
"rubric_feedback_prompt": "†es† Feedbåck Prømp†",
"rubric_feedback_default_text": "Ṫëṡẗ ḋëḟäüḷẗ ẗëẍẗ",
......@@ -335,7 +345,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<title>ƒσσ</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\" />",
......@@ -361,6 +371,8 @@
"empty_feedback_prompt": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "",
"rubric_feedback_default_text": "Test default text...",
......@@ -405,7 +417,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<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\" />",
......@@ -431,6 +443,8 @@
"no_feedback_prompt": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": null,
"rubric_feedback_default_text": null,
......@@ -475,7 +489,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<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\" />",
......@@ -499,6 +513,8 @@
"reverse_option_order": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -538,7 +554,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"peer-assessment\" start=\"2014-02-27T09:46:28\" due=\"2014-06-01T00:00:00\" must_grade=\"5\" must_be_graded_by=\"3\" />",
......@@ -563,6 +579,8 @@
"reverse_criteria_order": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -615,7 +633,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"peer-assessment\" start=\"2014-02-27T09:46:28\" due=\"2014-06-01T00:00:00\" must_grade=\"5\" must_be_graded_by=\"3\" />",
......@@ -646,6 +664,8 @@
"default_dates": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -692,7 +712,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"peer-assessment\" due=\"2014-03-01T00:00:00\" must_grade=\"5\" must_be_graded_by=\"3\" />",
......@@ -718,6 +738,8 @@
"set_dates": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -762,7 +784,7 @@
}
],
"expected_xml": [
"<openassessment submission_due=\"2020-04-15T00:00:00\">",
"<openassessment text_response=\"required\" submission_due=\"2020-04-15T00:00:00\">",
"<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\" />",
......@@ -788,6 +810,8 @@
"criterion_feedback_optional": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -833,7 +857,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"peer-assessment\" must_grade=\"5\" must_be_graded_by=\"3\" />",
......@@ -859,6 +883,8 @@
"criterion_feedback_required": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -904,7 +930,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"peer-assessment\" must_grade=\"5\" must_be_graded_by=\"3\" />",
......@@ -930,6 +956,8 @@
"student_training_no_examples": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -968,7 +996,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"student-training\" start=\"2014-02-27T09:46:28\" due=\"2014-03-01T00:00:00\" />",
......@@ -993,6 +1021,8 @@
"student_training_one_example": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -1064,7 +1094,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"student-training\" start=\"2014-02-27T09:46:28\" due=\"2014-03-01T00:00:00\">",
......@@ -1104,6 +1134,8 @@
"student_training_multiple_examples": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -1198,7 +1230,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"student-training\" start=\"2014-02-27T09:46:28\" due=\"2014-03-01T00:00:00\">",
......@@ -1247,6 +1279,8 @@
"ai_peer_self_staff": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -1319,7 +1353,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"example-based-assessment\" algorithm_id=\"sample-algorithm-id\">",
......@@ -1360,6 +1394,8 @@
"file_upload_type_none": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -1400,7 +1436,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<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\" />",
......@@ -1426,6 +1462,8 @@
"file_upload_type_image": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "Test default text...",
......@@ -1466,7 +1504,7 @@
}
],
"expected_xml": [
"<openassessment file_upload_type=\"image\">",
"<openassessment text_response=\"required\" file_upload_type=\"image\">",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"peer-assessment\" start=\"2014-02-27T09:46:28\" due=\"2014-03-01T00:00:00\" must_grade=\"5\" must_be_graded_by=\"3\" />",
......@@ -1492,6 +1530,8 @@
"missing_labels": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"start": null,
......@@ -1535,7 +1575,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<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\" />",
......@@ -1560,6 +1600,8 @@
"empty_feedback_default_text": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": "",
......@@ -1604,7 +1646,7 @@
}
],
"expected_xml": [
"<openassessment file_upload_type=\"image\">",
"<openassessment text_response=\"required\" file_upload_type=\"image\">",
"<title>Foo</title>",
"<assessments>",
"<assessment name=\"peer-assessment\" start=\"2014-02-27T09:46:28\" due=\"2014-03-01T00:00:00\" must_grade=\"5\" must_be_graded_by=\"3\" />",
......@@ -1630,6 +1672,8 @@
"null_feedback_default_text": {
"title": "Foo",
"text_response": "required",
"file_upload_response": null,
"prompt": "Test prompt",
"rubric_feedback_prompt": "Test Feedback Prompt",
"rubric_feedback_default_text": null,
......@@ -1673,7 +1717,7 @@
}
],
"expected_xml": [
"<openassessment>",
"<openassessment text_response=\"required\">",
"<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\" />",
......
<openassessment submission_start="2014-04-01" submission_due="2014-04-05">
<openassessment text_response="required" submission_start="2014-04-01" submission_due="2014-04-05">
<title>Open Assessment Test</title>
<prompts>
<prompt>
......
<openassessment>
<openassessment text_response="required">
<title>Open Assessment Test</title>
<prompts>
<prompt>
......
<openassessment submission_start="2014-04-01" submission_due="2999-05-06">
<openassessment text_response="required" submission_start="2014-04-01" submission_due="2999-05-06">
<title>Open Assessment Test</title>
<prompts>
<prompt>
......
<openassessment submission_start="4999-04-01">
<openassessment text_response="required" submission_start="4999-04-01">
<title>Open Assessment Test</title>
<prompts>
<prompt>
......
<openassessment text_response="" file_upload_response="required" 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>
</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>
</criterion>
</rubric>
<assessments>
<assessment name="peer-assessment" must_grade="1" must_be_graded_by="1" />
<assessment name="self-assessment" />
</assessments>
</openassessment>
<openassessment text_response="optional" file_upload_response="required" 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>
</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>
</criterion>
</rubric>
<assessments>
<assessment name="peer-assessment" must_grade="1" must_be_graded_by="1" />
<assessment name="self-assessment" />
</assessments>
</openassessment>
......@@ -31,6 +31,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false,
......@@ -88,6 +90,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false,
......@@ -141,6 +145,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "ɯʎ uǝʍ ʇıʇןǝ",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false,
......@@ -194,6 +200,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false,
......@@ -258,6 +266,8 @@
"feedback_default_text": "Feedback default text",
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false,
......@@ -311,6 +321,8 @@
"feedback_default_text": "Feedback default text",
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"text_response": "required",
"file_upload_response": "optional",
"file_upload_type": "image",
"white_listed_file_types": null,
"allow_latex": false,
......@@ -364,6 +376,8 @@
"feedback_default_text": "Feedback default text",
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"text_response": "required",
"file_upload_response": "optional",
"file_upload_type": "pdf-and-image",
"white_listed_file_types": null,
"allow_latex": false,
......@@ -417,6 +431,8 @@
"feedback_default_text": "Feedback default text",
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"text_response": "required",
"file_upload_response": "optional",
"file_upload_type": "custom",
"white_listed_file_types": "pdf,doc,docx",
"allow_latex": false,
......
......@@ -401,7 +401,7 @@ class TestPeerAssessmentRender(XBlockHandlerTestCase):
'review_num': 1,
'peer_submission': create_submission_dict(submission, xblock.prompts),
'file_upload_type': None,
'peer_file_url': '',
'peer_file_urls': [],
'submit_button_text': 'submit your assessment & move to response #2',
'allow_latex': False,
'user_timezone': pytz.utc,
......@@ -577,7 +577,7 @@ class TestPeerAssessmentRender(XBlockHandlerTestCase):
'peer_due': dt.datetime(2000, 1, 1).replace(tzinfo=pytz.utc),
'peer_submission': create_submission_dict(submission, xblock.prompts),
'file_upload_type': None,
'peer_file_url': '',
'peer_file_urls': [],
'review_num': 1,
'rubric_criteria': xblock.rubric_criteria,
'submit_button_text': 'Submit your assessment & review another response',
......
# -*- coding: utf-8 -*-
"""
Test that the student can save a files descriptions.
"""
import json
import mock
from .base import XBlockHandlerTestCase, scenario
class SaveFilesDescriptionsTest(XBlockHandlerTestCase):
"""
Group of tests to check ability to save files descriptions
"""
@scenario('data/save_scenario.xml', user_id="Daniels")
def test_save_files_descriptions_blank(self, xblock):
"""
Checks ability to call handler without descriptions.
"""
resp = self.request(xblock, 'save_files_descriptions', json.dumps({}))
self.assertIn('descriptions were not submitted', resp)
@scenario('data/save_scenario.xml', user_id="Perleman")
def test_save_files_descriptions(self, xblock):
"""
Checks ability to call handler with descriptions and then saved texts should be available after xblock render.
"""
# Save the response
descriptions = [u"Ѕраѓтаиѕ! ГоиіБЂт, Щэ ↁіиэ іи Нэll!", u"Ѕраѓтаиѕ! ГоиіБЂт, Щэ ↁіиэ іи Нэll!"]
payload = json.dumps({'descriptions': descriptions})
resp = self.request(xblock, 'save_files_descriptions', payload, response_format="json")
self.assertTrue(resp['success'])
self.assertEqual(resp['msg'], u'')
# Reload the submission UI
xblock._get_download_url = mock.MagicMock(side_effect=lambda i: "https://img-url/%d" % i)
resp = self.request(xblock, 'render_submission', json.dumps({}))
self.assertIn(descriptions[0], resp.decode('utf-8'))
self.assertIn(descriptions[1], resp.decode('utf-8'))
@scenario('data/save_scenario.xml', user_id="Valchek")
def test_overwrite_files_descriptions(self, xblock):
"""
Checks ability to overwrite existed files descriptions.
"""
descriptions1 = [u"Ѕраѓтаиѕ! ГоиіБЂт, Щэ ↁіиэ іи Нэll!", u"Ѕраѓтаиѕ! ГоиіБЂт, Щэ ↁіиэ іи Нэll!"]
payload = json.dumps({'descriptions': descriptions1})
self.request(xblock, 'save_files_descriptions', payload, response_format="json")
descriptions2 = [u"test1", u"test2"]
payload = json.dumps({'descriptions': descriptions2})
self.request(xblock, 'save_files_descriptions', payload, response_format="json")
# Reload the submission UI
xblock._get_download_url = mock.MagicMock(side_effect=lambda i: "https://img-url/%d" % i)
resp = self.request(xblock, 'render_submission', json.dumps({}))
self.assertNotIn(descriptions1[0], resp.decode('utf-8'))
self.assertNotIn(descriptions1[1], resp.decode('utf-8'))
self.assertIn(descriptions2[0], resp.decode('utf-8'))
self.assertIn(descriptions2[1], resp.decode('utf-8'))
......@@ -309,7 +309,7 @@ class TestSelfAssessmentRender(XBlockHandlerTestCase):
'rubric_criteria': xblock.rubric_criteria,
'self_submission': submission,
'file_upload_type': None,
'self_file_url': '',
'self_file_urls': [],
'allow_latex': False,
'user_timezone': pytz.utc,
'user_language': 'en'
......
......@@ -6,7 +6,7 @@ from collections import namedtuple
import json
import datetime
import urllib
from mock import MagicMock, Mock, patch
from mock import MagicMock, Mock, call, patch
from django.test.utils import override_settings
from openassessment.assessment.api import peer as peer_api
......@@ -408,7 +408,8 @@ class TestCourseStaff(XBlockHandlerTestCase):
# Create an image submission for Bob, and corresponding workflow.
self._create_submission(bob_item, {
'text': "Bob Answer",
'file_key': "test_key"
'file_keys': ["test_key"],
'files_descriptions': ["test_description"]
}, ['self'])
# Mock the file upload API to avoid hitting S3
......@@ -423,7 +424,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
file_api.get_download_url.assert_called_with("test_key")
# Check the context passed to the template
self.assertEquals('http://www.example.com/image.jpeg', context['staff_file_url'])
self.assertEquals([('http://www.example.com/image.jpeg', 'test_description')], context['staff_file_urls'])
self.assertEquals('image', context['file_upload_type'])
# Check the fully rendered template
......@@ -432,6 +433,58 @@ class TestCourseStaff(XBlockHandlerTestCase):
self.assertIn("http://www.example.com/image.jpeg", resp)
@scenario('data/self_only_scenario.xml', user_id='Bob')
def test_staff_area_student_info_many_images_submission(self, xblock):
"""
Test multiple file uploads support
"""
# Simulate that we are course staff
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, False, "Bob"
)
xblock.runtime._services['user'] = NullUserService()
bob_item = STUDENT_ITEM.copy()
bob_item["item_id"] = xblock.scope_ids.usage_id
file_keys = ["test_key0", "test_key1", "test_key2"]
files_descriptions = ["test_description0", "test_description1", "test_description2"]
images = ["http://www.example.com/image%d.jpeg" % i for i in range(3)]
file_keys_with_images = dict(zip(file_keys, images))
# Create an image submission for Bob, and corresponding workflow.
self._create_submission(bob_item, {
'text': "Bob Answer",
'file_keys': file_keys,
'files_descriptions': files_descriptions
}, ['self'])
# Mock the file upload API to avoid hitting S3
with patch("openassessment.xblock.submission_mixin.file_upload_api") as file_api:
file_api.get_download_url.return_value = Mock()
file_api.get_download_url.side_effect = lambda file_key: file_keys_with_images[file_key]
# also fake a file_upload_type so our patched url gets rendered
xblock.file_upload_type_raw = 'image'
__, context = xblock.get_student_info_path_and_context("Bob")
# Check that the right file key was passed to generate the download url
calls = [call("test_key%d" % i) for i in range(3)]
file_api.get_download_url.assert_has_calls(calls)
# Check the context passed to the template
self.assertEquals([(image, "test_description%d" % i) for i, image in enumerate(images)],
context['staff_file_urls'])
self.assertEquals('image', context['file_upload_type'])
# Check the fully rendered template
payload = urllib.urlencode({"student_username": "Bob"})
resp = self.request(xblock, "render_student_info", payload)
for i in range(3):
self.assertIn("http://www.example.com/image%d.jpeg" % i, resp)
self.assertIn("test_description%d" % i, resp)
@scenario('data/self_only_scenario.xml', user_id='Bob')
def test_staff_area_student_info_file_download_url_error(self, xblock):
# Simulate that we are course staff
xblock.xmodule_runtime = self._create_mock_runtime(
......@@ -445,7 +498,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
# Create an image submission for Bob, and corresponding workflow.
self._create_submission(bob_item, {
'text': "Bob Answer",
'file_key': "test_key"
'file_keys': ["test_key"]
}, ['self'])
# Mock the file upload API to simulate an error
......
......@@ -18,6 +18,8 @@ class StudioViewTest(XBlockHandlerTestCase):
"""
UPDATE_EDITOR_DATA = {
"title": "Test title",
"text_response": "required",
"file_upload_response": None,
"prompts": [{"description": "Test prompt"}],
"feedback_prompt": "Test feedback prompt",
"feedback_default_text": "Test feedback default text",
......
......@@ -90,6 +90,31 @@ class SubmissionTest(XBlockHandlerTestCase):
self.assertEqual(resp[1], "ENOPREVIEW")
self.assertIsNot(resp[2], None)
def _ability_to_submit_blank_answer(self, xblock):
"""
Checks ability to submit blank answer if text response is not required
"""
empty_submission = json.dumps({"submission": [""]})
resp = self.request(xblock, 'submit', empty_submission, response_format='json')
self.assertTrue(resp[0])
@scenario('data/text_response_optional.xml', user_id='Bob')
def test_ability_to_submit_blank_answer_if_text_response_optional(self, xblock):
"""
Checks ability to submit blank answer if text response is optional
"""
self._ability_to_submit_blank_answer(xblock)
@scenario('data/text_response_none.xml', user_id='Bob')
def test_ability_to_submit_blank_answer_if_text_response_none(self, xblock):
"""
Checks ability to submit blank answer if text response is None
"""
self._ability_to_submit_blank_answer(xblock)
@scenario('data/over_grade_scenario.xml', user_id='Alice')
def test_closed_submissions(self, xblock):
resp = self.request(xblock, 'render_submission', json.dumps(dict()))
......@@ -163,6 +188,39 @@ class SubmissionTest(XBlockHandlerTestCase):
self.assertTrue(resp['success'])
self.assertEqual(u'', 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_remove_all_uploaded_files(self, xblock):
""" Test remove all user files """
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?")
xblock.xmodule_runtime = Mock(
course_id='test_course',
anonymous_student_id='test_student',
)
download_url = api.get_download_url("test_student/test_course/" + xblock.scope_ids.usage_id)
resp = self.request(xblock, 'download_url', json.dumps(dict()), response_format='json')
self.assertTrue(resp['success'])
self.assertEqual(download_url, resp['url'])
resp = self.request(xblock, 'remove_all_uploaded_files', json.dumps(dict()), response_format='json')
self.assertTrue(resp['success'])
self.assertEqual(resp['removed_num'], 1)
resp = self.request(xblock, 'download_url', json.dumps(dict()), response_format='json')
self.assertTrue(resp['success'])
self.assertEqual(u'', resp['url'])
class SubmissionRenderTest(XBlockHandlerTestCase):
"""
......@@ -179,6 +237,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response_unavailable.html',
{
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'submission_start': dt.datetime(4999, 4, 1).replace(tzinfo=pytz.utc),
'allow_latex': False,
......@@ -202,6 +262,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
xblock, 'openassessmentblock/response/oa_response_submitted.html',
{
'student_submission': create_submission_dict(submission, xblock.prompts),
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'peer_incomplete': True,
'self_incomplete': True,
......@@ -216,6 +278,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response.html',
{
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'saved_response': create_submission_dict({
'answer': prepare_submission_for_serialization(
......@@ -236,6 +300,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response.html',
{
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'saved_response': create_submission_dict({
'answer': prepare_submission_for_serialization(
......@@ -260,6 +326,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response.html',
{
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'saved_response': create_submission_dict({
'answer': prepare_submission_for_serialization(
......@@ -285,6 +353,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response.html',
{
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'saved_response': create_submission_dict({
'answer': prepare_submission_for_serialization(
......@@ -311,6 +381,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
{
'submission_due': dt.datetime(2999, 5, 6).replace(tzinfo=pytz.utc),
'student_submission': create_submission_dict(submission, xblock.prompts),
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'peer_incomplete': True,
'self_incomplete': True,
......@@ -338,6 +410,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response_cancelled.html',
{
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'allow_latex': False,
'submission_due': dt.datetime(2999, 5, 6).replace(tzinfo=pytz.utc),
......@@ -371,6 +445,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
'student_submission': {"answer": {"parts": [
{"prompt": {'description': 'One prompt.'}, "text": "An old format response."}
]}},
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'peer_incomplete': True,
'self_incomplete': True,
......@@ -385,6 +461,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response_closed.html',
{
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'submission_due': dt.datetime(2014, 4, 5).replace(tzinfo=pytz.utc),
'allow_latex': False,
......@@ -404,6 +482,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
{
'submission_due': dt.datetime(2014, 4, 5).replace(tzinfo=pytz.utc),
'student_submission': create_submission_dict(submission, xblock.prompts),
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'peer_incomplete': False,
'self_incomplete': True,
......@@ -432,6 +512,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
{
'submission_due': dt.datetime(2999, 5, 6).replace(tzinfo=pytz.utc),
'student_submission': create_submission_dict(submission, xblock.prompts),
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'allow_latex': False,
'user_timezone': None,
......@@ -458,6 +540,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
{
'submission_due': dt.datetime(2014, 4, 5).replace(tzinfo=pytz.utc),
'student_submission': create_submission_dict(submission, xblock.prompts),
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'allow_latex': False,
'user_timezone': None,
......
......@@ -115,6 +115,8 @@ class TestSerializeContent(TestCase):
def _configure_xblock(self, data):
self.oa_block.title = data.get('title', '')
self.oa_block.text_response = data.get('text_response', '')
self.oa_block.file_upload_response = data.get('file_upload_response', None)
self.oa_block.prompt = data.get('prompt')
self.oa_block.prompts = create_prompts_list(data.get('prompt'))
self.oa_block.rubric_feedback_prompt = data.get('rubric_feedback_prompt')
......
......@@ -378,7 +378,7 @@ def validator(oa_block, _, strict_post_release=True):
return _inner
def validate_submission(submission, prompts, _):
def validate_submission(submission, prompts, _, text_response='required'):
"""
Validate submission dict.
......@@ -398,7 +398,7 @@ def validate_submission(submission, prompts, _):
if type(submission) != list:
return False, message
if len(submission) != len(prompts):
if text_response == 'required' and len(submission) != len(prompts):
return False, message
for submission_part in submission:
......
......@@ -700,6 +700,14 @@ def serialize_content_to_xml(oa_block, root):
if oa_block.leaderboard_show:
root.set('leaderboard_show', unicode(oa_block.leaderboard_show))
# Set text response
if oa_block.text_response:
root.set('text_response', unicode(oa_block.text_response))
# Set file upload response
if oa_block.file_upload_response:
root.set('file_upload_response', unicode(oa_block.file_upload_response))
# Set File upload settings
if oa_block.file_upload_type:
root.set('file_upload_type', unicode(oa_block.file_upload_type))
......@@ -833,6 +841,14 @@ def parse_from_xml(root):
if 'submission_due' in root.attrib:
submission_due = parse_date(unicode(root.attrib['submission_due']), name="submission due date")
text_response = None
if 'text_response' in root.attrib:
text_response = unicode(root.attrib['text_response'])
file_upload_response = None
if 'file_upload_response' in root.attrib:
file_upload_response = unicode(root.attrib['file_upload_response'])
allow_file_upload = None
if 'allow_file_upload' in root.attrib:
allow_file_upload = _parse_boolean(unicode(root.attrib['allow_file_upload']))
......@@ -890,6 +906,8 @@ def parse_from_xml(root):
'rubric_feedback_default_text': rubric['feedback_default_text'],
'submission_start': submission_start,
'submission_due': submission_due,
'text_response': text_response,
'file_upload_response': file_upload_response,
'allow_file_upload': allow_file_upload,
'file_upload_type': file_upload_type,
'white_listed_file_types': white_listed_file_types,
......
......@@ -158,6 +158,38 @@ class SubmissionPage(OpenAssessmentPage):
self.wait_for_element_visibility(".submission__answer__upload", "File select button is present")
self.q(css=".submission__answer__upload").results[0].send_keys(file_path_name)
def add_file_description(self, file_num, description):
"""
Submit a description for some file.
Args:
file_num (integer): file number
description (string): file description
"""
textarea_element = self._bounded_selector("textarea.file__description__%d" % file_num)
self.wait_for_element_visibility(textarea_element, "Textarea is present")
self.q(css=textarea_element).fill(description)
@property
def upload_file_button_is_enabled(self):
"""
Check if 'Upload files' button is enabled
Returns:
bool
"""
return self.q(css="button.file__upload").attrs('disabled') == ['false']
@property
def upload_file_button_is_disabled(self):
"""
Check if 'Upload files' button is disabled
Returns:
bool
"""
return self.q(css="button.file__upload").attrs('disabled') == ['true']
def upload_file(self):
"""
Upload the selected file
......@@ -196,14 +228,15 @@ class SubmissionPage(OpenAssessmentPage):
return self.q(css="div.upload__error > div.message--error").visible
@property
def has_file_uploaded(self):
def have_files_uploaded(self):
"""
Check whether file is successfully uploaded
Check whether files were successfully uploaded
Returns:
bool
"""
return self.q(css=".submission__custom__upload").visible
self.wait_for_element_visibility('.submission__custom__upload', 'Uploaded files block is presented')
return self.q(css=".submission__answer__files").visible
class AssessmentMixin(object):
......
......@@ -741,10 +741,22 @@ class FileUploadTest(OpenAssessmentTest):
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')
readme1 = os.path.dirname(os.path.realpath(__file__)) + '/README.rst'
readme2 = readme1.replace('test/acceptance/', '') # There's another README located at ../../
files = ', '.join([readme1, readme2])
self.submission_page.visit().select_file(files)
self.assertFalse(self.submission_page.has_file_error)
self.assertTrue(self.submission_page.upload_file_button_is_disabled)
self.submission_page.add_file_description(0, 'file description 1')
self.assertTrue(self.submission_page.upload_file_button_is_disabled)
self.submission_page.add_file_description(1, 'file description 2')
self.assertTrue(self.submission_page.upload_file_button_is_enabled)
self.submission_page.upload_file()
self.assertTrue(self.submission_page.has_file_uploaded)
self.assertTrue(self.submission_page.have_files_uploaded)
class FullWorkflowMixin(object):
......
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