Commit 25169ccc by Sarina Canelake

Merge pull request #9147 from open-craft/OC-791-answer-export

Move problem responses export from legacy instructor dash to new instructor dash
parents bb1631b2 df5b2dad
......@@ -4,6 +4,7 @@ Unit tests for instructor.api methods.
"""
import datetime
import ddt
import functools
import random
import pytz
import io
......@@ -28,6 +29,7 @@ from mock import Mock, patch
from nose.tools import raises
from nose.plugins.attrib import attr
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import UsageKey
from course_modes.models import CourseMode
from courseware.models import StudentModule
......@@ -107,6 +109,12 @@ REPORTS_DATA = (
'instructor_api_endpoint': 'get_proctored_exam_results',
'task_api_endpoint': 'instructor_task.api.submit_proctored_exam_results_report',
'extra_instructor_api_kwargs': {},
},
{
'report_type': 'problem responses',
'instructor_api_endpoint': 'get_problem_responses',
'task_api_endpoint': 'instructor_task.api.submit_calculate_problem_responses_csv',
'extra_instructor_api_kwargs': {},
}
)
......@@ -234,6 +242,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
('get_students_who_may_enroll', {}),
('get_exec_summary_report', {}),
('get_proctored_exam_results', {}),
('get_problem_responses', {}),
]
# Endpoints that only Instructors can access
self.instructor_level_endpoints = [
......@@ -286,6 +295,20 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
"Student should not be allowed to access endpoint " + endpoint
)
def _access_problem_responses_endpoint(self, msg):
"""
Access endpoint for problem responses report, ensuring that
UsageKey.from_string returns a problem key that the endpoint
can work with.
msg: message to display if assertion fails.
"""
mock_problem_key = Mock(return_value=u'')
mock_problem_key.course_key = self.course.id
with patch.object(UsageKey, 'from_string') as patched_method:
patched_method.return_value = mock_problem_key
self._access_endpoint('get_problem_responses', {}, 200, msg)
def test_staff_level(self):
"""
Ensure that a staff member can't access instructor endpoints.
......@@ -301,6 +324,11 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
# TODO: make these work
if endpoint in ['update_forum_role_membership', 'list_forum_members']:
continue
elif endpoint == 'get_problem_responses':
self._access_problem_responses_endpoint(
"Staff member should be allowed to access endpoint " + endpoint
)
continue
self._access_endpoint(
endpoint,
args,
......@@ -330,6 +358,11 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
# TODO: make these work
if endpoint in ['update_forum_role_membership']:
continue
elif endpoint == 'get_problem_responses':
self._access_problem_responses_endpoint(
"Instructor should be allowed to access endpoint " + endpoint
)
continue
self._access_endpoint(
endpoint,
args,
......@@ -2288,6 +2321,78 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
self.assertEqual(res['total_used_codes'], used_codes)
self.assertEqual(res['total_codes'], 5)
def test_get_problem_responses_invalid_location(self):
"""
Test whether get_problem_responses returns an appropriate status
message when users submit an invalid problem location.
"""
url = reverse(
'get_problem_responses',
kwargs={'course_id': unicode(self.course.id)}
)
problem_location = ''
response = self.client.get(url, {'problem_location': problem_location})
res_json = json.loads(response.content)
self.assertEqual(res_json, 'Could not find problem with this location.')
def valid_problem_location(test): # pylint: disable=no-self-argument
"""
Decorator for tests that target get_problem_responses endpoint and
need to pretend user submitted a valid problem location.
"""
@functools.wraps(test)
def wrapper(self, *args, **kwargs):
"""
Run `test` method, ensuring that UsageKey.from_string returns a
problem key that the get_problem_responses endpoint can
work with.
"""
mock_problem_key = Mock(return_value=u'')
mock_problem_key.course_key = self.course.id
with patch.object(UsageKey, 'from_string') as patched_method:
patched_method.return_value = mock_problem_key
test(self, *args, **kwargs)
return wrapper
@valid_problem_location
def test_get_problem_responses_successful(self):
"""
Test whether get_problem_responses returns an appropriate status
message if CSV generation was started successfully.
"""
url = reverse(
'get_problem_responses',
kwargs={'course_id': unicode(self.course.id)}
)
problem_location = ''
response = self.client.get(url, {'problem_location': problem_location})
res_json = json.loads(response.content)
self.assertIn('status', res_json)
status = res_json['status']
self.assertIn('is being created', status)
self.assertNotIn('already in progress', status)
@valid_problem_location
def test_get_problem_responses_already_running(self):
"""
Test whether get_problem_responses returns an appropriate status
message if CSV generation is already in progress.
"""
url = reverse(
'get_problem_responses',
kwargs={'course_id': unicode(self.course.id)}
)
with patch('instructor_task.api.submit_calculate_problem_responses_csv') as submit_task_function:
error = AlreadyRunningError()
submit_task_function.side_effect = error
response = self.client.get(url, {})
res_json = json.loads(response.content)
self.assertIn('status', res_json)
self.assertIn('already in progress', res_json['status'])
def test_get_students_features(self):
"""
Test that some minimum of information is formatted
......@@ -2593,16 +2698,21 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
@ddt.data(*REPORTS_DATA)
@ddt.unpack
@valid_problem_location
def test_calculate_report_csv_success(self, report_type, instructor_api_endpoint, task_api_endpoint, extra_instructor_api_kwargs):
kwargs = {'course_id': unicode(self.course.id)}
kwargs.update(extra_instructor_api_kwargs)
url = reverse(instructor_api_endpoint, kwargs=kwargs)
CourseFinanceAdminRole(self.course.id).add_users(self.instructor)
with patch(task_api_endpoint):
response = self.client.get(url, {})
success_status = "The {report_type} report is being created.".format(report_type=report_type)
self.assertIn(success_status, response.content)
if report_type == 'problem responses':
with patch(task_api_endpoint):
response = self.client.get(url, {'problem_location': ''})
self.assertIn(success_status, response.content)
else:
CourseFinanceAdminRole(self.course.id).add_users(self.instructor)
with patch(task_api_endpoint):
response = self.client.get(url, {})
self.assertIn(success_status, response.content)
@ddt.data(*EXECUTIVE_SUMMARY_DATA)
@ddt.unpack
......
......@@ -35,7 +35,7 @@ from util.file import (
store_uploaded_file, course_and_time_based_filename_generator,
FileValidationException, UniversalNewlineIterator
)
from util.json_request import JsonResponse
from util.json_request import JsonResponse, JsonResponseBadRequest
from instructor.views.instructor_task_helpers import extract_email_features, extract_task_features
from microsite_configuration import microsite
......@@ -107,7 +107,7 @@ from .tools import (
bulk_email_is_enabled_for_course,
add_block_ids,
)
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys import InvalidKeyError
from openedx.core.djangoapps.course_groups.cohorts import is_course_cohorted
......@@ -890,6 +890,51 @@ def list_course_role_members(request, course_id):
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
def get_problem_responses(request, course_id):
"""
Initiate generation of a CSV file containing all student answers
to a given problem.
Responds with JSON
{"status": "... status message ..."}
if initiation is successful (or generation task is already running).
Responds with BadRequest if problem location is faulty.
"""
course_key = CourseKey.from_string(course_id)
problem_location = request.GET.get('problem_location', '')
try:
problem_key = UsageKey.from_string(problem_location)
# Are we dealing with an "old-style" problem location?
run = getattr(problem_key, 'run')
if not run:
problem_key = course_key.make_usage_key_from_deprecated_string(problem_location)
if problem_key.course_key != course_key:
raise InvalidKeyError(type(problem_key), problem_key)
except InvalidKeyError:
return JsonResponseBadRequest(_("Could not find problem with this location."))
try:
instructor_task.api.submit_calculate_problem_responses_csv(request, course_key, problem_location)
success_status = _(
"The problem responses report is being created."
" To view the status of the report, see Pending Tasks below."
)
return JsonResponse({"status": success_status})
except AlreadyRunningError:
already_running_status = _(
"A problem responses report generation task is already in progress. "
"Check the 'Pending Tasks' table for the status of the task. "
"When completed, the report will be available for download in the table below."
)
return JsonResponse({"status": already_running_status})
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
def get_grading_config(request, course_id):
"""
Respond with json which contains a html formatted grade summary.
......
......@@ -17,6 +17,8 @@ urlpatterns = patterns(
'instructor.views.api.modify_access', name="modify_access"),
url(r'^bulk_beta_modify_access$',
'instructor.views.api.bulk_beta_modify_access', name="bulk_beta_modify_access"),
url(r'^get_problem_responses$',
'instructor.views.api.get_problem_responses', name="get_problem_responses"),
url(r'^get_grading_config$',
'instructor.views.api.get_grading_config', name="get_grading_config"),
url(r'^get_students_features(?P<csv>/csv)?$',
......
......@@ -495,6 +495,7 @@ def _section_data_download(course, access):
'section_display_name': _('Data Download'),
'access': access,
'show_generate_proctored_exam_report_button': settings.FEATURES.get('ENABLE_PROCTORED_EXAMS', False),
'get_problem_responses_url': reverse('get_problem_responses', kwargs={'course_id': unicode(course_key)}),
'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': unicode(course_key)}),
'get_students_features_url': reverse('get_students_features', kwargs={'course_id': unicode(course_key)}),
'get_students_who_may_enroll_url': reverse(
......
......@@ -277,35 +277,6 @@ def instructor_dashboard(request, course_id):
msg += msg2
#----------------------------------------
# DataDump
elif 'Download CSV of all responses to problem' in action:
problem_to_dump = request.POST.get('problem_to_dump', '')
if problem_to_dump[-4:] == ".xml":
problem_to_dump = problem_to_dump[:-4]
try:
module_state_key = course_key.make_usage_key_from_deprecated_string(problem_to_dump)
smdat = StudentModule.objects.filter(
course_id=course_key,
module_state_key=module_state_key
)
smdat = smdat.order_by('student')
msg += _("Found {num} records to dump.").format(num=smdat)
except Exception as err: # pylint: disable=broad-except
msg += "<font color='red'>{text}</font><pre>{err}</pre>".format(
text=_("Couldn't find module with that urlname."),
err=escape(err)
)
smdat = []
if smdat:
datatable = {'header': ['username', 'state']}
datatable['data'] = [[x.student.username, x.state] for x in smdat]
datatable['title'] = _('Student state for problem {problem}').format(problem=problem_to_dump)
return return_csv('student_state_from_{problem}.csv'.format(problem=problem_to_dump), datatable)
#----------------------------------------
# enrollment
elif action == 'Enroll multiple students':
......
......@@ -11,12 +11,14 @@ from shoppingcart.models import (
from django.db.models import Q
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
from opaque_keys.edx.keys import UsageKey
import xmodule.graders as xmgraders
from django.core.exceptions import ObjectDoesNotExist
from microsite_configuration import microsite
from student.models import CourseEnrollmentAllowed
from edx_proctoring.api import get_all_exam_attempts
from courseware.models import StudentModule
STUDENT_FEATURES = ('id', 'username', 'first_name', 'last_name', 'is_staff', 'email')
......@@ -317,6 +319,41 @@ def coupon_codes_features(features, coupons_list, course_id):
return [extract_coupon(coupon, features) for coupon in coupons_list]
def list_problem_responses(course_key, problem_location):
"""
Return responses to a given problem as a dict.
list_problem_responses(course_key, problem_location)
would return [
{'username': u'user1', 'state': u'...'},
{'username': u'user2', 'state': u'...'},
{'username': u'user3', 'state': u'...'},
]
where `state` represents a student's response to the problem
identified by `problem_location`.
"""
problem_key = UsageKey.from_string(problem_location)
# Are we dealing with an "old-style" problem location?
run = getattr(problem_key, 'run')
if not run:
problem_key = course_key.make_usage_key_from_deprecated_string(problem_location)
if problem_key.course_key != course_key:
return []
smdat = StudentModule.objects.filter(
course_id=course_key,
module_state_key=problem_key
)
smdat = smdat.order_by('student')
return [
{'username': response.student.username, 'state': response.state}
for response in smdat
]
def course_registration_features(features, registration_codes, csv_type):
"""
Return list of Course Registration Codes as dictionaries.
......
......@@ -2,27 +2,29 @@
Tests for instructor.basic
"""
import json
import datetime
from django.db.models import Q
import json
import pytz
from student.models import CourseEnrollment, CourseEnrollmentAllowed
from mock import MagicMock, Mock, patch
from django.core.urlresolvers import reverse
from mock import patch
from django.db.models import Q
from course_modes.models import CourseMode
from courseware.tests.factories import InstructorFactory
from instructor_analytics.basic import (
StudentModule, sale_record_features, sale_order_record_features, enrolled_students_features,
course_registration_features, coupon_codes_features, get_proctored_exam_results, list_may_enroll,
list_problem_responses, AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES
)
from opaque_keys.edx.locator import UsageKey
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
from student.models import CourseEnrollment, CourseEnrollmentAllowed
from student.roles import CourseSalesAdminRole
from student.tests.factories import UserFactory, CourseModeFactory
from shoppingcart.models import (
CourseRegistrationCode, RegistrationCodeRedemption, Order,
Invoice, Coupon, CourseRegCodeItem, CouponRedemption, CourseRegistrationCodeInvoiceItem
)
from course_modes.models import CourseMode
from instructor_analytics.basic import (
sale_record_features, sale_order_record_features, enrolled_students_features,
course_registration_features, coupon_codes_features, list_may_enroll,
AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES,
get_proctored_exam_results)
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
from courseware.tests.factories import InstructorFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from edx_proctoring.api import create_exam
......@@ -51,6 +53,48 @@ class TestAnalyticsBasic(ModuleStoreTestCase):
email=student.email, course_id=self.course_key
)
def test_list_problem_responses(self):
def result_factory(result_id):
"""
Return a dummy StudentModule object that can be queried for
relevant info (student.username and state).
"""
result = Mock(spec=['student', 'state'])
result.student.username.return_value = u'user{}'.format(result_id)
result.state.return_value = u'state{}'.format(result_id)
return result
# Ensure that UsageKey.from_string returns a problem key that list_problem_responses can work with
# (even when called with a dummy location):
mock_problem_key = Mock(return_value=u'')
mock_problem_key.course_key = self.course_key
with patch.object(UsageKey, 'from_string') as patched_from_string:
patched_from_string.return_value = mock_problem_key
# Ensure that StudentModule.objects.filter returns a result set that list_problem_responses can work with
# (this keeps us from having to create fixtures for this test):
mock_results = MagicMock(return_value=[result_factory(n) for n in range(5)])
with patch.object(StudentModule, 'objects') as patched_manager:
patched_manager.filter.return_value = mock_results
mock_problem_location = ''
problem_responses = list_problem_responses(self.course_key, problem_location=mock_problem_location)
# Check if list_problem_responses called UsageKey.from_string to look up problem key:
patched_from_string.assert_called_once_with(mock_problem_location)
# Check if list_problem_responses called StudentModule.objects.filter to obtain relevant records:
patched_manager.filter.assert_called_once_with(
course_id=self.course_key, module_state_key=mock_problem_key
)
# Check if list_problem_responses returned expected results:
self.assertEqual(len(problem_responses), len(mock_results))
for mock_result in mock_results:
self.assertTrue(
{'username': mock_result.student.username, 'state': mock_result.state} in
problem_responses
)
def test_enrolled_students_features_username(self):
self.assertIn('username', AVAILABLE_FEATURES)
userreports = enrolled_students_features(self.course_key, ['username'])
......
......@@ -18,6 +18,7 @@ from instructor_task.tasks import (
reset_problem_attempts,
delete_problem_state,
send_bulk_course_email,
calculate_problem_responses_csv,
calculate_grades_csv,
calculate_problem_grade_report,
calculate_students_features_csv,
......@@ -328,6 +329,21 @@ def submit_bulk_course_email(request, course_key, email_id):
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
def submit_calculate_problem_responses_csv(request, course_key, problem_location): # pylint: disable=invalid-name
"""
Submits a task to generate a CSV file containing all student
answers to a given problem.
Raises AlreadyRunningError if said file is already being updated.
"""
task_type = 'problem_responses_csv'
task_class = calculate_problem_responses_csv
task_input = {'problem_location': problem_location}
task_key = ""
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
def submit_calculate_grades_csv(request, course_key):
"""
AlreadyRunningError is raised if the course's grades are already being updated.
......
......@@ -34,6 +34,7 @@ from instructor_task.tasks_helper import (
rescore_problem_module_state,
reset_attempts_module_state,
delete_problem_module_state,
upload_problem_responses_csv,
upload_grades_csv,
upload_problem_grade_report,
upload_students_csv,
......@@ -146,6 +147,18 @@ def send_bulk_course_email(entry_id, _xmodule_instance_args):
@task(base=BaseInstructorTask, routing_key=settings.GRADES_DOWNLOAD_ROUTING_KEY) # pylint: disable=not-callable
def calculate_problem_responses_csv(entry_id, xmodule_instance_args):
"""
Compute student answers to a given problem and upload the CSV to
an S3 bucket for download.
"""
# Translators: This is a past-tense verb that is inserted into task progress messages as {action}.
action_name = ugettext_noop('generated')
task_fn = partial(upload_problem_responses_csv, xmodule_instance_args)
return run_main_task(entry_id, task_fn, action_name)
@task(base=BaseInstructorTask, routing_key=settings.GRADES_DOWNLOAD_ROUTING_KEY) # pylint: disable=not-callable
def calculate_grades_csv(entry_id, xmodule_instance_args):
"""
Grade a course and push the results to an S3 bucket for download.
......
......@@ -4,6 +4,7 @@ running state of a course.
"""
import json
import re
from collections import OrderedDict
from datetime import datetime
from django.conf import settings
......@@ -46,7 +47,12 @@ from courseware.grades import iterate_grades_for
from courseware.models import StudentModule
from courseware.model_data import DjangoKeyValueStore, FieldDataCache
from courseware.module_render import get_module_for_descriptor_internal
from instructor_analytics.basic import enrolled_students_features, list_may_enroll, get_proctored_exam_results
from instructor_analytics.basic import (
enrolled_students_features,
get_proctored_exam_results,
list_may_enroll,
list_problem_responses
)
from instructor_analytics.csvs import format_dictlist
from instructor_task.models import ReportStore, InstructorTask, PROGRESS
from lms.djangoapps.lms_xblock.runtime import LmsPartitionService
......@@ -849,6 +855,40 @@ def _order_problems(blocks):
return problems
def upload_problem_responses_csv(_xmodule_instance_args, _entry_id, course_id, task_input, action_name):
"""
For a given `course_id`, generate a CSV file containing
all student answers to a given problem, and store using a `ReportStore`.
"""
start_time = time()
start_date = datetime.now(UTC)
num_reports = 1
task_progress = TaskProgress(action_name, num_reports, start_time)
current_step = {'step': 'Calculating students answers to problem'}
task_progress.update_task_state(extra_meta=current_step)
# Compute result table and format it
problem_location = task_input.get('problem_location')
student_data = list_problem_responses(course_id, problem_location)
features = ['username', 'state']
header, rows = format_dictlist(student_data, features)
task_progress.attempted = task_progress.succeeded = len(rows)
task_progress.skipped = task_progress.total - task_progress.attempted
rows.insert(0, header)
current_step = {'step': 'Uploading CSV'}
task_progress.update_task_state(extra_meta=current_step)
# Perform the upload
problem_location = re.sub(r'[:/]', '_', problem_location)
csv_name = 'student_state_from_{}'.format(problem_location)
upload_csv_to_report_store(rows, csv_name, course_id, start_date)
return task_progress.update_task_state(extra_meta=current_step)
def upload_problem_grade_report(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name):
"""
Generate a CSV containing all students' problem grades within a given
......
......@@ -14,6 +14,7 @@ from instructor_task.api import (
submit_reset_problem_attempts_for_all_students,
submit_delete_problem_state_for_all_students,
submit_bulk_course_email,
submit_calculate_problem_responses_csv,
submit_calculate_students_features_csv,
submit_cohort_students,
submit_detailed_enrollment_features_csv,
......@@ -203,6 +204,14 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
)
self._test_resubmission(api_call)
def test_submit_calculate_problem_responses(self):
api_call = lambda: submit_calculate_problem_responses_csv(
self.create_task_request(self.instructor),
self.course.id,
problem_location=''
)
self._test_resubmission(api_call)
def test_submit_calculate_students_features(self):
api_call = lambda: submit_calculate_students_features_csv(
self.create_task_request(self.instructor),
......
......@@ -33,6 +33,7 @@ from xmodule.partitions.partitions import Group, UserPartition
from instructor_task.models import ReportStore
from instructor_task.tasks_helper import (
cohort_students_and_upload,
upload_problem_responses_csv,
upload_grades_csv,
upload_problem_grade_report,
upload_students_csv,
......@@ -277,6 +278,32 @@ class TestInstructorGradeReport(TestReportMixin, InstructorTaskCourseTestCase):
self.assertDictContainsSubset({'attempted': 1, 'succeeded': 1, 'failed': 0}, result)
class TestProblemResponsesReport(TestReportMixin, InstructorTaskCourseTestCase):
"""
Tests that generation of CSV files listing student answers to a
given problem works.
"""
def setUp(self):
super(TestProblemResponsesReport, self).setUp()
self.course = CourseFactory.create()
def test_success(self):
task_input = {'problem_location': ''}
with patch('instructor_task.tasks_helper._get_current_task'):
with patch('instructor_task.tasks_helper.list_problem_responses') as patched_data_source:
patched_data_source.return_value = [
{'username': 'user0', 'state': u'state0'},
{'username': 'user1', 'state': u'state1'},
{'username': 'user2', 'state': u'state2'},
]
result = upload_problem_responses_csv(None, None, self.course.id, task_input, 'calculated')
report_store = ReportStore.from_config(config_name='GRADES_DOWNLOAD')
links = report_store.links_for(self.course.id)
self.assertEquals(len(links), 1)
self.assertDictContainsSubset({'attempted': 3, 'succeeded': 3, 'failed': 0}, result)
@ddt.ddt
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
class TestInstructorDetailedEnrollmentReport(TestReportMixin, InstructorTaskCourseTestCase):
......
......@@ -22,6 +22,8 @@ class DataDownload
@$list_studs_csv_btn = @$section.find("input[name='list-profiles-csv']'")
@$list_proctored_exam_results_csv_btn = @$section.find("input[name='proctored-exam-results-report']'")
@$list_may_enroll_csv_btn = @$section.find("input[name='list-may-enroll-csv']")
@$list_problem_responses_csv_input = @$section.find("input[name='problem-location']")
@$list_problem_responses_csv_btn = @$section.find("input[name='list-problem-responses-csv']")
@$list_anon_btn = @$section.find("input[name='list-anon-ids']'")
@$grade_config_btn = @$section.find("input[name='dump-gradeconf']'")
@$calculate_grades_csv_btn = @$section.find("input[name='calculate-grades-csv']'")
......@@ -117,6 +119,22 @@ class DataDownload
grid = new Slick.Grid($table_placeholder, grid_data, columns, options)
# grid.autosizeColumns()
@$list_problem_responses_csv_btn.click (e) =>
@clear_display()
url = @$list_problem_responses_csv_btn.data 'endpoint'
$.ajax
dataType: 'json'
url: url
data:
problem_location: @$list_problem_responses_csv_input.val()
error: (std_ajax_err) =>
@$reports_request_response_error.text JSON.parse(std_ajax_err['responseText'])
$(".msg-error").css({"display":"block"})
success: (data) =>
@$reports_request_response.text data['status']
$(".msg-confirm").css({"display":"block"})
@$list_may_enroll_csv_btn.click (e) =>
@clear_display()
......
......@@ -361,9 +361,8 @@ function goto( mode)
%if modeflag.get('Data'):
<hr width="40%" style="align:left">
<p> ${_("Problem urlname:")}
<input type="text" name="problem_to_dump" size="40">
<input type="submit" name="action" value="Download CSV of all responses to problem">
<p class="is-deprecated">
${_("To download a CSV listing student responses to a given problem, visit the Data Download section of the Instructor Dashboard.")}
</p>
<p class="is-deprecated">
......
......@@ -39,6 +39,19 @@
<p>${_("Click to generate a CSV file of all proctored exam results in this course.")}</p>
<p><input type="button" name="proctored-exam-results-report" value="${_("Generate Proctored Exam Results Report")}" data-endpoint="${ section_data['list_proctored_results_url'] }"/></p>
%endif
<p>${_("To generate a CSV file that lists all student answers to a given problem, enter the location of the problem (from its Staff Debug Info).")}</p>
<p>
<label>
<span>${_("Problem location: ")}</span>
<input type="text" name="problem-location" />
</label>
</p>
<p>
<input type="button" name="list-problem-responses-csv" value="${_("Download a CSV of problem responses")}" data-endpoint="${ section_data['get_problem_responses_url'] }" data-csv="true">
</p>
% if not disable_buttons:
<p>${_("For smaller courses, click to list profile information for enrolled students directly on this page:")}</p>
<p><input type="button" name="list-profiles" value="${_("List enrolled students' profile information")}" data-endpoint="${ section_data['get_students_features_url'] }"></p>
......@@ -54,7 +67,7 @@
%endif
<div class="request-response msg msg-confirm copy" id="report-request-response"></div>
<div class="request-response-error msg msg-warning copy" id="report-request-response-error"></div>
<div class="request-response-error msg msg-error copy" id="report-request-response-error"></div>
<br>
<p><b>${_("Reports Available for Download")}</b></p>
......
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