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(
Args:
submission_uuid (str): The unique identifier for the submission being assessed.
user_id (str): The ID of the user creating the assessment. This must match the ID of the user who made the submission.
user_id (str): The ID of the user creating the assessment.
This must match the ID of the user who made the submission.
options_selected (dict): Mapping of rubric criterion names to option values selected.
criterion_feedback (dict): Dictionary mapping criterion names to the
free-form text feedback the user gave for the criterion.
......
......@@ -18,3 +18,10 @@ def get_download_url(key):
Returns the url at which the file that corresponds to the key can be downloaded.
"""
return backends.get_backend().get_download_url(key)
def remove_file(key):
"""
Remove file from the storage
"""
return backends.get_backend().remove_file(key)
......@@ -90,6 +90,19 @@ class BaseBackend(object):
"""
raise NotImplementedError
@abc.abstractmethod
def remove_file(self, key):
"""
Remove file from the storage
Args:
key (str): A unique identifier used to identify the data requested for remove.
Returns:
True if file was successfully removed or False is file was not removed or was not was not found.
"""
raise NotImplementedError
def _retrieve_parameters(self, key):
"""
Simple utility function to validate settings and arguments before compiling
......
from .base import BaseBackend
from .. import exceptions
......@@ -42,6 +41,10 @@ class Backend(BaseBackend):
make_download_url_available(self._get_key_name(key), self.DOWNLOAD_URL_TIMEOUT)
return self._get_url(key)
def remove_file(self, key):
from openassessment.fileupload.views_filesystem import safe_remove, get_file_path
return safe_remove(get_file_path(self._get_key_name(key)))
def _get_url(self, key):
key_name = self._get_key_name(key)
url = reverse("openassessment-filesystem-storage", kwargs={'key': key_name})
......
......@@ -28,7 +28,6 @@ class Backend(BaseBackend):
)
raise FileUploadInternalError(ex)
def get_download_url(self, key):
bucket_name, key_name = self._retrieve_parameters(key)
try:
......@@ -42,6 +41,18 @@ class Backend(BaseBackend):
)
raise FileUploadInternalError(ex)
def remove_file(self, key):
bucket_name, key_name = self._retrieve_parameters(key)
conn = _connect_to_s3()
bucket = conn.get_bucket(bucket_name)
s3_key = bucket.get_key(key_name)
if s3_key:
bucket.delete_key(s3_key)
return True
else:
return False
def _connect_to_s3():
"""Connect to s3
......
......@@ -63,6 +63,24 @@ class Backend(BaseBackend):
)
raise FileUploadInternalError(ex)
def remove_file(self, key):
bucket_name, key_name = self._retrieve_parameters(key)
key, url = get_settings()
try:
temp_url = swiftclient.utils.generate_temp_url(
path='%s/%s/%s' % (url.path, bucket_name, key_name),
key=key,
method='DELETE',
seconds=self.DOWNLOAD_URL_TIMEOUT)
remove_url = '%s://%s%s' % (url.scheme, url.netloc, temp_url)
response = requests.delete(remove_url)
return response.status_code == 204
except Exception as ex:
logger.exception(
u"An internal exception occurred while removing object on swift storage."
)
raise FileUploadInternalError(ex)
def get_settings():
"""
......
......@@ -57,6 +57,23 @@ class TestFileUploadService(TestCase):
downloadUrl = api.get_download_url("foo")
self.assertIn("https://mybucket.s3.amazonaws.com/submissions_attachments/foo", downloadUrl)
@mock_s3
@override_settings(
AWS_ACCESS_KEY_ID='foobar',
AWS_SECRET_ACCESS_KEY='bizbaz',
FILE_UPLOAD_STORAGE_BUCKET_NAME="mybucket"
)
def test_remove_file(self):
conn = boto.connect_s3()
bucket = conn.create_bucket('mybucket')
key = Key(bucket)
key.key = "submissions_attachments/foo"
key.set_contents_from_string("Test")
result = api.remove_file("foo")
self.assertTrue(result)
result = api.remove_file("foo")
self.assertFalse(result)
@raises(exceptions.FileUploadInternalError)
def test_get_upload_url_no_bucket(self):
api.get_upload_url("foo", "bar")
......@@ -280,6 +297,15 @@ class TestFileUploadServiceWithFilesystemBackend(TestCase):
self.assertEqual(200, upload_response.status_code)
self.assertEqual(200, download_response.status_code)
def test_remove_file(self):
self.set_key(u"noël.jpg")
upload_url = self.backend.get_upload_url(self.key, self.content_type)
self.client.put(upload_url, data=self.content.read(), content_type=self.content_type)
result = self.backend.remove_file(self.key)
self.assertTrue(result)
result = self.backend.remove_file(self.key)
self.assertFalse(result)
@override_settings(
ORA2_FILEUPLOAD_BACKEND='swift',
......
......@@ -114,6 +114,8 @@ def safe_remove(path):
"""
if os.path.exists(path):
os.remove(path)
return True
return False
def get_file_path(key):
......
......@@ -101,18 +101,42 @@
</div>
<p class="setting-help">{% trans "The date and time when learners can no longer submit responses." %}</p>
</li>
<li id="openassessment_submission_text_response_wrapper" class="field comp-setting-entry">
<div class="wrapper-comp-setting">
<label for="openassessment_submission_text_response" class="setting-label">{% trans "Text Response"%}</label>
<select id="openassessment_submission_text_response" class="input setting-input" name="text response">
{% for option_key, option_name in necessity_options.items %}
<option value="{{ option_key }}" {% if option_key == text_response %} selected="true" {% endif %}>{{ option_name }}</option>
{% endfor %}
</select>
</div>
<p class="setting-help">
{% trans "Specify whether learners must include a text based response to this problem's prompt." %}
</p>
</li>
<li id="openassessment_submission_file_wrapper" class="field comp-setting-entry">
<div class="wrapper-comp-setting">
<label for="openassessment_submission_upload_selector" class="setting-label">{% trans "Allow File Upload"%}</label>
<label for="openassessment_submission_file_upload_response" class="setting-label">{% trans "File Uploads Response"%}</label>
<select id="openassessment_submission_file_upload_response" class="input setting-input" name="text response">
{% for option_key, option_name in necessity_options.items %}
<option value="{{ option_key }}" {% if option_key == file_upload_response %} selected="true" {% endif %}>{{ option_name }}</option>
{% endfor %}
</select>
</div>
<p class="setting-help">
{% trans "Specify whether learners are able to upload files as a part of their response." %}
</p>
<div id="openassessment_submission_file_upload_type_wrapper" class="{% if not file_upload_response %}is--hidden{% endif %}">
<div class="wrapper-comp-setting">
<label for="openassessment_submission_upload_selector" class="setting-label">{% trans "File Upload Types"%}</label>
<select id="openassessment_submission_upload_selector" class="input setting-input" name="upload submission">
<option value="">{% trans "None"%}</option>
<option value="image" {% if file_upload_type == "image" %} selected="true" {% endif %}>{% trans "Image File"%}</option>
<option value="pdf-and-image" {% if file_upload_type == "pdf-and-image" %} selected="true" {% endif %}>{% trans "PDF or Image File"%}</option>
<option value="pdf-and-image" {% if file_upload_type == "pdf-and-image" %} selected="true" {% endif %}>{% trans "PDF or Image Files"%}</option>
<option value="image" {% if file_upload_type == "image" %} selected="true" {% endif %}>{% trans "Image Files"%}</option>
<option value="custom" {% if file_upload_type == "custom" %} selected="true" {% endif %}>{% trans "Custom File Types"%}</option>
</select>
</div>
<p class="setting-help">
{% trans "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 can submit files along with their text responses. Select Images to allow JPG, GIF, or PNG files. Select PDF or Images to allow PDF files and images. Select Custom File Types to allow files with extensions that you specify below. (Use the Select Custom File Types option with caution.)" %}
</p>
<div id="openassessment_submission_white_listed_file_types_wrapper" class="{% if file_upload_type != "custom" %}is--hidden{% endif %}">
<div class="wrapper-comp-setting">
......@@ -128,6 +152,7 @@
</p>&nbsp;
<p class="setting-help message-status error"></p>
</div>
</div>
</li>
<li id="openassessment_submission_latex_wrapper" class="field comp-setting-entry">
<div class="wrapper-comp-setting">
......
......@@ -33,7 +33,7 @@
{% include "openassessmentblock/oa_submission_answer.html" with answer=student_submission.answer answer_text_label=translated_label %}
{% trans "Your Upload" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url header=translated_header class_prefix="submission__answer" %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=file_urls header=translated_header class_prefix="submission__answer" including_template="grade_complete" xblock_id=xblock_id %}
</article>
<article class="submission__peer-evaluations step__content__section">
......
......@@ -23,7 +23,7 @@
<div class="leaderboard__answer">
{% trans "Your peer's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=topscore.submission.answer answer_text_label=translated_label %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=topscore.file class_prefix="submission__answer"%}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=topscore.files class_prefix="submission__answer" including_template="leaderboard_show" xblock_id=xblock_id %}
</div>
</li>
{% endfor %}
......
......@@ -11,12 +11,14 @@
{{ part.prompt.description|linebreaks }}
</div>
</article>
{% if part.text %}
<div class="submission__answer__part__text">
<h5 class="submission__answer__part__text__title">{{ answer_text_label }}</h5>
<div class="submission__answer__part__text__value">
{{ part.text|linebreaks }}
</div>
</div>
{% endif %}
</li>
{% endfor %}
</ol>
......
......@@ -10,18 +10,30 @@
</header>
{% endif %}
<div class="{{ class_prefix }}__display__file {% if not file_url %}is--hidden{% endif %} submission__{{ file_upload_type }}__upload" data-upload-type="{{ file_upload_type }}">
<div class="{{ class_prefix }}__display__file {% if not file_urls %}is--hidden{% endif %} submission__{{ file_upload_type }}__upload" data-upload-type="{{ file_upload_type }}">
<div class="submission__answer__files">
{% for file_url, file_description in file_urls %}
<div class="submission__answer__file__block submission__answer__file__block__{{ forloop.counter0 }}">
{% if file_upload_type == "image" %}
<img class="submission__answer__file submission--image"
alt="{% trans "The image associated with this submission." %}"
src="{{ file_url }}" />
{% if file_description %}
<div class="submission__file__description__label" id="file_description_{{ xblock_id }}_{{ including_template }}_{{ forloop.counter0 }}">{{ file_description }}:</div>
{% endif %}
<div><img class="submission__answer__file submission--image" src="{{ file_url }}"
aria-labelledby="file_description_{{ xblock_id }}_{{ including_template }}_{{ forloop.counter0 }}" /></div>
{% elif file_upload_type == "pdf-and-image" or file_upload_type == "custom" %}
<a href="{{ file_url }}" class="submission__answer__file submission--file" target="_blank">
{% trans "View the file associated with this submission." %}
{% if file_description %}
{{ file_description }}
{% else %}
{% trans "View the files associated with this submission:" %} #{{ forloop.counter }}
{% endif %}
</a>
{% if show_warning %}
<p class="submission_file_warning">{% trans "(Caution: This file was uploaded by another course learner and has not been verified, screened, approved, reviewed, or endorsed by the site administrator. If you decide to access it, you do so at your own risk.)" %}</p>
{% endif %}
</div>
{% endfor %}
</div>
{% if show_warning %}
<p class="submission_file_warning">{% trans "Caution: These files were uploaded by another course learner and have not been verified, screened, approved, reviewed, or endorsed by the site administrator. If you access the files, you do so at your own risk.)" %}</p>
{% endif %}
</div>
{% endif %}
......
......@@ -68,8 +68,8 @@
{% trans "Your peer's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=peer_submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=peer_file_url header=translated_header class_prefix="peer-assessment" show_warning="true" %}
{% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=peer_file_urls header=translated_header class_prefix="peer-assessment" show_warning="true" including_template="peer_assessment" xblock_id=xblock_id %}
</div>
<form class="peer-assessment--001__assessment peer-assessment__assessment" method="post">
......
......@@ -51,8 +51,8 @@
{% trans "Your peer's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=peer_submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=peer_file_url header=translated_header class_prefix="peer-assessment" show_warning="true" %}
{% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=peer_file_urls header=translated_header class_prefix="peer-assessment" show_warning="true" including_template="peer_turbo_mode" xblock_id=xblock_id %}
</div>
<form class="peer-assessment--001__assessment peer-assessment__assessment" method="post">
......
......@@ -72,12 +72,16 @@
</article>
{% if text_response %}
<div class="field field--textarea submission__answer__part__text">
<div class="submission__answer__part__text">
<h5 id="submission__answer__part__text__title__{{ forloop.counter }}__{{ xblock_id }}"
class="submission__answer__part__text__title">
{% trans "Your response" %}
{% if text_response == "required" %}
{% trans "Your response (required)" %}
{% elif text_response == "optional" %}
{% trans "Your response (optional)" %}
{% endif %}
</h5>
</div>
<textarea
......@@ -89,27 +93,11 @@
maxlength="100000"
>{{ part.text }}</textarea>
</div>
{% endif %}
</li>
{% endfor %}
{% if file_upload_type %}
{% if text_response %}
<li class="field">
<div class="upload__error">
<div class="message message--inline message--error message--error-server" tabindex="-1">
<h5 class="message__title">{% trans "We could not upload this file" %}</h5>
<div class="message__content"></div>
</div>
</div>
<label class="sr" for="submission_answer_upload_{{ xblock_id }}">{% trans "Select a file to upload for this submission." %}</label>
<input type="file" class="submission__answer__upload file--upload" id="submission_answer_upload_{{ xblock_id }}">
<button type="submit" class="file__upload action action--upload" disabled>{% trans "Upload your file" %}</button>
</li>
{% endif %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url class_prefix="submission__answer"%}
</ol>
<span class="tip" 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>
......@@ -148,6 +136,29 @@
{% endif %}
</ul>
</div>
</li>
{% endif %}
{% if file_upload_type %}
<li class="field">
<div class="upload__error">
<div class="message message--inline message--error message--error-server" tabindex="-1">
<h5 class="message__title">{% trans "We could not upload files" %}</h5>
<div class="message__content"></div>
</div>
</div>
<label class="sr" for="submission_answer_upload_{{ xblock_id }}">{% trans "Select a file to upload for this submission." %}</label>
<input type="file" class="submission__answer__upload file--upload" id="submission_answer_upload_{{ xblock_id }}" multiple="">
<button type="submit" class="file__upload action action--upload" disabled>{% trans "Upload files" %}</button>
<div class="files__descriptions"></div>
</li>
{% endif %}
<li class="field">
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=file_urls class_prefix="submission__answer" including_template="response" xblock_id=xblock_id %}
</li>
</ol>
<span class="tip" id="submission__answer__tip__{{ xblock_id }}">{% trans "You may continue to work on your response until you submit it." %}</span>
</form>
</div>
......@@ -160,6 +171,8 @@
<ul class="list list--actions">
<li class="list--actions__item">
<button type="submit" class="action action--submit step--response__submit"
text_response="{{text_response}}"
file_upload_response="{{file_upload_response}}"
{{submit_enabled|yesno:",disabled" }}>
{% trans "Submit your response and move to the next step" %}
</button>
......
......@@ -28,8 +28,8 @@
{% trans "Your response" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=student_submission.answer answer_text_label=translated_label %}
{% trans "Your Uploaded File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url header=translated_header class_prefix="submission__answer" %}
{% trans "Your Uploaded Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=file_urls header=translated_header class_prefix="submission__answer" including_template="response_graded" xblock_id=xblock_id %}
</article>
</div>
</div>
......
......@@ -49,8 +49,8 @@
{% trans "Your response" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=student_submission.answer answer_text_label=translated_label %}
{% trans "Your Uploaded File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url header=translated_header class_prefix="submission__answer" %}
{% trans "Your Uploaded Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=file_urls header=translated_header class_prefix="submission__answer" including_template="response_submitted" xblock_id=xblock_id %}
</article>
</div>
</div>
......
......@@ -51,8 +51,8 @@
{% trans "Your response" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=self_submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=self_file_url header=translated_header class_prefix="self-assessment" %}
{% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=self_file_urls header=translated_header class_prefix="self-assessment" including_template="self_assessment" xblock_id=xblock_id %}
</article>
<form class="self-assessment--001__assessment self-assessment__assessment" method="post">
......
......@@ -25,8 +25,8 @@
{% trans "The learner's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=staff_file_url header=translated_header class_prefix="staff-assessment" show_warning="true" %}
{% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=staff_file_urls header=translated_header class_prefix="staff-assessment" show_warning="true" including_template="staff_grade_learners_assessment" xblock_id=xblock_id %}
</div>
<form class="staff-assessment__assessment" method="post">
......
......@@ -24,8 +24,8 @@
{% trans "The learner's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=staff_file_url header=translated_header class_prefix="staff-assessment" show_warning="true" %}
{% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=staff_file_urls header=translated_header class_prefix="staff-assessment" show_warning="true" including_template="staff_override_assessment" xblock_id=xblock_id %}
</div>
<form class="staff-assessment__assessment" method="post">
......
......@@ -46,8 +46,8 @@
{% trans "The learner's response to the question above" as translated_label %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label=translated_label %}
{% trans "Associated File" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=staff_file_url header=translated_header class_prefix="staff-assessment" show_warning="true" %}
{% trans "Associated Files" as translated_header %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=staff_file_urls header=translated_header class_prefix="staff-assessment" show_warning="true" including_template="student_info" xblock_id=xblock_id %}
</div>
{% endif %}
</div>
......
......@@ -408,7 +408,8 @@ def get_assessment_workflow_cancellation(submission_uuid):
workflow_cancellation = AssessmentWorkflowCancellation.get_latest_workflow_cancellation(submission_uuid)
return AssessmentWorkflowCancellationSerializer(workflow_cancellation).data if workflow_cancellation else None
except DatabaseError:
error_message = u"Error finding assessment workflow cancellation for submission UUID {}.".format(submission_uuid)
error_message = u"Error finding assessment workflow cancellation for submission UUID {}."\
.format(submission_uuid)
logger.exception(error_message)
raise PeerAssessmentInternalError(error_message)
......
......@@ -146,7 +146,7 @@ class GradeMixin(object):
),
'file_upload_type': self.file_upload_type,
'allow_latex': self.allow_latex,
'file_url': self.get_download_url_from_submission(student_submission),
'file_urls': self.get_download_urls_from_submission(student_submission),
'xblock_id': self.get_xblock_id()
}
......
......@@ -8,6 +8,7 @@ from submissions import api as sub_api
from openassessment.assessment.errors import SelfAssessmentError, PeerAssessmentError
from openassessment.fileupload import api as file_upload_api
from openassessment.fileupload.exceptions import FileUploadError
from openassessment.xblock.data_conversion import create_submission_dict
......@@ -73,8 +74,18 @@ class LeaderboardMixin(object):
self.leaderboard_show
)
for score in scores:
if 'file_key' in score['content']:
score['file'] = file_upload_api.get_download_url(score['content']['file_key'])
score['files'] = []
if 'file_keys' in score['content']:
for key in score['content']['file_keys']:
url = ''
try:
url = file_upload_api.get_download_url(key)
except FileUploadError:
pass
if url:
score['files'].append(url)
elif 'file_key' in score['content']:
score['files'].append(file_upload_api.get_download_url(score['content']['file_key']))
if 'text' in score['content'] or 'parts' in score['content']:
submission = {'answer': score.pop('content')}
score['submission'] = create_submission_dict(submission, self.prompts)
......
......@@ -126,6 +126,18 @@ class OpenAssessmentBlock(MessageMixin,
help="ISO-8601 formatted string representing the submission due date."
)
text_response = String(
help="Specify whether learners must include a text based response to this problem's prompt.",
default="required",
scope=Scope.settings
)
file_upload_response_raw = String(
help="Specify whether learners are able to upload files as a part of their response.",
default=None,
scope=Scope.settings
)
allow_file_upload = Boolean(
default=None,
scope=Scope.content,
......@@ -216,6 +228,12 @@ class OpenAssessmentBlock(MessageMixin,
help="Saved response submission for the current user."
)
saved_files_descriptions = String(
default=u"",
scope=Scope.user_state,
help="Saved descriptions for each uploaded file."
)
no_peers = Boolean(
default=False,
scope=Scope.user_state,
......@@ -227,6 +245,24 @@ class OpenAssessmentBlock(MessageMixin,
return self._serialize_opaque_key(self.xmodule_runtime.course_id) # pylint:disable=E1101
@property
def file_upload_response(self):
"""
Backward compatibility for existing block before that were created without
'text_response' and 'file_upload_response_raw' fields.
"""
if not self.file_upload_response_raw and (self.file_upload_type_raw is not None or self.allow_file_upload):
return 'optional'
else:
return self.file_upload_response_raw
@file_upload_response.setter
def file_upload_response(self, value):
"""
Setter for file_upload_response_raw
"""
self.file_upload_response_raw = value if value else None
@property
def file_upload_type(self):
"""
Backward compatibility for existing block before the change from allow_file_upload to file_upload_type_raw.
......@@ -652,6 +688,8 @@ class OpenAssessmentBlock(MessageMixin,
block.submission_due = config['submission_due']
block.title = config['title']
block.prompts = config['prompts']
block.text_response = config['text_response']
block.file_upload_response = config['file_upload_response']
block.allow_file_upload = config['allow_file_upload']
block.file_upload_type = config['file_upload_type']
block.white_listed_file_types_string = config['white_listed_file_types']
......
......@@ -237,7 +237,7 @@ class PeerAssessmentMixin(object):
# Determine if file upload is supported for this XBlock.
context_dict["file_upload_type"] = self.file_upload_type
context_dict["peer_file_url"] = self.get_download_url_from_submission(peer_sub)
context_dict["peer_file_urls"] = self.get_download_urls_from_submission(peer_sub)
else:
path = 'openassessmentblock/peer/oa_peer_turbo_mode_waiting.html'
elif reason == 'due' and problem_closed:
......@@ -252,7 +252,7 @@ class PeerAssessmentMixin(object):
context_dict["peer_submission"] = create_submission_dict(peer_sub, self.prompts)
# Determine if file upload is supported for this XBlock.
context_dict["file_upload_type"] = self.file_upload_type
context_dict["peer_file_url"] = self.get_download_url_from_submission(peer_sub)
context_dict["peer_file_urls"] = self.get_download_urls_from_submission(peer_sub)
# Sets the XBlock boolean to signal to Message that it WAS NOT able to grab a submission
self.no_peers = False
else:
......
......@@ -56,6 +56,13 @@ def datetime_validator(value):
raise Invalid(u"Could not parse datetime from value \"{val}\"".format(val=value))
NECESSITY_OPTIONS = [
u'required',
u'optional',
u''
]
VALID_ASSESSMENT_TYPES = [
u'peer-assessment',
u'self-assessment',
......@@ -65,7 +72,6 @@ VALID_ASSESSMENT_TYPES = [
]
VALID_UPLOAD_FILE_TYPES = [
u'',
u'image',
u'pdf-and-image',
u'custom'
......@@ -83,11 +89,10 @@ EDITOR_UPDATE_SCHEMA = Schema({
Required('feedback_default_text'): utf8_validator,
Required('submission_start'): Any(datetime_validator, None),
Required('submission_due'): Any(datetime_validator, None),
Required('text_response', default='required'): Any(All(utf8_validator, In(NECESSITY_OPTIONS)), None),
Required('file_upload_response', default=None): Any(All(utf8_validator, In(NECESSITY_OPTIONS)), None),
'allow_file_upload': bool, # Backwards compatibility.
Required('file_upload_type', default=None): Any(
All(utf8_validator, In(VALID_UPLOAD_FILE_TYPES)),
None
),
Required('file_upload_type', default=None): Any(All(utf8_validator, In(VALID_UPLOAD_FILE_TYPES)), None),
'white_listed_file_types': utf8_validator,
Required('allow_latex'): bool,
Required('leaderboard_show'): int,
......
......@@ -105,7 +105,7 @@ class SelfAssessmentMixin(object):
# Determine if file upload is supported for this XBlock and what kind of files can be uploaded.
context["file_upload_type"] = self.file_upload_type
context['self_file_url'] = self.get_download_url_from_submission(submission)
context['self_file_urls'] = self.get_download_urls_from_submission(submission)
path = 'openassessmentblock/self/oa_self_assessment.html'
else:
......
......@@ -331,7 +331,7 @@ class StaffAreaMixin(object):
if submission:
context["file_upload_type"] = self.file_upload_type
context["staff_file_url"] = self.get_download_url_from_submission(submission)
context["staff_file_urls"] = self.get_download_urls_from_submission(submission)
if self.rubric_feedback_prompt is not None:
context["rubric_feedback_prompt"] = self.rubric_feedback_prompt
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -86,6 +86,9 @@
{
"template": "openassessmentblock/response/oa_response.html",
"context": {
"text_response": "required",
"file_upload_response": "optional",
"file_upload_type": "pdf-and-image",
"saved_response": {
"answer": {
"parts": [
......@@ -96,7 +99,8 @@
},
"save_status": "This response has not been saved.",
"submit_enabled": false,
"submission_due": ""
"submission_due": "",
"file_upload_type": "image"
},
"output": "oa_response.html"
},
......@@ -420,7 +424,14 @@
"title": "The most important of all questions.",
"submission_start": "2014-01-02T12:15",
"submission_due": "2014-10-01T04:53",
"text_response": "required",
"file_upload_response": "",
"leaderboard_show": 12,
"necessity_options": {
"required": "Required",
"optional": "Optional",
"": "None"
},
"criteria": [
{
"name": "criterion_1",
......@@ -502,7 +513,14 @@
"title": "Test title",
"submission_start": "2014-01-1T10:00:00",
"submission_due": "2014-10-1T10:00:00",
"text_response": "required",
"file_upload_response": "",
"leaderboard_show": 12,
"necessity_options": {
"required": "Required",
"optional": "Optional",
"": "None"
},
"criteria": [
{
"name": "criterion_with_two_options",
......@@ -1200,6 +1218,8 @@
{
"template": "openassessmentblock/response/oa_response.html",
"context": {
"text_response": "required",
"file_upload_response": "",
"saved_response": {
"answer": {
"parts": [
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -167,6 +167,34 @@ describe("OpenAssessment.Server", function() {
});
});
it("removes uploaded files", function() {
stubAjax(true, {'success': true, 'msg': ''});
var success = false;
server.removeUploadedFiles().done(function() { success = true; });
expect(success).toBe(true);
expect($.ajax).toHaveBeenCalledWith({
url: "/remove_all_uploaded_files",
type: "POST",
data: JSON.stringify({}),
contentType : jsonContentType
});
});
it("saves files descriptions", function() {
stubAjax(true, {'success': true, 'msg': ''});
var success = false;
server.saveFilesDescriptions(['test1', 'test2']).done(function() { success = true; });
expect(success).toBe(true);
expect($.ajax).toHaveBeenCalledWith({
url: "/save_files_descriptions",
type: "POST",
data: JSON.stringify({descriptions: ['test1', 'test2']}),
contentType : jsonContentType
});
});
it("sends a peer-assessment to the XBlock", function() {
stubAjax(true, {success: true, msg: ''});
......
......@@ -48,7 +48,7 @@ describe("OpenAssessment.StudioView", function() {
feedbackPrompt: "",
submissionStart: "2014-01-02T12:15",
submissionDue: "2014-10-01T04:53",
fileUploadType: "",
fileUploadType: null,
leaderboardNum: 12,
criteria: [
{
......@@ -80,7 +80,7 @@ describe("OpenAssessment.StudioView", function() {
prompt: "Prompt for criterion with no options",
order_num: 1,
options: [],
feedback: "required",
feedback: "required"
},
{
name: "criterion_3",
......@@ -96,7 +96,7 @@ describe("OpenAssessment.StudioView", function() {
label: "Good",
explanation: "Good explanation"
}
],
]
}
],
assessments: [
......@@ -118,7 +118,7 @@ describe("OpenAssessment.StudioView", function() {
"peer-assessment",
"self-assessment",
"example-based-assessment",
"staff-assessment",
"staff-assessment"
]
};
......
......@@ -93,14 +93,19 @@ describe("OpenAssessment.EditSettingsView", function() {
});
it("sets and loads the file upload state", function() {
view.fileUploadResponseNecessity('optional', true);
view.fileUploadType('image');
expect(view.fileUploadType()).toBe('image');
view.fileUploadType('pdf-and-image');
expect(view.fileUploadType()).toBe('pdf-and-image');
view.fileUploadType('custom');
expect(view.fileUploadType()).toBe('custom');
view.fileUploadType('');
view.fileUploadResponseNecessity('', true);
expect(view.fileUploadType()).toBe('');
view.fileUploadResponseNecessity('required', true);
expect(view.fileUploadType()).toBe('custom');
});
it("sets and loads the file type white list", function() {
......@@ -242,6 +247,8 @@ describe("OpenAssessment.EditSettingsView", function() {
});
it("validates file upload type and white list fields", function() {
view.fileUploadResponseNecessity('optional', true);
view.fileUploadType("image");
expect(view.validate()).toBe(true);
expect(view.validationErrors().length).toBe(0);
......
......@@ -442,6 +442,8 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
criteria: options.criteria,
assessments: options.assessments,
editor_assessments_order: options.editorAssessmentsOrder,
text_response: options.textResponse,
file_upload_response: options.fileUploadResponse,
file_upload_type: options.fileUploadType,
white_listed_file_types: options.fileTypeWhiteList,
allow_latex: options.latexEnabled,
......@@ -486,17 +488,18 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
*
* @param {string} contentType The Content Type for the file being uploaded.
* @param {string} filename The name of the file to be uploaded.
* @param {string} filenum The number of the file to be uploaded.
* @returns {promise} A promise which resolves with a presigned upload URL from the
* specified service used for uploading files on success, or with an error message
* upon failure.
*/
getUploadUrl: function(contentType, filename) {
getUploadUrl: function(contentType, filename, filenum) {
var url = this.url('upload_url');
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify({contentType: contentType, filename: filename}),
data: JSON.stringify({contentType: contentType, filename: filename, filenum: filenum}),
contentType: jsonContentType
}).done(function(data) {
if (data.success) { defer.resolve(data.url); }
......@@ -508,16 +511,57 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
},
/**
* Sends request to server to remove all uploaded files.
*/
removeUploadedFiles: function() {
var url = this.url('remove_all_uploaded_files');
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify({}),
contentType: jsonContentType
}).done(function(data) {
if (data.success) { defer.resolve(); }
else { defer.rejectWith(this, [data.msg]); }
}).fail(function() {
defer.rejectWith(this, [gettext('Server error.')]);
});
}).promise();
},
/**
* Sends request to server to save descriptions for each uploaded file.
*/
saveFilesDescriptions: function(descriptions) {
var url = this.url('save_files_descriptions');
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify({descriptions: descriptions}),
contentType: jsonContentType
}).done(function(data) {
if (data.success) { defer.resolve(); }
else { defer.rejectWith(this, [data.msg]); }
}).fail(function() {
defer.rejectWith(this, [gettext('Server error.')]);
});
}).promise();
},
/**
* Get a download url used to download related files for the submission.
*
* @param {string} filenum The number of the file to be downloaded.
* @returns {promise} A promise which resolves with a temporary download URL for
* retrieving documents from s3 on success, or with an error message upon failure.
*/
getDownloadUrl: function() {
getDownloadUrl: function(filenum) {
var url = this.url('download_url');
return $.Deferred(function(defer) {
$.ajax({
type: "POST", url: url, data: JSON.stringify({}), contentType: jsonContentType
type: "POST", url: url, data: JSON.stringify({filenum: filenum}), contentType: jsonContentType
}).done(function(data) {
if (data.success) { defer.resolve(data.url); }
else { defer.rejectWith(this, [data.msg]); }
......
......@@ -195,6 +195,8 @@ OpenAssessment.StudioView.prototype = {
this.runtime.notify('save', {state: 'start'});
var view = this;
var fileUploadType = view.settingsView.fileUploadType();
this.server.updateEditorContext({
prompts: view.promptsView.promptsDefinition(),
feedbackPrompt: view.rubricView.feedbackPrompt(),
......@@ -204,7 +206,9 @@ OpenAssessment.StudioView.prototype = {
submissionStart: view.settingsView.submissionStart(),
submissionDue: view.settingsView.submissionDue(),
assessments: view.settingsView.assessmentsDescription(),
fileUploadType: view.settingsView.fileUploadType(),
textResponse: view.settingsView.textResponseNecessity(),
fileUploadResponse: view.settingsView.fileUploadResponseNecessity(),
fileUploadType: fileUploadType !== '' ? fileUploadType : null,
fileTypeWhiteList: view.settingsView.fileTypeWhiteList(),
latexEnabled: view.settingsView.latexEnabled(),
leaderboardNum: view.settingsView.leaderboardNum(),
......
......@@ -323,6 +323,9 @@ OpenAssessment.SelectControl.prototype = {
},
change: function(selected) {
if ($.isFunction(this.mapping)) {
this.mapping(selected);
} else {
$.each(this.mapping, function(option, sel) {
if (option === selected) {
sel.removeClass('is--hidden');
......@@ -331,6 +334,7 @@ OpenAssessment.SelectControl.prototype = {
}
});
}
}
};
/**
......
......@@ -11,6 +11,7 @@ Returns:
**/
OpenAssessment.EditSettingsView = function(element, assessmentViews, data) {
var self = this;
this.settingsElement = element;
this.assessmentsElement = $(element).siblings('#openassessment_assessment_module_settings_editors').get(0);
this.assessmentViews = assessmentViews;
......@@ -29,6 +30,21 @@ OpenAssessment.EditSettingsView = function(element, assessmentViews, data) {
).install();
new OpenAssessment.SelectControl(
$("#openassessment_submission_file_upload_response", this.element),
function(selectedValue) {
var el = $("#openassessment_submission_file_upload_type_wrapper", self.element);
if (!selectedValue) {
el.addClass('is--hidden');
} else {
el.removeClass('is--hidden');
}
},
new OpenAssessment.Notifier([
new OpenAssessment.AssessmentToggleListener()
])
).install();
new OpenAssessment.SelectControl(
$("#openassessment_submission_upload_selector", this.element),
{'custom': $("#openassessment_submission_white_listed_file_types_wrapper", this.element)},
new OpenAssessment.Notifier([
......@@ -153,6 +169,44 @@ OpenAssessment.EditSettingsView.prototype = {
},
/**
Get or set text response necessity.
Args:
value (string, optional): If provided, set text response necessity.
Returns:
string ('required', 'optional' or '')
*/
textResponseNecessity: function(value) {
var sel = $("#openassessment_submission_text_response", this.settingsElement);
if (value !== undefined) {
sel.val(value);
}
return sel.val();
},
/**
Get or set file upload necessity.
Args:
value (string, optional): If provided, set file upload necessity.
Returns:
string ('required', 'optional' or '')
*/
fileUploadResponseNecessity: function(value, triggerChange) {
var sel = $("#openassessment_submission_file_upload_response", this.settingsElement);
if (value !== undefined) {
triggerChange = triggerChange || false;
sel.val(value);
if (triggerChange) {
$(sel).trigger("change");
}
}
return sel.val();
},
/**
Get or set upload file type.
Args:
......@@ -163,11 +217,17 @@ OpenAssessment.EditSettingsView.prototype = {
**/
fileUploadType: function(uploadType) {
var fileUploadTypeWrapper = $("#openassessment_submission_file_upload_type_wrapper", this.settingsElement);
var fileUploadAllowed = !$(fileUploadTypeWrapper).hasClass('is--hidden');
if (fileUploadAllowed) {
var sel = $("#openassessment_submission_upload_selector", this.settingsElement);
if (uploadType !== undefined) {
sel.val(uploadType);
}
return sel.val();
}
return '';
},
/**
......
......@@ -1048,6 +1048,7 @@
@extend %action-2;
@include text-align(center);
@include float(right);
display: inline-block;
margin: ($baseline-v/2) 0;
box-shadow: none;
......
......@@ -554,6 +554,19 @@
}
}
.submission__file__description__label {
margin-bottom: 5px;
}
.submission__answer__file__block {
margin-bottom: 8px;
}
.submission__img__preview {
float: left;
margin-right: 10px;
}
// --------------------
// response
// --------------------
......@@ -573,10 +586,28 @@
@extend %text-sr;
}
.files__descriptions {
display: none;
.submission__file__description {
padding-bottom: 10px;
}
}
.submission__answer__part__text {
textarea {
@extend %ui-content-longanswer;
min-height: ($baseline-v*10);
}
}
.submission__file__description {
textarea {
@extend %ui-content-longanswer;
min-height: ($baseline-v*4);
width: 70%;
}
}
.tip {
@extend %t-score;
......
......@@ -10,6 +10,7 @@ from xml import UpdateFromXmlError
from django.conf import settings
from django.template import Context
from django.template.loader import get_template
from django.utils.translation import ugettext as _
from voluptuous import MultipleInvalid
from xblock.core import XBlock
from xblock.fields import List, Scope
......@@ -43,6 +44,12 @@ class StudioMixin(object):
}
]
NECESSITY_OPTIONS = {
"required": _("Required"),
"optional": _("Optional"),
"": _("None")
}
# Since the XBlock problem definition contains only assessment
# modules that are enabled, we need to keep track of the order
# that the user left assessments in the editor, including
......@@ -135,6 +142,9 @@ class StudioMixin(object):
'criteria': criteria,
'feedbackprompt': self.rubric_feedback_prompt,
'feedback_default_text': feedback_default_text,
'text_response': self.text_response if self.text_response else '',
'file_upload_response': self.file_upload_response if self.file_upload_response else '',
'necessity_options': self.NECESSITY_OPTIONS,
'file_upload_type': self.file_upload_type,
'white_listed_file_types': self.white_listed_file_types_string,
'allow_latex': self.allow_latex,
......@@ -186,6 +196,15 @@ class StudioMixin(object):
logger.exception('editor_assessments_order does not contain all expected assessment types')
return {'success': False, 'msg': self._('Error updating XBlock configuration')}
if not data['text_response'] and not data['file_upload_response']:
return {'success': False, 'msg': self._("Error: both text and file upload responses can't be disabled")}
if not data['text_response'] and data['file_upload_response'] == 'optional':
return {'success': False,
'msg': self._("Error: in case if text response is disabled file upload response must be required")}
if not data['file_upload_response'] and data['text_response'] == 'optional':
return {'success': False,
'msg': self._("Error: in case if file upload response is disabled text response must be required")}
# Backwards compatibility: We used to treat "name" as both a user-facing label
# and a unique identifier for criteria and options.
# Now we treat "name" as a unique identifier, and we've added an additional "label"
......@@ -243,8 +262,14 @@ class StudioMixin(object):
self.rubric_feedback_default_text = data['feedback_default_text']
self.submission_start = data['submission_start']
self.submission_due = data['submission_due']
self.text_response = data['text_response']
self.file_upload_response = data['file_upload_response']
if data['file_upload_response']:
self.file_upload_type = data['file_upload_type']
self.white_listed_file_types_string = data['white_listed_file_types']
else:
self.file_upload_type = None
self.white_listed_file_types_string = None
self.allow_latex = bool(data['allow_latex'])
self.leaderboard_show = data['leaderboard_show']
......
<openassessment>
<openassessment text_response="required" file_upload_response="">
<title>Open Assessment Test</title>
<prompts>
<prompt>
......
<openassessment>
<openassessment text_response="required" file_upload_response="optional" file_upload_type="pdf-and-image">
<title>Open Assessment Test</title>
<prompts>
<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>
<prompts>
<prompt>
......
<openassessment>
<openassessment text_response="required">
<title>Open Assessment Test</title>
<prompts>
<prompt>
......
<openassessment submission_start="2014-04-01" submission_due="2999-05-06">
<openassessment text_response="required" submission_start="2014-04-01" submission_due="2999-05-06">
<title>Open Assessment Test</title>
<prompts>
<prompt>
......
<openassessment submission_start="4999-04-01">
<openassessment text_response="required" submission_start="4999-04-01">
<title>Open Assessment Test</title>
<prompts>
<prompt>
......
<openassessment text_response="" file_upload_response="required" file_upload_type="pdf-and-image">
<title>Open Assessment Test</title>
<prompts>
<prompt>
<description>Given the state of the world today, what do you think should be done to combat poverty? Please answer in a short essay of 200-300 words.</description>
</prompt>
</prompts>
<rubric>
<criterion>
<name>Concise</name>
<prompt>How concise is it?</prompt>
<option points="0">
<name>Neal Stephenson (late)</name>
<explanation>Neal Stephenson explanation</explanation>
</option>
<option points="1">
<name>HP Lovecraft</name>
<explanation>HP Lovecraft explanation</explanation>
</option>
</criterion>
</rubric>
<assessments>
<assessment name="peer-assessment" must_grade="1" must_be_graded_by="1" />
<assessment name="self-assessment" />
</assessments>
</openassessment>
<openassessment text_response="optional" file_upload_response="required" file_upload_type="pdf-and-image">
<title>Open Assessment Test</title>
<prompts>
<prompt>
<description>Given the state of the world today, what do you think should be done to combat poverty? Please answer in a short essay of 200-300 words.</description>
</prompt>
</prompts>
<rubric>
<criterion>
<name>Concise</name>
<prompt>How concise is it?</prompt>
<option points="0">
<name>Neal Stephenson (late)</name>
<explanation>Neal Stephenson explanation</explanation>
</option>
<option points="1">
<name>HP Lovecraft</name>
<explanation>HP Lovecraft explanation</explanation>
</option>
</criterion>
</rubric>
<assessments>
<assessment name="peer-assessment" must_grade="1" must_be_graded_by="1" />
<assessment name="self-assessment" />
</assessments>
</openassessment>
......@@ -31,6 +31,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false,
......@@ -88,6 +90,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false,
......@@ -141,6 +145,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "ɯʎ uǝʍ ʇıʇןǝ",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false,
......@@ -194,6 +200,8 @@
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false,
......@@ -258,6 +266,8 @@
"feedback_default_text": "Feedback default text",
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"text_response": "required",
"file_upload_response": null,
"file_upload_type": null,
"white_listed_file_types": null,
"allow_latex": false,
......@@ -311,6 +321,8 @@
"feedback_default_text": "Feedback default text",
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"text_response": "required",
"file_upload_response": "optional",
"file_upload_type": "image",
"white_listed_file_types": null,
"allow_latex": false,
......@@ -364,6 +376,8 @@
"feedback_default_text": "Feedback default text",
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"text_response": "required",
"file_upload_response": "optional",
"file_upload_type": "pdf-and-image",
"white_listed_file_types": null,
"allow_latex": false,
......@@ -417,6 +431,8 @@
"feedback_default_text": "Feedback default text",
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"text_response": "required",
"file_upload_response": "optional",
"file_upload_type": "custom",
"white_listed_file_types": "pdf,doc,docx",
"allow_latex": false,
......
......@@ -401,7 +401,7 @@ class TestPeerAssessmentRender(XBlockHandlerTestCase):
'review_num': 1,
'peer_submission': create_submission_dict(submission, xblock.prompts),
'file_upload_type': None,
'peer_file_url': '',
'peer_file_urls': [],
'submit_button_text': 'submit your assessment & move to response #2',
'allow_latex': False,
'user_timezone': pytz.utc,
......@@ -577,7 +577,7 @@ class TestPeerAssessmentRender(XBlockHandlerTestCase):
'peer_due': dt.datetime(2000, 1, 1).replace(tzinfo=pytz.utc),
'peer_submission': create_submission_dict(submission, xblock.prompts),
'file_upload_type': None,
'peer_file_url': '',
'peer_file_urls': [],
'review_num': 1,
'rubric_criteria': xblock.rubric_criteria,
'submit_button_text': 'Submit your assessment & review another response',
......
# -*- coding: utf-8 -*-
"""
Test that the student can save a files descriptions.
"""
import json
import mock
from .base import XBlockHandlerTestCase, scenario
class SaveFilesDescriptionsTest(XBlockHandlerTestCase):
"""
Group of tests to check ability to save files descriptions
"""
@scenario('data/save_scenario.xml', user_id="Daniels")
def test_save_files_descriptions_blank(self, xblock):
"""
Checks ability to call handler without descriptions.
"""
resp = self.request(xblock, 'save_files_descriptions', json.dumps({}))
self.assertIn('descriptions were not submitted', resp)
@scenario('data/save_scenario.xml', user_id="Perleman")
def test_save_files_descriptions(self, xblock):
"""
Checks ability to call handler with descriptions and then saved texts should be available after xblock render.
"""
# Save the response
descriptions = [u"Ѕраѓтаиѕ! ГоиіБЂт, Щэ ↁіиэ іи Нэll!", u"Ѕраѓтаиѕ! ГоиіБЂт, Щэ ↁіиэ іи Нэll!"]
payload = json.dumps({'descriptions': descriptions})
resp = self.request(xblock, 'save_files_descriptions', payload, response_format="json")
self.assertTrue(resp['success'])
self.assertEqual(resp['msg'], u'')
# Reload the submission UI
xblock._get_download_url = mock.MagicMock(side_effect=lambda i: "https://img-url/%d" % i)
resp = self.request(xblock, 'render_submission', json.dumps({}))
self.assertIn(descriptions[0], resp.decode('utf-8'))
self.assertIn(descriptions[1], resp.decode('utf-8'))
@scenario('data/save_scenario.xml', user_id="Valchek")
def test_overwrite_files_descriptions(self, xblock):
"""
Checks ability to overwrite existed files descriptions.
"""
descriptions1 = [u"Ѕраѓтаиѕ! ГоиіБЂт, Щэ ↁіиэ іи Нэll!", u"Ѕраѓтаиѕ! ГоиіБЂт, Щэ ↁіиэ іи Нэll!"]
payload = json.dumps({'descriptions': descriptions1})
self.request(xblock, 'save_files_descriptions', payload, response_format="json")
descriptions2 = [u"test1", u"test2"]
payload = json.dumps({'descriptions': descriptions2})
self.request(xblock, 'save_files_descriptions', payload, response_format="json")
# Reload the submission UI
xblock._get_download_url = mock.MagicMock(side_effect=lambda i: "https://img-url/%d" % i)
resp = self.request(xblock, 'render_submission', json.dumps({}))
self.assertNotIn(descriptions1[0], resp.decode('utf-8'))
self.assertNotIn(descriptions1[1], resp.decode('utf-8'))
self.assertIn(descriptions2[0], resp.decode('utf-8'))
self.assertIn(descriptions2[1], resp.decode('utf-8'))
......@@ -309,7 +309,7 @@ class TestSelfAssessmentRender(XBlockHandlerTestCase):
'rubric_criteria': xblock.rubric_criteria,
'self_submission': submission,
'file_upload_type': None,
'self_file_url': '',
'self_file_urls': [],
'allow_latex': False,
'user_timezone': pytz.utc,
'user_language': 'en'
......
......@@ -6,7 +6,7 @@ from collections import namedtuple
import json
import datetime
import urllib
from mock import MagicMock, Mock, patch
from mock import MagicMock, Mock, call, patch
from django.test.utils import override_settings
from openassessment.assessment.api import peer as peer_api
......@@ -408,7 +408,8 @@ class TestCourseStaff(XBlockHandlerTestCase):
# Create an image submission for Bob, and corresponding workflow.
self._create_submission(bob_item, {
'text': "Bob Answer",
'file_key': "test_key"
'file_keys': ["test_key"],
'files_descriptions': ["test_description"]
}, ['self'])
# Mock the file upload API to avoid hitting S3
......@@ -423,7 +424,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
file_api.get_download_url.assert_called_with("test_key")
# Check the context passed to the template
self.assertEquals('http://www.example.com/image.jpeg', context['staff_file_url'])
self.assertEquals([('http://www.example.com/image.jpeg', 'test_description')], context['staff_file_urls'])
self.assertEquals('image', context['file_upload_type'])
# Check the fully rendered template
......@@ -432,6 +433,58 @@ class TestCourseStaff(XBlockHandlerTestCase):
self.assertIn("http://www.example.com/image.jpeg", resp)
@scenario('data/self_only_scenario.xml', user_id='Bob')
def test_staff_area_student_info_many_images_submission(self, xblock):
"""
Test multiple file uploads support
"""
# Simulate that we are course staff
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, False, "Bob"
)
xblock.runtime._services['user'] = NullUserService()
bob_item = STUDENT_ITEM.copy()
bob_item["item_id"] = xblock.scope_ids.usage_id
file_keys = ["test_key0", "test_key1", "test_key2"]
files_descriptions = ["test_description0", "test_description1", "test_description2"]
images = ["http://www.example.com/image%d.jpeg" % i for i in range(3)]
file_keys_with_images = dict(zip(file_keys, images))
# Create an image submission for Bob, and corresponding workflow.
self._create_submission(bob_item, {
'text': "Bob Answer",
'file_keys': file_keys,
'files_descriptions': files_descriptions
}, ['self'])
# Mock the file upload API to avoid hitting S3
with patch("openassessment.xblock.submission_mixin.file_upload_api") as file_api:
file_api.get_download_url.return_value = Mock()
file_api.get_download_url.side_effect = lambda file_key: file_keys_with_images[file_key]
# also fake a file_upload_type so our patched url gets rendered
xblock.file_upload_type_raw = 'image'
__, context = xblock.get_student_info_path_and_context("Bob")
# Check that the right file key was passed to generate the download url
calls = [call("test_key%d" % i) for i in range(3)]
file_api.get_download_url.assert_has_calls(calls)
# Check the context passed to the template
self.assertEquals([(image, "test_description%d" % i) for i, image in enumerate(images)],
context['staff_file_urls'])
self.assertEquals('image', context['file_upload_type'])
# Check the fully rendered template
payload = urllib.urlencode({"student_username": "Bob"})
resp = self.request(xblock, "render_student_info", payload)
for i in range(3):
self.assertIn("http://www.example.com/image%d.jpeg" % i, resp)
self.assertIn("test_description%d" % i, resp)
@scenario('data/self_only_scenario.xml', user_id='Bob')
def test_staff_area_student_info_file_download_url_error(self, xblock):
# Simulate that we are course staff
xblock.xmodule_runtime = self._create_mock_runtime(
......@@ -445,7 +498,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
# Create an image submission for Bob, and corresponding workflow.
self._create_submission(bob_item, {
'text': "Bob Answer",
'file_key': "test_key"
'file_keys': ["test_key"]
}, ['self'])
# Mock the file upload API to simulate an error
......
......@@ -18,6 +18,8 @@ class StudioViewTest(XBlockHandlerTestCase):
"""
UPDATE_EDITOR_DATA = {
"title": "Test title",
"text_response": "required",
"file_upload_response": None,
"prompts": [{"description": "Test prompt"}],
"feedback_prompt": "Test feedback prompt",
"feedback_default_text": "Test feedback default text",
......
......@@ -90,6 +90,31 @@ class SubmissionTest(XBlockHandlerTestCase):
self.assertEqual(resp[1], "ENOPREVIEW")
self.assertIsNot(resp[2], None)
def _ability_to_submit_blank_answer(self, xblock):
"""
Checks ability to submit blank answer if text response is not required
"""
empty_submission = json.dumps({"submission": [""]})
resp = self.request(xblock, 'submit', empty_submission, response_format='json')
self.assertTrue(resp[0])
@scenario('data/text_response_optional.xml', user_id='Bob')
def test_ability_to_submit_blank_answer_if_text_response_optional(self, xblock):
"""
Checks ability to submit blank answer if text response is optional
"""
self._ability_to_submit_blank_answer(xblock)
@scenario('data/text_response_none.xml', user_id='Bob')
def test_ability_to_submit_blank_answer_if_text_response_none(self, xblock):
"""
Checks ability to submit blank answer if text response is None
"""
self._ability_to_submit_blank_answer(xblock)
@scenario('data/over_grade_scenario.xml', user_id='Alice')
def test_closed_submissions(self, xblock):
resp = self.request(xblock, 'render_submission', json.dumps(dict()))
......@@ -163,6 +188,39 @@ class SubmissionTest(XBlockHandlerTestCase):
self.assertTrue(resp['success'])
self.assertEqual(u'', resp['url'])
@mock_s3
@override_settings(
AWS_ACCESS_KEY_ID='foobar',
AWS_SECRET_ACCESS_KEY='bizbaz',
FILE_UPLOAD_STORAGE_BUCKET_NAME="mybucket"
)
@scenario('data/file_upload_scenario.xml')
def test_remove_all_uploaded_files(self, xblock):
""" Test remove all user files """
conn = boto.connect_s3()
bucket = conn.create_bucket('mybucket')
key = Key(bucket)
key.key = "submissions_attachments/test_student/test_course/" + xblock.scope_ids.usage_id
key.set_contents_from_string("How d'ya do?")
xblock.xmodule_runtime = Mock(
course_id='test_course',
anonymous_student_id='test_student',
)
download_url = api.get_download_url("test_student/test_course/" + xblock.scope_ids.usage_id)
resp = self.request(xblock, 'download_url', json.dumps(dict()), response_format='json')
self.assertTrue(resp['success'])
self.assertEqual(download_url, resp['url'])
resp = self.request(xblock, 'remove_all_uploaded_files', json.dumps(dict()), response_format='json')
self.assertTrue(resp['success'])
self.assertEqual(resp['removed_num'], 1)
resp = self.request(xblock, 'download_url', json.dumps(dict()), response_format='json')
self.assertTrue(resp['success'])
self.assertEqual(u'', resp['url'])
class SubmissionRenderTest(XBlockHandlerTestCase):
"""
......@@ -179,6 +237,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response_unavailable.html',
{
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'submission_start': dt.datetime(4999, 4, 1).replace(tzinfo=pytz.utc),
'allow_latex': False,
......@@ -202,6 +262,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
xblock, 'openassessmentblock/response/oa_response_submitted.html',
{
'student_submission': create_submission_dict(submission, xblock.prompts),
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'peer_incomplete': True,
'self_incomplete': True,
......@@ -216,6 +278,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response.html',
{
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'saved_response': create_submission_dict({
'answer': prepare_submission_for_serialization(
......@@ -236,6 +300,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response.html',
{
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'saved_response': create_submission_dict({
'answer': prepare_submission_for_serialization(
......@@ -260,6 +326,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response.html',
{
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'saved_response': create_submission_dict({
'answer': prepare_submission_for_serialization(
......@@ -285,6 +353,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response.html',
{
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'saved_response': create_submission_dict({
'answer': prepare_submission_for_serialization(
......@@ -311,6 +381,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
{
'submission_due': dt.datetime(2999, 5, 6).replace(tzinfo=pytz.utc),
'student_submission': create_submission_dict(submission, xblock.prompts),
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'peer_incomplete': True,
'self_incomplete': True,
......@@ -338,6 +410,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response_cancelled.html',
{
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'allow_latex': False,
'submission_due': dt.datetime(2999, 5, 6).replace(tzinfo=pytz.utc),
......@@ -371,6 +445,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
'student_submission': {"answer": {"parts": [
{"prompt": {'description': 'One prompt.'}, "text": "An old format response."}
]}},
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'peer_incomplete': True,
'self_incomplete': True,
......@@ -385,6 +461,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
self._assert_path_and_context(
xblock, 'openassessmentblock/response/oa_response_closed.html',
{
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'submission_due': dt.datetime(2014, 4, 5).replace(tzinfo=pytz.utc),
'allow_latex': False,
......@@ -404,6 +482,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
{
'submission_due': dt.datetime(2014, 4, 5).replace(tzinfo=pytz.utc),
'student_submission': create_submission_dict(submission, xblock.prompts),
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'peer_incomplete': False,
'self_incomplete': True,
......@@ -432,6 +512,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
{
'submission_due': dt.datetime(2999, 5, 6).replace(tzinfo=pytz.utc),
'student_submission': create_submission_dict(submission, xblock.prompts),
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'allow_latex': False,
'user_timezone': None,
......@@ -458,6 +540,8 @@ class SubmissionRenderTest(XBlockHandlerTestCase):
{
'submission_due': dt.datetime(2014, 4, 5).replace(tzinfo=pytz.utc),
'student_submission': create_submission_dict(submission, xblock.prompts),
'text_response': 'required',
'file_upload_response': None,
'file_upload_type': None,
'allow_latex': False,
'user_timezone': None,
......
......@@ -115,6 +115,8 @@ class TestSerializeContent(TestCase):
def _configure_xblock(self, data):
self.oa_block.title = data.get('title', '')
self.oa_block.text_response = data.get('text_response', '')
self.oa_block.file_upload_response = data.get('file_upload_response', None)
self.oa_block.prompt = data.get('prompt')
self.oa_block.prompts = create_prompts_list(data.get('prompt'))
self.oa_block.rubric_feedback_prompt = data.get('rubric_feedback_prompt')
......
......@@ -378,7 +378,7 @@ def validator(oa_block, _, strict_post_release=True):
return _inner
def validate_submission(submission, prompts, _):
def validate_submission(submission, prompts, _, text_response='required'):
"""
Validate submission dict.
......@@ -398,7 +398,7 @@ def validate_submission(submission, prompts, _):
if type(submission) != list:
return False, message
if len(submission) != len(prompts):
if text_response == 'required' and len(submission) != len(prompts):
return False, message
for submission_part in submission:
......
......@@ -700,6 +700,14 @@ def serialize_content_to_xml(oa_block, root):
if oa_block.leaderboard_show:
root.set('leaderboard_show', unicode(oa_block.leaderboard_show))
# Set text response
if oa_block.text_response:
root.set('text_response', unicode(oa_block.text_response))
# Set file upload response
if oa_block.file_upload_response:
root.set('file_upload_response', unicode(oa_block.file_upload_response))
# Set File upload settings
if oa_block.file_upload_type:
root.set('file_upload_type', unicode(oa_block.file_upload_type))
......@@ -833,6 +841,14 @@ def parse_from_xml(root):
if 'submission_due' in root.attrib:
submission_due = parse_date(unicode(root.attrib['submission_due']), name="submission due date")
text_response = None
if 'text_response' in root.attrib:
text_response = unicode(root.attrib['text_response'])
file_upload_response = None
if 'file_upload_response' in root.attrib:
file_upload_response = unicode(root.attrib['file_upload_response'])
allow_file_upload = None
if 'allow_file_upload' in root.attrib:
allow_file_upload = _parse_boolean(unicode(root.attrib['allow_file_upload']))
......@@ -890,6 +906,8 @@ def parse_from_xml(root):
'rubric_feedback_default_text': rubric['feedback_default_text'],
'submission_start': submission_start,
'submission_due': submission_due,
'text_response': text_response,
'file_upload_response': file_upload_response,
'allow_file_upload': allow_file_upload,
'file_upload_type': file_upload_type,
'white_listed_file_types': white_listed_file_types,
......
......@@ -158,6 +158,38 @@ class SubmissionPage(OpenAssessmentPage):
self.wait_for_element_visibility(".submission__answer__upload", "File select button is present")
self.q(css=".submission__answer__upload").results[0].send_keys(file_path_name)
def add_file_description(self, file_num, description):
"""
Submit a description for some file.
Args:
file_num (integer): file number
description (string): file description
"""
textarea_element = self._bounded_selector("textarea.file__description__%d" % file_num)
self.wait_for_element_visibility(textarea_element, "Textarea is present")
self.q(css=textarea_element).fill(description)
@property
def upload_file_button_is_enabled(self):
"""
Check if 'Upload files' button is enabled
Returns:
bool
"""
return self.q(css="button.file__upload").attrs('disabled') == ['false']
@property
def upload_file_button_is_disabled(self):
"""
Check if 'Upload files' button is disabled
Returns:
bool
"""
return self.q(css="button.file__upload").attrs('disabled') == ['true']
def upload_file(self):
"""
Upload the selected file
......@@ -196,14 +228,15 @@ class SubmissionPage(OpenAssessmentPage):
return self.q(css="div.upload__error > div.message--error").visible
@property
def has_file_uploaded(self):
def have_files_uploaded(self):
"""
Check whether file is successfully uploaded
Check whether files were successfully uploaded
Returns:
bool
"""
return self.q(css=".submission__custom__upload").visible
self.wait_for_element_visibility('.submission__custom__upload', 'Uploaded files block is presented')
return self.q(css=".submission__answer__files").visible
class AssessmentMixin(object):
......
......@@ -741,10 +741,22 @@ class FileUploadTest(OpenAssessmentTest):
self.assertTrue(self.submission_page.has_file_error)
# trying to upload a acceptable file
self.submission_page.visit().select_file(os.path.dirname(os.path.realpath(__file__)) + '/README.rst')
readme1 = os.path.dirname(os.path.realpath(__file__)) + '/README.rst'
readme2 = readme1.replace('test/acceptance/', '') # There's another README located at ../../
files = ', '.join([readme1, readme2])
self.submission_page.visit().select_file(files)
self.assertFalse(self.submission_page.has_file_error)
self.assertTrue(self.submission_page.upload_file_button_is_disabled)
self.submission_page.add_file_description(0, 'file description 1')
self.assertTrue(self.submission_page.upload_file_button_is_disabled)
self.submission_page.add_file_description(1, 'file description 2')
self.assertTrue(self.submission_page.upload_file_button_is_enabled)
self.submission_page.upload_file()
self.assertTrue(self.submission_page.has_file_uploaded)
self.assertTrue(self.submission_page.have_files_uploaded)
class FullWorkflowMixin(object):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment