diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py
index 3741e16f163414b53119bef96941141e3b5b0512..b2dbdef99586e1ef22b63f4ab2c78eabaf679a42 100644
--- a/lms/djangoapps/instructor/tests/test_api.py
+++ b/lms/djangoapps/instructor/tests/test_api.py
@@ -149,6 +149,7 @@ INSTRUCTOR_POST_ENDPOINTS = set([
     'calculate_grades_csv',
     'change_due_date',
     'export_ora2_data',
+    'export_ora2_submission_files',
     'get_grading_config',
     'get_problem_responses',
     'get_proctored_exam_results',
@@ -428,6 +429,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
             ('get_proctored_exam_results', {}),
             ('get_problem_responses', {}),
             ('export_ora2_data', {}),
+            ('export_ora2_submission_files', {}),
             ('rescore_problem',
              {'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.user.email}),
             ('override_problem_score',
@@ -2875,6 +2877,32 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
 
         self.assertContains(response, already_running_status, status_code=400)
 
+    def test_get_ora2_submission_files_success(self):
+        url = reverse('export_ora2_submission_files', kwargs={'course_id': text_type(self.course.id)})
+
+        with patch(
+            'lms.djangoapps.instructor_task.api.submit_export_ora2_submission_files'
+        ) as mock_submit_ora2_task:
+            mock_submit_ora2_task.return_value = True
+            response = self.client.post(url, {})
+
+        success_status = 'Attachments archive is being created.'
+
+        self.assertContains(response, success_status)
+
+    def test_get_ora2_submission_files_already_running(self):
+        url = reverse('export_ora2_submission_files', kwargs={'course_id': text_type(self.course.id)})
+        task_type = 'export_ora2_submission_files'
+        already_running_status = generate_already_running_error_message(task_type)
+
+        with patch(
+            'lms.djangoapps.instructor_task.api.submit_export_ora2_submission_files'
+        ) as mock_submit_ora2_task:
+            mock_submit_ora2_task.side_effect = AlreadyRunningError(already_running_status)
+            response = self.client.post(url, {})
+
+        self.assertContains(response, already_running_status, status_code=400)
+
     def test_get_student_progress_url(self):
         """ Test that progress_url is in the successful response. """
         url = reverse('get_student_progress_url', kwargs={'course_id': text_type(self.course.id)})
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index 640d27e46c5b6df934db8b829e5701f2eb1bd9ae..904d8dfe7df98ac3cf4431a9c94688839856da62 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -2049,6 +2049,28 @@ def export_ora2_data(request, course_id):
     return JsonResponse({"status": success_status})
 
 
+@transaction.non_atomic_requests
+@require_POST
+@ensure_csrf_cookie
+@cache_control(no_cache=True, no_store=True, must_revalidate=True)
+@require_course_permission(permissions.CAN_RESEARCH)
+@common_exceptions_400
+def export_ora2_submission_files(request, course_id):
+    """
+    Pushes a Celery task which will download and compress all submission
+    files (texts, attachments) into a zip archive.
+    """
+    course_key = CourseKey.from_string(course_id)
+
+    task_api.submit_export_ora2_submission_files(request, course_key)
+
+    return JsonResponse({
+        "status": _(
+            "Attachments archive is being created."
+        )
+    })
+
+
 @transaction.non_atomic_requests
 @require_POST
 @ensure_csrf_cookie
diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py
index b9f3442c381f4dbfa80e5e9ac3d8578d1f272b73..b7cf4dfa1099e806339c21fb5142a7e2b1c12713 100644
--- a/lms/djangoapps/instructor/views/api_urls.py
+++ b/lms/djangoapps/instructor/views/api_urls.py
@@ -54,6 +54,9 @@ urlpatterns = [
     url(r'^get_course_survey_results$', api.get_course_survey_results, name='get_course_survey_results'),
     url(r'^export_ora2_data', api.export_ora2_data, name='export_ora2_data'),
 
+    url(r'^export_ora2_submission_files', api.export_ora2_submission_files,
+        name='export_ora2_submission_files'),
+
     # spoc gradebook
     url(r'^gradebook$', gradebook_api.spoc_gradebook, name='spoc_gradebook'),
 
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index 7b39156a8a244e82b86c6a825b467e6ca989a49d..9aa02d022f777a294b9c0313b3d06ad259ac7a53 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -628,6 +628,9 @@ def _section_data_download(course, access):
             'get_course_survey_results', kwargs={'course_id': six.text_type(course_key)}
         ),
         'export_ora2_data_url': reverse('export_ora2_data', kwargs={'course_id': six.text_type(course_key)}),
+        'export_ora2_submission_files_url': reverse(
+            'export_ora2_submission_files', kwargs={'course_id': six.text_type(course_key)}
+        ),
     }
     if not access.get('data_researcher'):
         section_data['is_hidden'] = True
diff --git a/lms/djangoapps/instructor_task/api.py b/lms/djangoapps/instructor_task/api.py
index 013840cb81795c7b42781c802cc50f1aeef17f40..c9c2a84997f3deeaba7fdf0a7b7414466f51b26f 100644
--- a/lms/djangoapps/instructor_task/api.py
+++ b/lms/djangoapps/instructor_task/api.py
@@ -35,6 +35,7 @@ from lms.djangoapps.instructor_task.tasks import (
     course_survey_report_csv,
     delete_problem_state,
     export_ora2_data,
+    export_ora2_submission_files,
     generate_certificates,
     override_problem_score,
     proctored_exam_results_csv,
@@ -450,6 +451,19 @@ def submit_export_ora2_data(request, course_key):
     return submit_task(request, task_type, task_class, course_key, task_input, task_key)
 
 
+def submit_export_ora2_submission_files(request, course_key):
+    """
+    Submits a task to download and compress all submissions
+    files (texts, attachments) for given course.
+    """
+    task_type = 'export_ora2_submission_files'
+    task_class = export_ora2_submission_files
+    task_input = {}
+    task_key = ''
+
+    return submit_task(request, task_type, task_class, course_key, task_input, task_key)
+
+
 def generate_certificates_for_students(request, course_key, student_set=None, specific_student_id=None):
     """
     Submits a task to generate certificates for given students enrolled in the course.
diff --git a/lms/djangoapps/instructor_task/models.py b/lms/djangoapps/instructor_task/models.py
index 9300b1b7d7dedcda9f3b17e743222c4cd762abc6..95a8d05d0ac509e616c60b40fd79af16669a80f2 100644
--- a/lms/djangoapps/instructor_task/models.py
+++ b/lms/djangoapps/instructor_task/models.py
@@ -280,9 +280,14 @@ class DjangoStorageReportStore(ReportStore):
         """
         path = self.path_to(course_id, filename)
         # See https://github.com/boto/boto/issues/2868
-        # Boto doesn't play nice with unicod in python3
+        # Boto doesn't play nice with unicode in python3
         if not six.PY2:
-            buff = ContentFile(buff.read().encode('utf-8'))
+            buff_contents = buff.read()
+
+            if not isinstance(buff_contents, bytes):
+                buff_contents = buff_contents.encode('utf-8')
+
+            buff = ContentFile(buff_contents)
 
         self.storage.save(path, buff)
 
diff --git a/lms/djangoapps/instructor_task/tasks.py b/lms/djangoapps/instructor_task/tasks.py
index d87627f079ac63b91faa7d7bcf059470b26aa248..bd7f428a643e0a5d226d2ea42fce8d2e08db60b6 100644
--- a/lms/djangoapps/instructor_task/tasks.py
+++ b/lms/djangoapps/instructor_task/tasks.py
@@ -40,6 +40,7 @@ from lms.djangoapps.instructor_task.tasks_helper.misc import (
     cohort_students_and_upload,
     upload_course_survey_report,
     upload_ora2_data,
+    upload_ora2_submission_files,
     upload_proctored_exam_results_report
 )
 from lms.djangoapps.instructor_task.tasks_helper.module_state import (
@@ -292,3 +293,14 @@ def export_ora2_data(entry_id, xmodule_instance_args):
     action_name = ugettext_noop('generated')
     task_fn = partial(upload_ora2_data, xmodule_instance_args)
     return run_main_task(entry_id, task_fn, action_name)
+
+
+@task(base=BaseInstructorTask)
+def export_ora2_submission_files(entry_id, xmodule_instance_args):
+    """
+    Download all submission files, generate csv downloads list,
+    put all this into zip archive and push it to S3.
+    """
+    action_name = ugettext_noop('compressed')
+    task_fn = partial(upload_ora2_submission_files, xmodule_instance_args)
+    return run_main_task(entry_id, task_fn, action_name)
diff --git a/lms/djangoapps/instructor_task/tasks_helper/misc.py b/lms/djangoapps/instructor_task/tasks_helper/misc.py
index b2906d01f9ad34d08c3477be000caa4b7292841f..b8477e0b6c3ce4df5b8439d7236949a7a98ac769 100644
--- a/lms/djangoapps/instructor_task/tasks_helper/misc.py
+++ b/lms/djangoapps/instructor_task/tasks_helper/misc.py
@@ -7,16 +7,21 @@ running state of a course.
 
 import logging
 from collections import OrderedDict
+from contextlib import contextmanager
 from datetime import datetime
+from io import StringIO
+from tempfile import TemporaryFile
 from time import time
+from zipfile import ZipFile
 import csv
+import os
 import unicodecsv
 import six
 
 from django.contrib.auth.models import User
 from django.core.exceptions import ValidationError
 from django.core.files.storage import DefaultStorage
-from openassessment.data import OraAggregateData
+from openassessment.data import OraAggregateData, OraDownloadData
 from pytz import UTC
 
 from lms.djangoapps.instructor_analytics.basic import get_proctored_exam_results
@@ -27,7 +32,12 @@ from survey.models import SurveyAnswer
 from util.file import UniversalNewlineIterator
 
 from .runner import TaskProgress
-from .utils import UPDATE_STATUS_FAILED, UPDATE_STATUS_SUCCEEDED, upload_csv_to_report_store
+from .utils import (
+    UPDATE_STATUS_FAILED,
+    UPDATE_STATUS_SUCCEEDED,
+    upload_csv_to_report_store,
+    upload_zip_to_report_store,
+)
 
 # define different loggers for use within tasks and on client side
 TASK_LOG = logging.getLogger('edx.celery.task')
@@ -340,3 +350,108 @@ def upload_ora2_data(
     TASK_LOG.info(u'%s, Task type: %s, Upload complete.', task_info_string, action_name)
 
     return UPDATE_STATUS_SUCCEEDED
+
+
+def _task_step(task_progress, task_info_string, action_name):
+    """
+    Returns a context manager, that logs error and updates TaskProgress
+    filures counter in case inner block throws an exception.
+    """
+
+    @contextmanager
+    def _step_context_manager(step_description, exception_text, step_error_description):
+        curr_step = {'step': step_description}
+        TASK_LOG.info(
+            '%s, Task type: %s, Current step: %s',
+            task_info_string,
+            action_name,
+            curr_step,
+        )
+
+        task_progress.update_task_state(extra_meta=curr_step)
+
+        try:
+            yield
+
+        # Update progress to failed regardless of error type
+        except Exception:  # pylint: disable=broad-except
+            TASK_LOG.exception(exception_text)
+            task_progress.failed = 1
+
+            task_progress.update_task_state(extra_meta={'step': step_error_description})
+
+    return _step_context_manager
+
+
+def upload_ora2_submission_files(
+    _xmodule_instance_args, _entry_id, course_id, _task_input, action_name
+):
+    """
+    Creates zip archive with submission files in three steps:
+
+    1. Collect all files information using ORA download helper.
+    2. Download all submission attachments, put them in temporary zip
+        file along with submission texts and csv downloads list.
+    3. Upload zip file into reports storage.
+    """
+
+    start_time = time()
+    start_date = datetime.now(UTC)
+
+    num_attempted = 1
+    num_total = 1
+
+    fmt = 'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}'
+    task_info_string = fmt.format(
+        task_id=_xmodule_instance_args.get('task_id') if _xmodule_instance_args is not None else None,
+        entry_id=_entry_id,
+        course_id=course_id,
+        task_input=_task_input
+    )
+    TASK_LOG.info(u'%s, Task type: %s, Starting task execution', task_info_string, action_name)
+
+    task_progress = TaskProgress(action_name, num_total, start_time)
+    task_progress.attempted = num_attempted
+
+    step_manager = _task_step(task_progress, task_info_string, action_name)
+
+    submission_files_data = None
+    with step_manager(
+        'Collecting attachments data',
+        'Failed to get ORA submissions attachments data.',
+        'Error while collecting data',
+    ):
+        submission_files_data = OraDownloadData.collect_ora2_submission_files(course_id)
+
+    if submission_files_data is None:
+        return UPDATE_STATUS_FAILED
+
+    with TemporaryFile('rb+') as zip_file:
+        compressed = None
+        with step_manager(
+            'Downloading and compressing attachments files',
+            'Failed to download and compress submissions attachments.',
+            'Error while downloading and compressing submissions attachments',
+        ):
+            compressed = OraDownloadData.create_zip_with_attachments(zip_file, course_id, submission_files_data)
+
+        if compressed is None:
+            return UPDATE_STATUS_FAILED
+
+        zip_filename = None
+        with step_manager(
+            'Uploading zip file to storage',
+            'Failed to upload zip file to storage.',
+            'Error while uploading zip file to storage',
+        ):
+            zip_filename = upload_zip_to_report_store(zip_file, 'submission_files', course_id, start_date),
+
+        if not zip_filename:
+            return UPDATE_STATUS_FAILED
+
+    task_progress.succeeded = 1
+    curr_step = {'step': 'Finalizing attachments extracting'}
+    task_progress.update_task_state(extra_meta=curr_step)
+    TASK_LOG.info(u'%s, Task type: %s, Upload complete.', task_info_string, action_name)
+
+    return UPDATE_STATUS_SUCCEEDED
diff --git a/lms/djangoapps/instructor_task/tasks_helper/utils.py b/lms/djangoapps/instructor_task/tasks_helper/utils.py
index cb72ea4877f5c21cbcf57097bad1b69ada3443c4..607851f77f4502de61246fe071674cd8d9a281f7 100644
--- a/lms/djangoapps/instructor_task/tasks_helper/utils.py
+++ b/lms/djangoapps/instructor_task/tasks_helper/utils.py
@@ -49,6 +49,23 @@ def upload_csv_to_report_store(rows, csv_name, course_id, timestamp, config_name
     return report_name, report_path
 
 
+def upload_zip_to_report_store(file, zip_name, course_id, timestamp, config_name='GRADES_DOWNLOAD'):
+    """
+    Upload given file buffer as a zip file using ReportStore.
+    """
+    report_store = ReportStore.from_config(config_name)
+
+    report_name = u"{course_prefix}_{zip_name}_{timestamp_str}.zip".format(
+        course_prefix=course_filename_prefix_generator(course_id),
+        zip_name=zip_name,
+        timestamp_str=timestamp.strftime("%Y-%m-%d-%H%M")
+    )
+
+    report_store.store(course_id, report_name, file)
+    tracker_emit(zip_name)
+    return report_name
+
+
 def tracker_emit(report_name):
     """
     Emits a 'report.requested' event for the given report.
diff --git a/lms/djangoapps/instructor_task/tests/test_api.py b/lms/djangoapps/instructor_task/tests/test_api.py
index 9d10f2e0408e3f3d1083fb0d0d621021e050d40e..1e386aa20c2bd1993ff1a8eacae0ef28a49163e6 100644
--- a/lms/djangoapps/instructor_task/tests/test_api.py
+++ b/lms/djangoapps/instructor_task/tests/test_api.py
@@ -27,6 +27,7 @@ from lms.djangoapps.instructor_task.api import (
     submit_delete_entrance_exam_state_for_student,
     submit_delete_problem_state_for_all_students,
     submit_export_ora2_data,
+    submit_export_ora2_submission_files,
     submit_override_score,
     submit_rescore_entrance_exam_for_student,
     submit_rescore_problem_for_all_students,
@@ -36,7 +37,7 @@ from lms.djangoapps.instructor_task.api import (
 )
 from lms.djangoapps.instructor_task.api_helper import AlreadyRunningError, QueueConnectionError
 from lms.djangoapps.instructor_task.models import PROGRESS, InstructorTask
-from lms.djangoapps.instructor_task.tasks import export_ora2_data
+from lms.djangoapps.instructor_task.tasks import export_ora2_data, export_ora2_submission_files
 from lms.djangoapps.instructor_task.tests.test_base import (
     TEST_COURSE_KEY,
     InstructorTaskCourseTestCase,
@@ -282,6 +283,22 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
             mock_submit_task.assert_called_once_with(
                 request, 'export_ora2_data', export_ora2_data, self.course.id, {}, '')
 
+    def test_submit_export_ora2_submission_files(self):
+        request = self.create_task_request(self.instructor)
+
+        with patch('lms.djangoapps.instructor_task.api.submit_task') as mock_submit_task:
+            mock_submit_task.return_value = MagicMock()
+            submit_export_ora2_submission_files(request, self.course.id)
+
+            mock_submit_task.assert_called_once_with(
+                request,
+                'export_ora2_submission_files',
+                export_ora2_submission_files,
+                self.course.id,
+                {},
+                ''
+            )
+
     def test_submit_generate_certs_students(self):
         """
         Tests certificates generation task submission api
diff --git a/lms/djangoapps/instructor_task/tests/test_tasks.py b/lms/djangoapps/instructor_task/tests/test_tasks.py
index 3f4fe877ac16d81d41430dcffa37e81fc9e98cd1..2f9a90630b1a6b31923b72764a8f6630e3bf3aba 100644
--- a/lms/djangoapps/instructor_task/tests/test_tasks.py
+++ b/lms/djangoapps/instructor_task/tests/test_tasks.py
@@ -25,6 +25,7 @@ from lms.djangoapps.instructor_task.models import InstructorTask
 from lms.djangoapps.instructor_task.tasks import (
     delete_problem_state,
     export_ora2_data,
+    export_ora2_submission_files,
     generate_certificates,
     override_problem_score,
     rescore_problem,
@@ -684,3 +685,33 @@ class TestOra2ResponsesInstructorTask(TestInstructorTasks):
             assert args[0] == task_entry.id
             assert callable(args[1])
             assert args[2] == action_name
+
+
+class TestOra2ExportSubmissionFilesInstructorTask(TestInstructorTasks):
+    """Tests instructor task that exports ora2 submission files archive."""
+
+    def test_ora2_missing_current_task(self):
+        self._test_missing_current_task(export_ora2_submission_files)
+
+    def test_ora2_with_failure(self):
+        self._test_run_with_failure(export_ora2_submission_files, 'We expected this to fail')
+
+    def test_ora2_with_long_error_msg(self):
+        self._test_run_with_long_error_msg(export_ora2_submission_files)
+
+    def test_ora2_with_short_error_msg(self):
+        self._test_run_with_short_error_msg(export_ora2_submission_files)
+
+    def test_ora2_runs_task(self):
+        task_entry = self._create_input_entry()
+        task_xmodule_args = self._get_xmodule_instance_args()
+
+        with patch('lms.djangoapps.instructor_task.tasks.run_main_task') as mock_main_task:
+            export_ora2_submission_files(task_entry.id, task_xmodule_args)
+            action_name = ugettext_noop('compressed')
+
+            assert mock_main_task.call_count == 1
+            args = mock_main_task.call_args[0]
+            assert args[0] == task_entry.id
+            assert callable(args[1])
+            assert args[2] == action_name
diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py
index 87761e0c773ca275f444997d728893991e532d6e..4e0e46e768fd9f046ae2ce89321edd03ce93f635 100644
--- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py
+++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py
@@ -12,8 +12,10 @@ Unit tests for LMS instructor-initiated background tasks helper functions.
 import os
 import shutil
 import tempfile
-from contextlib import contextmanager
+from contextlib import contextmanager, ExitStack
 from datetime import datetime, timedelta
+from io import BytesIO
+from zipfile import ZipFile
 
 import ddt
 import unicodecsv
@@ -57,7 +59,8 @@ from lms.djangoapps.instructor_task.tasks_helper.grades import (
 from lms.djangoapps.instructor_task.tasks_helper.misc import (
     cohort_students_and_upload,
     upload_course_survey_report,
-    upload_ora2_data
+    upload_ora2_data,
+    upload_ora2_submission_files
 )
 from lms.djangoapps.instructor_task.tests.test_base import (
     InstructorTaskCourseTestCase,
@@ -2539,25 +2542,126 @@ class TestInstructorOra2Report(SharedModuleStoreTestCase):
                 self.assertEqual(response, UPDATE_STATUS_FAILED)
 
     def test_report_stores_results(self):
-        with freeze_time('2001-01-01 00:00:00'):
+        with ExitStack() as stack:
+            stack.enter_context(freeze_time('2001-01-01 00:00:00'))
+
+            mock_current_task = stack.enter_context(
+                patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task')
+            )
+            mock_collect_data = stack.enter_context(
+                patch('lms.djangoapps.instructor_task.tasks_helper.misc.OraAggregateData.collect_ora2_data')
+            )
+            mock_store_rows = stack.enter_context(
+                patch('lms.djangoapps.instructor_task.models.DjangoStorageReportStore.store_rows')
+            )
+
+            mock_current_task.return_value = self.current_task
+
             test_header = ['field1', 'field2']
             test_rows = [['row1_field1', 'row1_field2'], ['row2_field1', 'row2_field2']]
 
+            mock_collect_data.return_value = (test_header, test_rows)
+
+            return_val = upload_ora2_data(None, None, self.course.id, None, 'generated')
+
+            timestamp_str = datetime.now(UTC).strftime('%Y-%m-%d-%H%M')
+            course_id_string = quote(text_type(self.course.id).replace('/', '_'))
+            filename = u'{}_ORA_data_{}.csv'.format(course_id_string, timestamp_str)
+
+            self.assertEqual(return_val, UPDATE_STATUS_SUCCEEDED)
+            mock_store_rows.assert_called_once_with(self.course.id, filename, [test_header] + test_rows)
+
+
+class TestInstructorOra2AttachmentsExport(SharedModuleStoreTestCase):
+    """
+    Tests that ORA2 submission files export works.
+    """
+
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls.course = CourseFactory.create()
+
+    def setUp(self):
+        super().setUp()
+
+        self.current_task = Mock()
+        self.current_task.update_state = Mock()
+
+    def test_export_fails_if_error_on_collect_step(self):
         with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task') as mock_current_task:
             mock_current_task.return_value = self.current_task
 
             with patch(
-                'lms.djangoapps.instructor_task.tasks_helper.misc.OraAggregateData.collect_ora2_data'
+                'lms.djangoapps.instructor_task.tasks_helper.misc.OraDownloadData.collect_ora2_submission_files'
             ) as mock_collect_data:
-                mock_collect_data.return_value = (test_header, test_rows)
-                with patch(
-                    'lms.djangoapps.instructor_task.models.DjangoStorageReportStore.store_rows'
-                ) as mock_store_rows:
-                    return_val = upload_ora2_data(None, None, self.course.id, None, 'generated')
-
-                    timestamp_str = datetime.now(UTC).strftime('%Y-%m-%d-%H%M')
-                    course_id_string = quote(text_type(self.course.id).replace('/', '_'))
-                    filename = u'{}_ORA_data_{}.csv'.format(course_id_string, timestamp_str)
-
-                    self.assertEqual(return_val, UPDATE_STATUS_SUCCEEDED)
-                    mock_store_rows.assert_called_once_with(self.course.id, filename, [test_header] + test_rows)
+                mock_collect_data.side_effect = KeyError
+
+                response = upload_ora2_submission_files(None, None, self.course.id, None, 'compressed')
+                self.assertEqual(response, UPDATE_STATUS_FAILED)
+
+    def test_export_fails_if_error_on_create_zip_step(self):
+        with ExitStack() as stack:
+            mock_current_task = stack.enter_context(
+                patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task')
+            )
+            mock_current_task.return_value = self.current_task
+
+            stack.enter_context(
+                patch('lms.djangoapps.instructor_task.tasks_helper.misc.OraDownloadData.collect_ora2_submission_files')
+            )
+            create_zip_mock = stack.enter_context(
+                patch('lms.djangoapps.instructor_task.tasks_helper.misc.OraDownloadData.create_zip_with_attachments')
+            )
+
+            create_zip_mock.side_effect = KeyError
+
+            response = upload_ora2_submission_files(None, None, self.course.id, None, 'compressed')
+            self.assertEqual(response, UPDATE_STATUS_FAILED)
+
+    def test_export_fails_if_error_on_upload_step(self):
+        with ExitStack() as stack:
+            mock_current_task = stack.enter_context(
+                patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task')
+            )
+            mock_current_task.return_value = self.current_task
+
+            stack.enter_context(
+                patch('lms.djangoapps.instructor_task.tasks_helper.misc.OraDownloadData.collect_ora2_submission_files')
+            )
+            stack.enter_context(
+                patch('lms.djangoapps.instructor_task.tasks_helper.misc.OraDownloadData.create_zip_with_attachments')
+            )
+            upload_mock = stack.enter_context(
+                patch('lms.djangoapps.instructor_task.tasks_helper.misc.upload_zip_to_report_store')
+            )
+
+            upload_mock.side_effect = KeyError
+
+            response = upload_ora2_submission_files(None, None, self.course.id, None, 'compressed')
+            self.assertEqual(response, UPDATE_STATUS_FAILED)
+
+    def test_task_stores_zip_with_attachments(self):
+        with ExitStack() as stack:
+            mock_current_task = stack.enter_context(
+                patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task')
+            )
+            mock_collect_files = stack.enter_context(
+                patch('lms.djangoapps.instructor_task.tasks_helper.misc.OraDownloadData.collect_ora2_submission_files')
+            )
+            mock_create_zip = stack.enter_context(
+                patch('lms.djangoapps.instructor_task.tasks_helper.misc.OraDownloadData.create_zip_with_attachments')
+            )
+            mock_store = stack.enter_context(
+                patch('lms.djangoapps.instructor_task.models.DjangoStorageReportStore.store')
+            )
+
+            mock_current_task.return_value = self.current_task
+
+            response = upload_ora2_submission_files(None, None, self.course.id, None, 'compressed')
+
+            mock_collect_files.assert_called_once()
+            mock_create_zip.assert_called_once()
+            mock_store.assert_called_once()
+
+            self.assertEqual(response, UPDATE_STATUS_SUCCEEDED)
diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download.html b/lms/templates/instructor/instructor_dashboard_2/data_download.html
index 051bb48ad179396d7d7ec2100024bf96d6f6e3ab..67bc40dba005bdf64c275c971c2e83b62cdf8728 100644
--- a/lms/templates/instructor/instructor_dashboard_2/data_download.html
+++ b/lms/templates/instructor/instructor_dashboard_2/data_download.html
@@ -94,6 +94,11 @@ from openedx.core.djangolib.markup import HTML, Text
       <input type="button" name="problem-grade-report" class="async-report-btn" value="${_("Generate Problem Grade Report")}" data-endpoint="${ section_data['problem_grade_report_url'] }"/>
       <input type="button" name="export-ora2-data" class="async-report-btn" value="${_("Generate ORA Data Report")}" data-endpoint="${ section_data['export_ora2_data_url'] }"/>
     </p>
+
+    <p>${_("Click to generate a ZIP file that contains all submission texts and attachments.")}</p>
+
+    <p><input type="button" name="export-ora2-data" class="async-report-btn" value="${_("Generate Submission Files Archive")}" data-endpoint="${ section_data['export_ora2_submission_files_url'] }"/></p>
+
   %endif
 
     <div class="request-response msg msg-confirm copy" id="report-request-response"></div>