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