Merge pull request #19903 from edx/revert-19635-giovanni/bb-728-add-problem-response-report-api-upstream
Revert "[BB-728] Add problem response report API"
This commit is contained in:
@@ -1,25 +0,0 @@
|
||||
"""
|
||||
Instructor permissions for class based views
|
||||
"""
|
||||
|
||||
from django.http import Http404
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys import InvalidKeyError
|
||||
from rest_framework import permissions
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.courses import get_course_by_id
|
||||
|
||||
|
||||
class IsCourseStaff(permissions.BasePermission):
|
||||
"""
|
||||
Check if the requesting user is a course's staff member
|
||||
"""
|
||||
def has_permission(self, request, view):
|
||||
try:
|
||||
course_key = CourseKey.from_string(view.kwargs.get('course_id'))
|
||||
except InvalidKeyError:
|
||||
raise Http404()
|
||||
|
||||
course = get_course_by_id(course_key)
|
||||
return has_access(request.user, 'staff', course)
|
||||
@@ -47,7 +47,6 @@ from courseware.tests.factories import (
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from django_comment_common.models import FORUM_ROLE_COMMUNITY_TA
|
||||
from django_comment_common.utils import seed_permissions_roles
|
||||
from edx_oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory
|
||||
from lms.djangoapps.instructor.tests.utils import FakeContentTask, FakeEmail, FakeEmailInfo
|
||||
from lms.djangoapps.instructor.views.api import (
|
||||
_split_input_list,
|
||||
@@ -142,7 +141,7 @@ REPORTS_DATA = (
|
||||
},
|
||||
{
|
||||
'report_type': 'problem responses',
|
||||
'instructor_api_endpoint': 'api_instructor:get_problem_responses',
|
||||
'instructor_api_endpoint': 'get_problem_responses',
|
||||
'task_api_endpoint': 'lms.djangoapps.instructor_task.api.submit_calculate_problem_responses_csv',
|
||||
'extra_instructor_api_kwargs': {},
|
||||
}
|
||||
@@ -178,7 +177,7 @@ INSTRUCTOR_POST_ENDPOINTS = set([
|
||||
'get_enrollment_report',
|
||||
'get_exec_summary_report',
|
||||
'get_grading_config',
|
||||
'api_instructor:get_problem_responses',
|
||||
'get_problem_responses',
|
||||
'get_proctored_exam_results',
|
||||
'get_registration_codes',
|
||||
'get_student_enrollment_status',
|
||||
@@ -192,8 +191,8 @@ INSTRUCTOR_POST_ENDPOINTS = set([
|
||||
'list_entrance_exam_instructor_tasks',
|
||||
'list_financial_report_downloads',
|
||||
'list_forum_members',
|
||||
'api_instructor:list_instructor_tasks',
|
||||
'api_instructor:list_report_downloads',
|
||||
'list_instructor_tasks',
|
||||
'list_report_downloads',
|
||||
'mark_student_can_skip_entrance_exam',
|
||||
'modify_access',
|
||||
'register_and_enroll_students',
|
||||
@@ -449,9 +448,9 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
{'unique_student_identifier': self.user.email, 'rolename': 'Moderator', 'action': 'allow'}),
|
||||
('list_forum_members', {'rolename': FORUM_ROLE_COMMUNITY_TA}),
|
||||
('send_email', {'send_to': '["staff"]', 'subject': 'test', 'message': 'asdf'}),
|
||||
('api_instructor:list_instructor_tasks', {}),
|
||||
('list_instructor_tasks', {}),
|
||||
('list_background_email_tasks', {}),
|
||||
('api_instructor:list_report_downloads', {}),
|
||||
('list_report_downloads', {}),
|
||||
('list_financial_report_downloads', {}),
|
||||
('calculate_grades_csv', {}),
|
||||
('get_students_features', {}),
|
||||
@@ -459,7 +458,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
('get_students_who_may_enroll', {}),
|
||||
('get_exec_summary_report', {}),
|
||||
('get_proctored_exam_results', {}),
|
||||
('api_instructor:get_problem_responses', {}),
|
||||
('get_problem_responses', {}),
|
||||
('export_ora2_data', {}),
|
||||
('rescore_problem',
|
||||
{'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.user.email}),
|
||||
@@ -539,7 +538,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
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('api_instructor:get_problem_responses', {}, 200, msg)
|
||||
self._access_endpoint('get_problem_responses', {}, 200, msg)
|
||||
|
||||
def test_staff_level(self):
|
||||
"""
|
||||
@@ -558,7 +557,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
# TODO: make these work
|
||||
if endpoint in ['update_forum_role_membership', 'list_forum_members']:
|
||||
continue
|
||||
elif endpoint == 'api_instructor:get_problem_responses':
|
||||
elif endpoint == 'get_problem_responses':
|
||||
self._access_problem_responses_endpoint(
|
||||
"Staff member should be allowed to access endpoint " + endpoint
|
||||
)
|
||||
@@ -594,7 +593,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
# TODO: make these work
|
||||
if endpoint in ['update_forum_role_membership']:
|
||||
continue
|
||||
elif endpoint == 'api_instructor:get_problem_responses':
|
||||
elif endpoint == 'get_problem_responses':
|
||||
self._access_problem_responses_endpoint(
|
||||
"Instructor should be allowed to access endpoint " + endpoint
|
||||
)
|
||||
@@ -2899,7 +2898,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
|
||||
message when users submit an invalid problem location.
|
||||
"""
|
||||
url = reverse(
|
||||
'api_instructor:get_problem_responses',
|
||||
'get_problem_responses',
|
||||
kwargs={'course_id': unicode(self.course.id)}
|
||||
)
|
||||
problem_location = ''
|
||||
@@ -2934,7 +2933,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
|
||||
message if CSV generation was started successfully.
|
||||
"""
|
||||
url = reverse(
|
||||
'api_instructor:get_problem_responses',
|
||||
'get_problem_responses',
|
||||
kwargs={'course_id': unicode(self.course.id)}
|
||||
)
|
||||
problem_location = ''
|
||||
@@ -2954,7 +2953,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
|
||||
message if CSV generation is already in progress.
|
||||
"""
|
||||
url = reverse(
|
||||
'api_instructor:get_problem_responses',
|
||||
'get_problem_responses',
|
||||
kwargs={'course_id': unicode(self.course.id)}
|
||||
)
|
||||
task_type = 'problem_responses_csv'
|
||||
@@ -3281,7 +3280,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
|
||||
"""
|
||||
ex_status = 503
|
||||
ex_reason = 'Slow Down'
|
||||
url = reverse('api_instructor:list_report_downloads', kwargs={'course_id': text_type(self.course.id)})
|
||||
url = reverse('list_report_downloads', kwargs={'course_id': text_type(self.course.id)})
|
||||
with patch('openedx.core.storage.S3ReportStorage.listdir', side_effect=BotoServerError(ex_status, ex_reason)):
|
||||
response = self.client.post(url, {})
|
||||
mock_error.assert_called_with(
|
||||
@@ -3295,7 +3294,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
|
||||
self.assertEqual(res_json, {"downloads": []})
|
||||
|
||||
def test_list_report_downloads(self):
|
||||
url = reverse('api_instructor:list_report_downloads', kwargs={'course_id': text_type(self.course.id)})
|
||||
url = reverse('list_report_downloads', kwargs={'course_id': text_type(self.course.id)})
|
||||
with patch('lms.djangoapps.instructor_task.models.DjangoStorageReportStore.links_for') as mock_links_for:
|
||||
mock_links_for.return_value = [
|
||||
('mock_file_name_1', 'https://1.mock.url'),
|
||||
@@ -4100,7 +4099,7 @@ class TestInstructorAPITaskLists(SharedModuleStoreTestCase, LoginEnrollmentTestC
|
||||
def test_list_instructor_tasks_running(self, act):
|
||||
""" Test list of all running tasks. """
|
||||
act.return_value = self.tasks
|
||||
url = reverse('api_instructor:list_instructor_tasks', kwargs={'course_id': text_type(self.course.id)})
|
||||
url = reverse('list_instructor_tasks', kwargs={'course_id': text_type(self.course.id)})
|
||||
mock_factory = MockCompletionInfo()
|
||||
with patch(
|
||||
'lms.djangoapps.instructor.views.instructor_task_helpers.get_task_completion_info'
|
||||
@@ -4142,7 +4141,7 @@ class TestInstructorAPITaskLists(SharedModuleStoreTestCase, LoginEnrollmentTestC
|
||||
def test_list_instructor_tasks_problem(self, act):
|
||||
""" Test list task history for problem. """
|
||||
act.return_value = self.tasks
|
||||
url = reverse('api_instructor:list_instructor_tasks', kwargs={'course_id': text_type(self.course.id)})
|
||||
url = reverse('list_instructor_tasks', kwargs={'course_id': text_type(self.course.id)})
|
||||
mock_factory = MockCompletionInfo()
|
||||
with patch(
|
||||
'lms.djangoapps.instructor.views.instructor_task_helpers.get_task_completion_info'
|
||||
@@ -4165,7 +4164,7 @@ class TestInstructorAPITaskLists(SharedModuleStoreTestCase, LoginEnrollmentTestC
|
||||
def test_list_instructor_tasks_problem_student(self, act):
|
||||
""" Test list task history for problem AND student. """
|
||||
act.return_value = self.tasks
|
||||
url = reverse('api_instructor:list_instructor_tasks', kwargs={'course_id': text_type(self.course.id)})
|
||||
url = reverse('list_instructor_tasks', kwargs={'course_id': text_type(self.course.id)})
|
||||
mock_factory = MockCompletionInfo()
|
||||
with patch(
|
||||
'lms.djangoapps.instructor.views.instructor_task_helpers.get_task_completion_info'
|
||||
@@ -4187,88 +4186,6 @@ class TestInstructorAPITaskLists(SharedModuleStoreTestCase, LoginEnrollmentTestC
|
||||
self.assertEqual(actual_tasks, expected_tasks)
|
||||
|
||||
|
||||
class TestInstructorAPIOAuth(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
Test instructor API OAuth endpoint support.
|
||||
"""
|
||||
password = 'password'
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestInstructorAPIOAuth, cls).setUpClass()
|
||||
cls.course = CourseFactory.create(
|
||||
entrance_exam_id='i4x://{}/{}/chapter/Entrance_exam'.format('test_org', 'test_course')
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(TestInstructorAPIOAuth, self).setUp()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
|
||||
@patch.object(lms.djangoapps.instructor_task.api, 'get_running_instructor_tasks')
|
||||
def test_list_instructor_tasks_oauth(self, act):
|
||||
"""
|
||||
Test if list_instructor_tasks endpoints supports OAuth
|
||||
"""
|
||||
act.return_value = []
|
||||
url = reverse('api_instructor:list_instructor_tasks', kwargs={'course_id': unicode(self.course.id)})
|
||||
# OAuth Client
|
||||
oauth_client = ClientFactory.create()
|
||||
access_token = AccessTokenFactory.create(
|
||||
user=self.instructor,
|
||||
client=oauth_client
|
||||
).token
|
||||
headers = {
|
||||
'HTTP_AUTHORIZATION': 'Bearer ' + access_token
|
||||
}
|
||||
mock_factory = MockCompletionInfo()
|
||||
with patch(
|
||||
'lms.djangoapps.instructor.views.instructor_task_helpers.get_task_completion_info'
|
||||
) as mock_completion_info:
|
||||
mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info
|
||||
response = self.client.post(url, {}, **headers)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_get_problem_responses_oauth(self):
|
||||
"""
|
||||
Test whether get_problem_responses allows access via OAuth
|
||||
"""
|
||||
url = reverse('api_instructor:get_problem_responses', kwargs={'course_id': unicode(self.course.id)})
|
||||
problem_location = ''
|
||||
|
||||
# OAuth Client
|
||||
oauth_client = ClientFactory.create()
|
||||
access_token = AccessTokenFactory.create(
|
||||
user=self.instructor,
|
||||
client=oauth_client
|
||||
).token
|
||||
headers = {
|
||||
'HTTP_AUTHORIZATION': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
response = self.client.post(url, {'problem_location': problem_location}, **headers)
|
||||
# Http error 400 means Bad request, but our user was authorized
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_list_report_downloads_oauth(self):
|
||||
"""
|
||||
Test whether list_report_downloads allows access via OAuth
|
||||
"""
|
||||
url = reverse('api_instructor:list_report_downloads', kwargs={'course_id': unicode(self.course.id)})
|
||||
|
||||
# OAuth Client
|
||||
oauth_client = ClientFactory.create()
|
||||
access_token = AccessTokenFactory.create(
|
||||
user=self.instructor,
|
||||
client=oauth_client
|
||||
).token
|
||||
headers = {
|
||||
'HTTP_AUTHORIZATION': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
response = self.client.post(url, {}, **headers)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
@patch.object(lms.djangoapps.instructor_task.api, 'get_instructor_task_history', autospec=True)
|
||||
class TestInstructorEmailContentList(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
"""
|
||||
Instructor API endpoint new urls.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
|
||||
import lms.djangoapps.instructor.views.api
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(
|
||||
r'^v1/course/{}/tasks$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
lms.djangoapps.instructor.views.api.InstructorTasks.as_view(),
|
||||
name='list_instructor_tasks',
|
||||
),
|
||||
url(
|
||||
r'^v1/course/{}/reports$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
lms.djangoapps.instructor.views.api.ReportDownloadsList.as_view(),
|
||||
name='list_report_downloads',
|
||||
),
|
||||
url(
|
||||
r'^v1/course/{}/reports/problem_responses$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
lms.djangoapps.instructor.views.api.ProblemResponseReport.as_view(),
|
||||
name='get_problem_responses',
|
||||
),
|
||||
]
|
||||
@@ -41,7 +41,6 @@ from edx_rest_framework_extensions.auth.session.authentication import SessionAut
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from six import text_type
|
||||
@@ -82,7 +81,6 @@ from lms.djangoapps.instructor.enrollment import (
|
||||
send_mail_to_student,
|
||||
unenroll_email
|
||||
)
|
||||
from lms.djangoapps.instructor.permissions import IsCourseStaff
|
||||
from lms.djangoapps.instructor.views import INVOICE_KEY
|
||||
from lms.djangoapps.instructor.views.instructor_task_helpers import extract_email_features, extract_task_features
|
||||
from lms.djangoapps.instructor_task.api import submit_override_score
|
||||
@@ -996,67 +994,45 @@ def list_course_role_members(request, course_id):
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
class ProblemResponseReport(DeveloperErrorViewMixin, APIView):
|
||||
@transaction.non_atomic_requests
|
||||
@require_POST
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@common_exceptions_400
|
||||
def get_problem_responses(request, course_id):
|
||||
"""
|
||||
**Use Cases**
|
||||
Initiate generation of a CSV file containing all student answers
|
||||
to a given problem.
|
||||
|
||||
Initiate generation of a CSV file containing all student answers
|
||||
to a given problem.
|
||||
Responds with JSON
|
||||
{"status": "... status message ...", "task_id": created_task_UUID}
|
||||
|
||||
**Example Requests**:
|
||||
if initiation is successful (or generation task is already running).
|
||||
|
||||
POST /api/instructor/v1/course/{}/reports
|
||||
|
||||
**Response Values**
|
||||
{
|
||||
"task_id": "task_id"
|
||||
"status": "... status message ..."
|
||||
}
|
||||
Responds with BadRequest if problem location is faulty.
|
||||
Responds with BadRequest if problem location is faulty.
|
||||
"""
|
||||
authentication_classes = (JwtAuthentication, OAuth2AuthenticationAllowInactiveUser, SessionAuthentication,)
|
||||
permission_classes = (permissions.IsAuthenticated, IsCourseStaff)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
problem_location = request.POST.get('problem_location', '')
|
||||
report_type = _('problem responses')
|
||||
|
||||
# The non-atomic decorator is required because this view calls a celery
|
||||
# task which uses the 'outer_atomic' context manager.
|
||||
@method_decorator(transaction.non_atomic_requests)
|
||||
def dispatch(self, *args, **kwargs): # pylint: disable=W0221
|
||||
return super(ProblemResponseReport, self).dispatch(*args, **kwargs)
|
||||
try:
|
||||
problem_key = UsageKey.from_string(problem_location)
|
||||
# Are we dealing with an "old-style" problem location?
|
||||
run = problem_key.run
|
||||
if not run:
|
||||
problem_key = UsageKey.from_string(problem_location).map_into_course(course_key)
|
||||
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."))
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def post(self, request, course_id):
|
||||
"""
|
||||
Initiate generation of a CSV file containing all student answers
|
||||
to a given problem.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
problem_location = request.POST.get('problem_location', '')
|
||||
report_type = _('problem responses')
|
||||
task = lms.djangoapps.instructor_task.api.submit_calculate_problem_responses_csv(
|
||||
request, course_key, problem_location
|
||||
)
|
||||
success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type)
|
||||
|
||||
try:
|
||||
problem_key = UsageKey.from_string(problem_location)
|
||||
# Are we dealing with an "old-style" problem location?
|
||||
run = problem_key.run
|
||||
if not run:
|
||||
problem_key = problem_key.map_into_course(course_key)
|
||||
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:
|
||||
task = lms.djangoapps.instructor_task.api.submit_calculate_problem_responses_csv(
|
||||
request, course_key, problem_location
|
||||
)
|
||||
success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type)
|
||||
|
||||
return JsonResponse({
|
||||
"status": success_status,
|
||||
"task_id": task.task_id
|
||||
})
|
||||
except AlreadyRunningError as err:
|
||||
error_message = unicode(err)
|
||||
return JsonResponseBadRequest(error_message)
|
||||
return JsonResponse({"status": success_status, "task_id": task.task_id})
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -2425,84 +2401,50 @@ def list_email_content(request, course_id): # pylint: disable=unused-argument
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
class InstructorTasks(DeveloperErrorViewMixin, APIView):
|
||||
@require_POST
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
def list_instructor_tasks(request, course_id):
|
||||
"""
|
||||
**Use Cases**
|
||||
List instructor tasks.
|
||||
|
||||
Lists currently running instructor tasks
|
||||
|
||||
**Parameters**
|
||||
Takes optional query paremeters.
|
||||
- With no arguments, lists running tasks.
|
||||
- `problem_location_str` lists task history for problem
|
||||
- `problem_location_str` and `unique_student_identifier` lists task
|
||||
history for problem AND student (intersection)
|
||||
|
||||
**Example Requests**:
|
||||
|
||||
POST /api/instructor/v1/course/{}/tasks
|
||||
|
||||
**Response Values**
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"status": "Incomplete",
|
||||
"task_type": "grade_problems",
|
||||
"task_id": "2519ff31-22d9-4a62-91e2-55495895b355",
|
||||
"created": "2019-01-15T18:00:15.902470+00:00",
|
||||
"task_input": "{}",
|
||||
"duration_sec": "unknown",
|
||||
"task_message": "No status information available",
|
||||
"requester": "staff",
|
||||
"task_state": "PROGRESS"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
authentication_classes = (JwtAuthentication, OAuth2AuthenticationAllowInactiveUser, SessionAuthentication,)
|
||||
permission_classes = (permissions.IsAuthenticated, IsCourseStaff)
|
||||
course_id = CourseKey.from_string(course_id)
|
||||
problem_location_str = strip_if_string(request.POST.get('problem_location_str', False))
|
||||
student = request.POST.get('unique_student_identifier', None)
|
||||
if student is not None:
|
||||
student = get_student_from_identifier(student)
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def post(self, request, course_id):
|
||||
"""
|
||||
List instructor tasks.
|
||||
"""
|
||||
course_id = CourseKey.from_string(course_id)
|
||||
problem_location_str = strip_if_string(request.POST.get('problem_location_str', False))
|
||||
student = request.POST.get('unique_student_identifier', None)
|
||||
if student is not None:
|
||||
student = get_student_from_identifier(student)
|
||||
if student and not problem_location_str:
|
||||
return HttpResponseBadRequest(
|
||||
"unique_student_identifier must accompany problem_location_str"
|
||||
)
|
||||
|
||||
if student and not problem_location_str:
|
||||
return HttpResponseBadRequest(
|
||||
"unique_student_identifier must accompany problem_location_str"
|
||||
)
|
||||
|
||||
if problem_location_str:
|
||||
try:
|
||||
module_state_key = course_id.make_usage_key_from_deprecated_string(problem_location_str)
|
||||
except InvalidKeyError:
|
||||
return HttpResponseBadRequest()
|
||||
if student:
|
||||
# Specifying for a single student's history on this problem
|
||||
tasks = lms.djangoapps.instructor_task.api.get_instructor_task_history(
|
||||
course_id,
|
||||
module_state_key,
|
||||
student
|
||||
)
|
||||
else:
|
||||
# Specifying for single problem's history
|
||||
tasks = lms.djangoapps.instructor_task.api.get_instructor_task_history(
|
||||
course_id,
|
||||
module_state_key
|
||||
)
|
||||
if problem_location_str:
|
||||
try:
|
||||
module_state_key = UsageKey.from_string(problem_location_str).map_into_course(course_id)
|
||||
except InvalidKeyError:
|
||||
return HttpResponseBadRequest()
|
||||
if student:
|
||||
# Specifying for a single student's history on this problem
|
||||
tasks = lms.djangoapps.instructor_task.api.get_instructor_task_history(course_id, module_state_key, student)
|
||||
else:
|
||||
# If no problem or student, just get currently running tasks
|
||||
tasks = lms.djangoapps.instructor_task.api.get_running_instructor_tasks(course_id)
|
||||
# Specifying for single problem's history
|
||||
tasks = lms.djangoapps.instructor_task.api.get_instructor_task_history(course_id, module_state_key)
|
||||
else:
|
||||
# If no problem or student, just get currently running tasks
|
||||
tasks = lms.djangoapps.instructor_task.api.get_running_instructor_tasks(course_id)
|
||||
|
||||
response_payload = {
|
||||
'tasks': map(extract_task_features, tasks),
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
response_payload = {
|
||||
'tasks': map(extract_task_features, tasks),
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -2547,49 +2489,28 @@ def list_entrance_exam_instructor_tasks(request, course_id):
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
class ReportDownloadsList(DeveloperErrorViewMixin, APIView):
|
||||
@require_POST
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
def list_report_downloads(request, course_id):
|
||||
"""
|
||||
**Use Cases**
|
||||
List grade CSV files that are available for download for this course.
|
||||
|
||||
Lists reports available for download
|
||||
|
||||
**Example Requests**:
|
||||
|
||||
POST /api/instructor/v1/course/{}/tasks
|
||||
|
||||
**Response Values**
|
||||
{
|
||||
"downloads": [
|
||||
{
|
||||
"url": "https://1.mock.url",
|
||||
"link": "<a href=\"https://1.mock.url\">mock_file_name_1</a>",
|
||||
"name": "mock_file_name_1"
|
||||
}
|
||||
]
|
||||
}
|
||||
Takes the following query parameters:
|
||||
- (optional) report_name - name of the report
|
||||
"""
|
||||
authentication_classes = (JwtAuthentication, OAuth2AuthenticationAllowInactiveUser, SessionAuthentication,)
|
||||
permission_classes = (permissions.IsAuthenticated, IsCourseStaff)
|
||||
course_id = CourseKey.from_string(course_id)
|
||||
report_store = ReportStore.from_config(config_name='GRADES_DOWNLOAD')
|
||||
report_name = request.POST.get("report_name", None)
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def post(self, request, course_id):
|
||||
"""
|
||||
List grade CSV files that are available for download for this course.
|
||||
|
||||
Takes the following query parameters:
|
||||
- (optional) report_name - name of the report
|
||||
"""
|
||||
course_id = CourseKey.from_string(course_id)
|
||||
report_store = ReportStore.from_config(config_name='GRADES_DOWNLOAD')
|
||||
report_name = request.POST.get("report_name", None)
|
||||
|
||||
response_payload = {
|
||||
'downloads': [
|
||||
dict(name=name, url=url, link=HTML(u'<a href="{}">{}</a>').format(HTML(url), Text(name)))
|
||||
for name, url in report_store.links_for(course_id) if report_name is None or name == report_name
|
||||
]
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
response_payload = {
|
||||
'downloads': [
|
||||
dict(name=name, url=url, link=HTML(u'<a href="{}">{}</a>').format(HTML(url), Text(name)))
|
||||
for name, url in report_store.links_for(course_id) if report_name is None or name == report_name
|
||||
]
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
@require_POST
|
||||
|
||||
@@ -12,6 +12,7 @@ urlpatterns = [
|
||||
url(r'^list_course_role_members$', api.list_course_role_members, name='list_course_role_members'),
|
||||
url(r'^modify_access$', api.modify_access, name='modify_access'),
|
||||
url(r'^bulk_beta_modify_access$', api.bulk_beta_modify_access, name='bulk_beta_modify_access'),
|
||||
url(r'^get_problem_responses$', api.get_problem_responses, name='get_problem_responses'),
|
||||
url(r'^get_grading_config$', api.get_grading_config, name='get_grading_config'),
|
||||
url(r'^get_students_features(?P<csv>/csv)?$', api.get_students_features, name='get_students_features'),
|
||||
url(r'^get_issued_certificates/$', api.get_issued_certificates, name='get_issued_certificates'),
|
||||
@@ -33,6 +34,7 @@ urlpatterns = [
|
||||
name='list_entrance_exam_instructor_tasks'),
|
||||
url(r'^mark_student_can_skip_entrance_exam', api.mark_student_can_skip_entrance_exam,
|
||||
name='mark_student_can_skip_entrance_exam'),
|
||||
url(r'^list_instructor_tasks$', api.list_instructor_tasks, name='list_instructor_tasks'),
|
||||
url(r'^list_background_email_tasks$', api.list_background_email_tasks, name='list_background_email_tasks'),
|
||||
url(r'^list_email_content$', api.list_email_content, name='list_email_content'),
|
||||
url(r'^list_forum_members$', api.list_forum_members, name='list_forum_members'),
|
||||
@@ -47,6 +49,7 @@ urlpatterns = [
|
||||
url(r'^get_proctored_exam_results$', api.get_proctored_exam_results, name='get_proctored_exam_results'),
|
||||
|
||||
# Grade downloads...
|
||||
url(r'^list_report_downloads$', api.list_report_downloads, name='list_report_downloads'),
|
||||
url(r'calculate_grades_csv$', api.calculate_grades_csv, name='calculate_grades_csv'),
|
||||
url(r'problem_grade_report$', api.problem_grade_report, name='problem_grade_report'),
|
||||
|
||||
@@ -88,21 +91,4 @@ urlpatterns = [
|
||||
url(r'^generate_bulk_certificate_exceptions', api.generate_bulk_certificate_exceptions,
|
||||
name='generate_bulk_certificate_exceptions'),
|
||||
url(r'^certificate_invalidation_view/$', api.certificate_invalidation_view, name='certificate_invalidation_view'),
|
||||
|
||||
# Instructor endpoints moved to the new API, kept here for backwards compatibility
|
||||
url(
|
||||
r'^list_instructor_tasks$',
|
||||
api.InstructorTasks.as_view(),
|
||||
name='list_instructor_tasks_old',
|
||||
),
|
||||
url(
|
||||
r'^get_problem_responses$',
|
||||
api.ProblemResponseReport.as_view(),
|
||||
name='get_problem_responses_old',
|
||||
),
|
||||
url(
|
||||
r'^list_report_downloads$',
|
||||
api.ReportDownloadsList.as_view(),
|
||||
name='list_report_downloads_old',
|
||||
),
|
||||
]
|
||||
|
||||
@@ -295,10 +295,7 @@ def _section_e_commerce(course, access, paid_mode, coupons_enabled, reports_enab
|
||||
'exec_summary_report_url': reverse('get_exec_summary_report', kwargs={'course_id': unicode(course_key)}),
|
||||
'list_financial_report_downloads_url': reverse('list_financial_report_downloads',
|
||||
kwargs={'course_id': unicode(course_key)}),
|
||||
'list_instructor_tasks_url': reverse(
|
||||
'api_instructor:list_instructor_tasks',
|
||||
kwargs={'course_id': unicode(course_key)}
|
||||
),
|
||||
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': unicode(course_key)}),
|
||||
'look_up_registration_code': reverse('look_up_registration_code', kwargs={'course_id': unicode(course_key)}),
|
||||
'coupons': coupons,
|
||||
'sales_admin': access['sales_admin'],
|
||||
@@ -397,7 +394,7 @@ def _section_certificates(course):
|
||||
kwargs={'course_id': course.id}
|
||||
),
|
||||
'list_instructor_tasks_url': reverse(
|
||||
'api_instructor:list_instructor_tasks',
|
||||
'list_instructor_tasks',
|
||||
kwargs={'course_id': course.id}
|
||||
),
|
||||
}
|
||||
@@ -457,10 +454,7 @@ def _section_course_info(course, access):
|
||||
'start_date': course.start,
|
||||
'end_date': course.end,
|
||||
'num_sections': len(course.children),
|
||||
'list_instructor_tasks_url': reverse(
|
||||
'api_instructor:list_instructor_tasks',
|
||||
kwargs={'course_id': unicode(course_key)}
|
||||
),
|
||||
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': unicode(course_key)}),
|
||||
}
|
||||
|
||||
if settings.FEATURES.get('DISPLAY_ANALYTICS_ENROLLMENTS'):
|
||||
@@ -594,10 +588,7 @@ def _section_student_admin(course, access):
|
||||
'mark_student_can_skip_entrance_exam',
|
||||
kwargs={'course_id': unicode(course_key)},
|
||||
),
|
||||
'list_instructor_tasks_url': reverse(
|
||||
'api_instructor:list_instructor_tasks',
|
||||
kwargs={'course_id': unicode(course_key)}
|
||||
),
|
||||
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': unicode(course_key)}),
|
||||
'list_entrace_exam_instructor_tasks_url': reverse('list_entrance_exam_instructor_tasks',
|
||||
kwargs={'course_id': unicode(course_key)}),
|
||||
'spoc_gradebook_url': reverse('spoc_gradebook', kwargs={'course_id': unicode(course_key)}),
|
||||
@@ -636,10 +627,7 @@ def _section_data_download(course, access):
|
||||
'section_display_name': _('Data Download'),
|
||||
'access': access,
|
||||
'show_generate_proctored_exam_report_button': show_proctored_report_button,
|
||||
'get_problem_responses_url': reverse(
|
||||
'api_instructor:get_problem_responses',
|
||||
kwargs={'course_id': unicode(course_key)}
|
||||
),
|
||||
'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_issued_certificates_url': reverse(
|
||||
@@ -650,14 +638,8 @@ def _section_data_download(course, access):
|
||||
),
|
||||
'get_anon_ids_url': reverse('get_anon_ids', kwargs={'course_id': unicode(course_key)}),
|
||||
'list_proctored_results_url': reverse('get_proctored_exam_results', kwargs={'course_id': unicode(course_key)}),
|
||||
'list_instructor_tasks_url': reverse(
|
||||
'api_instructor:list_instructor_tasks',
|
||||
kwargs={'course_id': unicode(course_key)}
|
||||
),
|
||||
'list_report_downloads_url': reverse(
|
||||
'api_instructor:list_report_downloads',
|
||||
kwargs={'course_id': unicode(course_key)}
|
||||
),
|
||||
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': unicode(course_key)}),
|
||||
'list_report_downloads_url': reverse('list_report_downloads', kwargs={'course_id': unicode(course_key)}),
|
||||
'calculate_grades_csv_url': reverse('calculate_grades_csv', kwargs={'course_id': unicode(course_key)}),
|
||||
'problem_grade_report_url': reverse('problem_grade_report', kwargs={'course_id': unicode(course_key)}),
|
||||
'course_has_survey': True if course.course_survey_name else False,
|
||||
@@ -714,7 +696,7 @@ def _section_send_email(course, access):
|
||||
'course_modes': course_modes,
|
||||
'default_cohort_name': DEFAULT_COHORT_NAME,
|
||||
'list_instructor_tasks_url': reverse(
|
||||
'api_instructor:list_instructor_tasks', kwargs={'course_id': unicode(course_key)}
|
||||
'list_instructor_tasks', kwargs={'course_id': unicode(course_key)}
|
||||
),
|
||||
'email_background_tasks_url': reverse(
|
||||
'list_background_email_tasks', kwargs={'course_id': unicode(course_key)}
|
||||
|
||||
@@ -505,9 +505,6 @@ urlpatterns += [
|
||||
include(COURSE_URLS)
|
||||
),
|
||||
|
||||
# Instructor API (accessible via OAuth)
|
||||
url(r'^api/instructor/', include('lms.djangoapps.instructor.urls', namespace='api_instructor')),
|
||||
|
||||
# Discussions Management
|
||||
url(
|
||||
r'^courses/{}/discussions/settings$'.format(
|
||||
|
||||
Reference in New Issue
Block a user