Commit 179f685b by Dmitry Viskov

Multiple file uploads support

parent 88915a6b
...@@ -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
......
...@@ -42,6 +42,10 @@ class Backend(BaseBackend): ...@@ -42,6 +42,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,10 @@ class Backend(BaseBackend): ...@@ -63,6 +63,10 @@ class Backend(BaseBackend):
) )
raise FileUploadInternalError(ex) raise FileUploadInternalError(ex)
def remove_file(self, key):
"""
@TODO Need to be implemented to remove previously uploaded files
"""
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):
......
...@@ -103,16 +103,16 @@ ...@@ -103,16 +103,16 @@
</li> </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_upload_selector" class="setting-label">{% trans "Allow File Uploads"%}</label>
<select id="openassessment_submission_upload_selector" class="input setting-input" name="upload submission"> <select id="openassessment_submission_upload_selector" class="input setting-input" name="upload submission">
<option value="">{% trans "None"%}</option> <option value="">{% trans "None"%}</option>
<option value="image" {% if file_upload_type == "image" %} selected="true" {% endif %}>{% trans "Image File"%}</option> <option value="image" {% if file_upload_type == "image" %} selected="true" {% endif %}>{% trans "Image Files"%}</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="custom" {% if file_upload_type == "custom" %} selected="true" {% endif %}>{% trans "Custom File Types"%}</option> <option value="custom" {% if file_upload_type == "custom" %} selected="true" {% endif %}>{% trans "Custom File Types"%}</option>
</select> </select>
</div> </div>
<p class="setting-help"> <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> </p>
<div id="openassessment_submission_white_listed_file_types_wrapper" class="{% if file_upload_type != "custom" %}is--hidden{% endif %}"> <div id="openassessment_submission_white_listed_file_types_wrapper" class="{% if file_upload_type != "custom" %}is--hidden{% endif %}">
<div class="wrapper-comp-setting"> <div class="wrapper-comp-setting">
......
...@@ -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" %}
</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"%}
</div> </div>
</li> </li>
{% endfor %} {% endfor %}
......
...@@ -10,18 +10,24 @@ ...@@ -10,18 +10,24 @@
</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 in file_urls %}
alt="{% trans "The image associated with this submission." %}" <div class="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" %} <img class="submission__answer__file submission--image"
<a href="{{ file_url }}" class="submission__answer__file submission--file" target="_blank"> alt="{% trans "The image associated with this submission:" %} #{{ forloop.counter }}"
{% trans "View the file associated with this submission." %} src="{{ file_url }}" />
</a> {% elif file_upload_type == "pdf-and-image" or file_upload_type == "custom" %}
{% if show_warning %} <a href="{{ file_url }}" class="submission__answer__file submission--file" target="_blank">
<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> {% trans "View the files associated with this submission:" %} #{{ forloop.counter }}
</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 %}
......
...@@ -69,7 +69,7 @@ ...@@ -69,7 +69,7 @@
{% 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 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" %} {% 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" %}
</div> </div>
<form class="peer-assessment--001__assessment peer-assessment__assessment" method="post"> <form class="peer-assessment--001__assessment peer-assessment__assessment" method="post">
......
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
{% 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 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" %} {% 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" %}
</div> </div>
<form class="peer-assessment--001__assessment peer-assessment__assessment" method="post"> <form class="peer-assessment--001__assessment peer-assessment__assessment" method="post">
......
...@@ -100,12 +100,12 @@ ...@@ -100,12 +100,12 @@
</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>
</li> </li>
{% endif %} {% endif %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=file_url class_prefix="submission__answer"%} {% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=file_urls class_prefix="submission__answer"%}
</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>
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,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 Uploaded File" as translated_header %} {% 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" %} {% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=file_urls header=translated_header class_prefix="submission__answer" %}
</article> </article>
</div> </div>
</div> </div>
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,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 Uploaded File" as translated_header %} {% 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" %} {% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_urls=file_urls header=translated_header class_prefix="submission__answer" %}
</article> </article>
</div> </div>
</div> </div>
......
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
{% 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 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" %} {% 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" %}
</article> </article>
<form class="self-assessment--001__assessment self-assessment__assessment" method="post"> <form class="self-assessment--001__assessment self-assessment__assessment" method="post">
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
{% 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 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" %} {% 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" %}
</div> </div>
<form class="staff-assessment__assessment" method="post"> <form class="staff-assessment__assessment" method="post">
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
{% 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 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" %} {% 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" %}
</div> </div>
<form class="staff-assessment__assessment" method="post"> <form class="staff-assessment__assessment" method="post">
......
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
{% 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 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" %} {% 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" %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
......
...@@ -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)
......
...@@ -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:
......
...@@ -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.
...@@ -65,14 +65,14 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -65,14 +65,14 @@ describe("OpenAssessment.ResponseView", function() {
var errorPromise = $.Deferred(function(defer) { defer.rejectWith(this, ["ERROR"]); }).promise(); var errorPromise = $.Deferred(function(defer) { defer.rejectWith(this, ["ERROR"]); }).promise();
this.uploadError = false; this.uploadError = false;
this.uploadArgs = null; this.uploadArgs = [];
this.upload = function(url, data) { this.upload = function(url, data) {
// Store the args we were passed so we can verify them // Store the args we were passed so we can verify them
this.uploadArgs = { this.uploadArgs.push({
url: url, url: url,
data: data data: data
}; });
// Return a promise indicating success or error // Return a promise indicating success or error
return this.uploadError ? errorPromise : successPromise; return this.uploadError ? errorPromise : successPromise;
...@@ -135,6 +135,11 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -135,6 +135,11 @@ describe("OpenAssessment.ResponseView", function() {
else { defer.reject(); } else { defer.reject(); }
}); });
}); });
spyOn(view, 'removeUploadedFiles').and.callFake(function() {
return $.Deferred(function(defer) {
defer.resolve();
});
});
}); });
afterEach(function() { afterEach(function() {
...@@ -436,7 +441,8 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -436,7 +441,8 @@ describe("OpenAssessment.ResponseView", function() {
spyOn(view.baseView, 'toggleActionError').and.callThrough(); spyOn(view.baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'image/jpeg', size: 6000000, name: 'huge-picture.jpg', data: ''}]; var files = [{type: 'image/jpeg', size: 6000000, name: 'huge-picture.jpg', data: ''}];
view.prepareUpload(files, 'image'); view.prepareUpload(files, 'image');
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File size must be 5MB or less.'); expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload',
'File size must be 5MB or less.');
}); });
it("selects the wrong image file type", function() { it("selects the wrong image file type", function() {
...@@ -471,31 +477,62 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -471,31 +477,62 @@ describe("OpenAssessment.ResponseView", function() {
view.data.FILE_TYPE_WHITE_LIST = ['exe']; view.data.FILE_TYPE_WHITE_LIST = ['exe'];
var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}]; var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}];
view.prepareUpload(files, 'custom'); view.prepareUpload(files, 'custom');
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File type is not allowed.'); expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload',
'File type is not allowed.');
});
it("selects one small and one large file", function() {
spyOn(view.baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'image/jpeg', size: 1024, name: 'small-picture.jpg', data: ''},
{type: 'image/jpeg', size: 6000000, name: 'huge-picture.jpg', data: ''}];
view.prepareUpload(files, 'image');
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload',
'File size must be 5MB or less.');
});
it("selects three files - one with invalid extension", function() {
spyOn(view.baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'image/jpeg', size: 1024, name: 'small-picture-1.jpg', data: ''},
{type: 'application/exe', size: 1024, name: 'application.exe', data: ''},
{type: 'image/jpeg', size: 1024, name: 'small-picture-2.jpg', data: ''}];
view.prepareUpload(files, 'image');
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload',
'You can upload files with these file types: JPG, PNG or GIF');
}); });
it("uploads an image using a one-time URL", function() { it("uploads an image using a one-time URL", function() {
var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}]; var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files, 'image'); view.prepareUpload(files, 'image');
view.fileUpload(); view.uploadFiles();
expect(fileUploader.uploadArgs.url).toEqual(FAKE_URL); expect(fileUploader.uploadArgs[0].url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs.data).toEqual(files[0]); expect(fileUploader.uploadArgs[0].data).toEqual(files[0]);
});
it("uploads two images using a one-time URL", function() {
var files = [{type: 'image/jpeg', size: 1024, name: 'picture1.jpg', data: ''},
{type: 'image/jpeg', size: 1024, name: 'picture2.jpg', data: ''}];
view.prepareUpload(files, 'image');
view.uploadFiles();
expect(fileUploader.uploadArgs[0].url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs[0].data).toEqual(files[0]);
expect(fileUploader.uploadArgs[1].url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs[1].data).toEqual(files[1]);
}); });
it("uploads a PDF using a one-time URL", function() { it("uploads a PDF using a one-time URL", function() {
var files = [{type: 'application/pdf', size: 1024, name: 'application.pdf', data: ''}]; var files = [{type: 'application/pdf', size: 1024, name: 'application.pdf', data: ''}];
view.prepareUpload(files, 'pdf-and-image'); view.prepareUpload(files, 'pdf-and-image');
view.fileUpload(); view.uploadFiles();
expect(fileUploader.uploadArgs.url).toEqual(FAKE_URL); expect(fileUploader.uploadArgs[0].url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs.data).toEqual(files[0]); expect(fileUploader.uploadArgs[0].data).toEqual(files[0]);
}); });
it("uploads a arbitrary type file using a one-time URL", function() { it("uploads a arbitrary type file using a one-time URL", function() {
var files = [{type: 'text/html', size: 1024, name: 'index.html', data: ''}]; var files = [{type: 'text/html', size: 1024, name: 'index.html', data: ''}];
view.prepareUpload(files, 'custom'); view.prepareUpload(files, 'custom');
view.fileUpload(); view.uploadFiles();
expect(fileUploader.uploadArgs.url).toEqual(FAKE_URL); expect(fileUploader.uploadArgs[0].url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs.data).toEqual(files[0]); expect(fileUploader.uploadArgs[0].data).toEqual(files[0]);
}); });
it("displays an error if a one-time file upload URL cannot be retrieved", function() { it("displays an error if a one-time file upload URL cannot be retrieved", function() {
...@@ -506,7 +543,7 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -506,7 +543,7 @@ describe("OpenAssessment.ResponseView", function() {
// Attempt to upload a file // Attempt to upload a file
var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}]; var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files, 'image'); view.prepareUpload(files, 'image');
view.fileUpload(); view.uploadFiles();
// Expect an error to be displayed // Expect an error to be displayed
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'ERROR'); expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'ERROR');
...@@ -520,7 +557,7 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -520,7 +557,7 @@ describe("OpenAssessment.ResponseView", function() {
// Attempt to upload a file // Attempt to upload a file
var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}]; var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files, 'image'); view.prepareUpload(files, 'image');
view.fileUpload(); view.uploadFiles();
// Expect an error to be displayed // Expect an error to be displayed
expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'ERROR'); expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'ERROR');
......
...@@ -486,17 +486,18 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) { ...@@ -486,17 +486,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 +509,36 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) { ...@@ -508,16 +509,36 @@ 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() {
defer.resolve();
}).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]); }
......
...@@ -34,6 +34,8 @@ class SubmissionMixin(object): ...@@ -34,6 +34,8 @@ class SubmissionMixin(object):
ALLOWED_FILE_MIME_TYPES = ['application/pdf'] + ALLOWED_IMAGE_MIME_TYPES ALLOWED_FILE_MIME_TYPES = ['application/pdf'] + ALLOWED_IMAGE_MIME_TYPES
MAX_FILES_COUNT = 20
# taken from http://www.howtogeek.com/137270/50-file-extensions-that-are-potentially-dangerous-on-windows/ # taken from http://www.howtogeek.com/137270/50-file-extensions-that-are-potentially-dangerous-on-windows/
# and http://pcsupport.about.com/od/tipstricks/a/execfileext.htm # and http://pcsupport.about.com/od/tipstricks/a/execfileext.htm
# left out .js and office extensions # left out .js and office extensions
...@@ -58,9 +60,9 @@ class SubmissionMixin(object): ...@@ -58,9 +60,9 @@ class SubmissionMixin(object):
Args: Args:
data (dict): Data may contain two attributes: submission and data (dict): Data may contain two attributes: submission and
file_url. submission is the response from the student which file_urls. submission is the response from the student which
should be stored in the Open Assessment system. file_url is the should be stored in the Open Assessment system. file_urls is the
path to a related file for the submission. file_url is optional. path to a related file for the submission. file_urls is optional.
suffix (str): Not used in this handler. suffix (str): Not used in this handler.
Returns: Returns:
...@@ -154,7 +156,7 @@ class SubmissionMixin(object): ...@@ -154,7 +156,7 @@ class SubmissionMixin(object):
Args: Args:
data (dict): Data should have a single key 'submission' that contains data (dict): Data should have a single key 'submission' that contains
the text of the student's response. Optionally, the data could the text of the student's response. Optionally, the data could
have a 'file_url' key that is the path to an associated file for have a 'file_urls' key that is the path to an associated file for
this submission. this submission.
suffix (str): Not used. suffix (str): Not used.
...@@ -192,7 +194,21 @@ class SubmissionMixin(object): ...@@ -192,7 +194,21 @@ class SubmissionMixin(object):
student_sub_dict = prepare_submission_for_serialization(student_sub_data) student_sub_dict = prepare_submission_for_serialization(student_sub_data)
if self.file_upload_type: if self.file_upload_type:
student_sub_dict['file_key'] = self._get_student_item_key() student_sub_dict['file_keys'] = []
for i in range(self.MAX_FILES_COUNT):
key_to_save = ''
item_key = self._get_student_item_key(i)
try:
url = file_upload_api.get_download_url(item_key)
if url:
key_to_save = item_key
except FileUploadError:
pass
if key_to_save:
student_sub_dict['file_keys'].append(key_to_save)
else:
break
submission = api.create_submission(student_item_dict, student_sub_dict) submission = api.create_submission(student_item_dict, student_sub_dict)
self.create_workflow(submission["uuid"]) self.create_workflow(submission["uuid"])
self.submission_uuid = submission["uuid"] self.submission_uuid = submission["uuid"]
...@@ -227,6 +243,7 @@ class SubmissionMixin(object): ...@@ -227,6 +243,7 @@ class SubmissionMixin(object):
content_type = data['contentType'] content_type = data['contentType']
file_name = data['filename'] file_name = data['filename']
file_name_parts = file_name.split('.') file_name_parts = file_name.split('.')
file_num = int(data.get('filenum', 0))
file_ext = file_name_parts[-1] if len(file_name_parts) > 1 else None file_ext = file_name_parts[-1] if len(file_name_parts) > 1 else None
if self.file_upload_type == 'image' and content_type not in self.ALLOWED_IMAGE_MIME_TYPES: if self.file_upload_type == 'image' and content_type not in self.ALLOWED_IMAGE_MIME_TYPES:
...@@ -242,7 +259,7 @@ class SubmissionMixin(object): ...@@ -242,7 +259,7 @@ class SubmissionMixin(object):
if file_ext in self.FILE_EXT_BLACK_LIST: if file_ext in self.FILE_EXT_BLACK_LIST:
return {'success': False, 'msg': self._(u"File type is not allowed.")} return {'success': False, 'msg': self._(u"File type is not allowed.")}
try: try:
key = self._get_student_item_key() key = self._get_student_item_key(file_num)
url = file_upload_api.get_upload_url(key, content_type) url = file_upload_api.get_upload_url(key, content_type)
return {'success': True, 'url': url} return {'success': True, 'url': url}
except FileUploadError: except FileUploadError:
...@@ -258,20 +275,36 @@ class SubmissionMixin(object): ...@@ -258,20 +275,36 @@ class SubmissionMixin(object):
A URL to be used for downloading content related to the submission. A URL to be used for downloading content related to the submission.
""" """
return {'success': True, 'url': self._get_download_url()} file_num = int(data.get('filenum', 0))
return {'success': True, 'url': self._get_download_url(file_num)}
@XBlock.json_handler
def remove_all_uploaded_files(self, data, suffix=''):
"""
Removes all uploaded user files.
"""
removed_num = 0
for i in range(self.MAX_FILES_COUNT):
removed = file_upload_api.remove_file(self._get_student_item_key(i))
if removed:
removed_num += 1
else:
break
return {'success': True, 'removed_num': removed_num}
def _get_download_url(self): def _get_download_url(self, file_num=0):
""" """
Internal function for retrieving the download url. Internal function for retrieving the download url.
""" """
try: try:
return file_upload_api.get_download_url(self._get_student_item_key()) return file_upload_api.get_download_url(self._get_student_item_key(file_num))
except FileUploadError: except FileUploadError:
logger.exception("Error retrieving download URL.") logger.exception("Error retrieving download URL.")
return '' return ''
def _get_student_item_key(self): def _get_student_item_key(self, num=0):
""" """
Simple utility method to generate a common file upload key based on Simple utility method to generate a common file upload key based on
the student item. the student item.
...@@ -281,27 +314,23 @@ class SubmissionMixin(object): ...@@ -281,27 +314,23 @@ class SubmissionMixin(object):
""" """
student_item_dict = self.get_student_item_dict() student_item_dict = self.get_student_item_dict()
return u"{student_id}/{course_id}/{item_id}".format( num = int(num)
**student_item_dict if num > 0:
) student_item_dict['num'] = num
return u"{student_id}/{course_id}/{item_id}/{num}".format(
**student_item_dict
)
else:
return u"{student_id}/{course_id}/{item_id}".format(
**student_item_dict
)
def get_download_url_from_submission(self, submission): def _get_url_by_file_key(self, key):
""" """
Returns a download URL for retrieving content within a submission. Return download url for some particular file key.
Args:
submission (dict): Dictionary containing an answer and a file_key.
The file_key is used to try and retrieve a download url
with related content
Returns:
A URL to related content. If there is no content related to this
key, or if there is no key for the submission, returns an empty
string.
""" """
url = "" url = ''
key = submission['answer'].get('file_key', '')
try: try:
if key: if key:
url = file_upload_api.get_download_url(key) url = file_upload_api.get_download_url(key)
...@@ -309,6 +338,37 @@ class SubmissionMixin(object): ...@@ -309,6 +338,37 @@ class SubmissionMixin(object):
logger.exception("Unable to generate download url for file key {}".format(key)) logger.exception("Unable to generate download url for file key {}".format(key))
return url return url
def get_download_urls_from_submission(self, submission):
"""
Returns a download URLs for retrieving content within a submission.
Args:
submission (dict): Dictionary containing an answer and a file_keys.
The file_keys is used to try and retrieve a download urls
with related content
Returns:
List with URLs to related content. If there is no content related to this
key, or if there is no key for the submission, returns an empty
list.
"""
urls = []
if 'file_keys' in submission['answer']:
keys = submission['answer'].get('file_keys', '')
for key in keys:
url = self._get_url_by_file_key(key)
if url:
urls.append(url)
else:
break
elif 'file_key' in submission['answer']:
key = submission['answer'].get('file_key', '')
url = self._get_url_by_file_key(key)
if url:
urls.append(url)
return urls
@staticmethod @staticmethod
def get_user_submission(submission_uuid): def get_user_submission(submission_uuid):
"""Return the most recent submission by user in workflow """Return the most recent submission by user in workflow
...@@ -395,7 +455,13 @@ class SubmissionMixin(object): ...@@ -395,7 +455,13 @@ class SubmissionMixin(object):
context['allow_latex'] = self.allow_latex context['allow_latex'] = self.allow_latex
if self.file_upload_type: if self.file_upload_type:
context['file_url'] = self._get_download_url() context['file_urls'] = []
for i in range(self.MAX_FILES_COUNT):
file_url = self._get_download_url(i)
if file_url:
context['file_urls'].append(file_url)
else:
break
if self.file_upload_type == 'custom': if self.file_upload_type == 'custom':
context['white_listed_file_types'] = self.white_listed_file_types context['white_listed_file_types'] = self.white_listed_file_types
......
...@@ -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',
......
...@@ -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,7 @@ class TestCourseStaff(XBlockHandlerTestCase): ...@@ -408,7 +408,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 avoid hitting S3 # Mock the file upload API to avoid hitting S3
...@@ -423,7 +423,7 @@ class TestCourseStaff(XBlockHandlerTestCase): ...@@ -423,7 +423,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'], 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 +432,54 @@ class TestCourseStaff(XBlockHandlerTestCase): ...@@ -432,6 +432,54 @@ 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"]
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
}, ['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(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)
@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 +493,7 @@ class TestCourseStaff(XBlockHandlerTestCase): ...@@ -445,7 +493,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
......
...@@ -163,6 +163,39 @@ class SubmissionTest(XBlockHandlerTestCase): ...@@ -163,6 +163,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):
""" """
......
...@@ -196,14 +196,15 @@ class SubmissionPage(OpenAssessmentPage): ...@@ -196,14 +196,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,13 @@ class FileUploadTest(OpenAssessmentTest): ...@@ -741,10 +741,13 @@ 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.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