Commit 7568a700 by Dmitry Viskov

Multiple file uploads support

Usability improvement (Text/File uploads Response: Required/Optional/None)
parent 88915a6b
...@@ -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.
...@@ -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);
......
...@@ -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']
......
<openassessment> <openassessment text_response="required" file_upload_response="">
<title>Open Assessment Test</title> <title>Open Assessment Test</title>
<prompts> <prompts>
<prompt> <prompt>
......
<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>
......
<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