diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py
index 9f4499a42e5d..9653f515a809 100644
--- a/lms/djangoapps/instructor/tests/test_api.py
+++ b/lms/djangoapps/instructor/tests/test_api.py
@@ -86,6 +86,7 @@
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted
from openedx.core.djangoapps.django_comment_common.models import FORUM_ROLE_COMMUNITY_TA
from openedx.core.djangoapps.django_comment_common.utils import seed_permissions_roles
+
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
from openedx.core.lib.teams_config import TeamsConfig
@@ -132,7 +133,7 @@
},
{
'report_type': 'problem responses',
- 'instructor_api_endpoint': 'get_problem_responses',
+ 'instructor_api_endpoint': 'api_instructor:get_problem_responses',
'task_api_endpoint': 'lms.djangoapps.instructor_task.api.submit_calculate_problem_responses_csv',
'extra_instructor_api_kwargs': {},
}
@@ -152,7 +153,7 @@
'export_ora2_submission_files',
'export_ora2_summary',
'get_grading_config',
- 'get_problem_responses',
+ 'api_instructor:get_problem_responses',
'get_proctored_exam_results',
'get_student_enrollment_status',
'get_student_progress_url',
@@ -163,8 +164,8 @@
'list_email_content',
'list_entrance_exam_instructor_tasks',
'list_forum_members',
- 'list_instructor_tasks',
- 'list_report_downloads',
+ 'api_instructor:list_instructor_tasks',
+ 'api_instructor:list_report_downloads',
'mark_student_can_skip_entrance_exam',
'modify_access',
'register_and_enroll_students',
@@ -413,14 +414,14 @@ def setUp(self):
{'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'}),
- ('list_instructor_tasks', {}),
+ ('api_instructor:list_instructor_tasks', {}),
('list_background_email_tasks', {}),
- ('list_report_downloads', {}),
+ ('api_instructor:list_report_downloads', {}),
('calculate_grades_csv', {}),
('get_students_features', {}),
('get_students_who_may_enroll', {}),
('get_proctored_exam_results', {}),
- ('get_problem_responses', {}),
+ ('api_instructor:get_problem_responses', {}),
('export_ora2_data', {}),
('export_ora2_submission_files', {}),
('export_ora2_summary', {}),
@@ -498,7 +499,7 @@ def _access_problem_responses_endpoint(self, msg):
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)
+ self._access_endpoint('api_instructor:get_problem_responses', {}, 200, msg)
def test_staff_level(self):
"""
@@ -518,7 +519,7 @@ def test_staff_level(self):
# TODO: make these work
if endpoint in ['update_forum_role_membership', 'list_forum_members']:
continue
- elif endpoint == 'get_problem_responses':
+ elif endpoint == 'api_instructor:get_problem_responses':
self._access_problem_responses_endpoint(
"Staff member should be allowed to access endpoint " + endpoint
)
@@ -555,7 +556,7 @@ def test_instructor_level(self):
# TODO: make these work
if endpoint in ['update_forum_role_membership']:
continue
- elif endpoint == 'get_problem_responses':
+ elif endpoint == 'api_instructor:get_problem_responses':
self._access_problem_responses_endpoint(
"Instructor should be allowed to access endpoint " + endpoint
)
@@ -2493,7 +2494,7 @@ def test_get_problem_responses_invalid_location(self):
message when users submit an invalid problem location.
"""
url = reverse(
- 'get_problem_responses',
+ 'api_instructor:get_problem_responses',
kwargs={'course_id': str(self.course.id)}
)
problem_location = ''
@@ -2528,7 +2529,7 @@ def test_get_problem_responses_successful(self):
message if CSV generation was started successfully.
"""
url = reverse(
- 'get_problem_responses',
+ 'api_instructor:get_problem_responses',
kwargs={'course_id': str(self.course.id)}
)
problem_location = ''
@@ -2548,7 +2549,7 @@ def test_get_problem_responses_already_running(self):
message if CSV generation is already in progress.
"""
url = reverse(
- 'get_problem_responses',
+ 'api_instructor:get_problem_responses',
kwargs={'course_id': str(self.course.id)}
)
task_type = 'problem_responses_csv'
@@ -2739,7 +2740,7 @@ def test_list_report_downloads_error(self, mock_error):
"""
ex_status = 503
ex_reason = 'Slow Down'
- url = reverse('list_report_downloads', kwargs={'course_id': str(self.course.id)})
+ url = reverse('api_instructor:list_report_downloads', kwargs={'course_id': str(self.course.id)})
with patch('storages.backends.s3boto.S3BotoStorage.listdir', side_effect=BotoServerError(ex_status, ex_reason)):
response = self.client.post(url, {})
mock_error.assert_called_with(
@@ -2753,7 +2754,7 @@ def test_list_report_downloads_error(self, mock_error):
assert res_json == {'downloads': []}
def test_list_report_downloads(self):
- url = reverse('list_report_downloads', kwargs={'course_id': str(self.course.id)})
+ url = reverse('api_instructor:list_report_downloads', kwargs={'course_id': str(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'),
@@ -3539,7 +3540,7 @@ def setUp(self):
def test_list_instructor_tasks_running(self, act):
""" Test list of all running tasks. """
act.return_value = self.tasks
- url = reverse('list_instructor_tasks', kwargs={'course_id': str(self.course.id)})
+ url = reverse('api_instructor:list_instructor_tasks', kwargs={'course_id': str(self.course.id)})
mock_factory = MockCompletionInfo()
with patch(
'lms.djangoapps.instructor.views.instructor_task_helpers.get_task_completion_info'
@@ -3581,7 +3582,7 @@ def test_list_background_email_tasks(self, act):
def test_list_instructor_tasks_problem(self, act):
""" Test list task history for problem. """
act.return_value = self.tasks
- url = reverse('list_instructor_tasks', kwargs={'course_id': str(self.course.id)})
+ url = reverse('api_instructor:list_instructor_tasks', kwargs={'course_id': str(self.course.id)})
mock_factory = MockCompletionInfo()
with patch(
'lms.djangoapps.instructor.views.instructor_task_helpers.get_task_completion_info'
@@ -3604,7 +3605,7 @@ def test_list_instructor_tasks_problem(self, act):
def test_list_instructor_tasks_problem_student(self, act):
""" Test list task history for problem AND student. """
act.return_value = self.tasks
- url = reverse('list_instructor_tasks', kwargs={'course_id': str(self.course.id)})
+ url = reverse('api_instructor:list_instructor_tasks', kwargs={'course_id': str(self.course.id)})
mock_factory = MockCompletionInfo()
with patch(
'lms.djangoapps.instructor.views.instructor_task_helpers.get_task_completion_info'
@@ -3626,6 +3627,88 @@ def test_list_instructor_tasks_problem_student(self, act):
assert 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().setUp()
+ self.staff = GlobalStaffFactory()
+
+ @patch('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': text_type(self.course.id)})
+ # OAuth Client
+ oauth_client = ApplicationFactory.create()
+ access_token = AccessTokenFactory.create(
+ user=self.staff,
+ application=oauth_client
+ ).token
+ headers = {
+ 'HTTP_AUTHORIZATION': 'Bearer ' + access_token
+ }
+ mock_factory = MockCompletionInfo()
+ with patch(
+ 'lms.djangoapps.instructor_task.views.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': text_type(self.course.id)})
+ problem_location = ''
+
+ # OAuth Client
+ oauth_client = ApplicationFactory.create()
+ access_token = AccessTokenFactory.create(
+ user=self.staff,
+ application=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': text_type(self.course.id)})
+
+ # OAuth Client
+ oauth_client = ApplicationFactory.create()
+ access_token = AccessTokenFactory.create(
+ user=self.staff,
+ application=oauth_client
+ ).token
+ headers = {
+ 'HTTP_AUTHORIZATION': 'Bearer ' + access_token
+ }
+
+ response = self.client.post(url, {}, **headers)
+ self.assertEqual(response.status_code, 200)
+
+
@patch('lms.djangoapps.instructor_task.api.get_instructor_task_history', autospec=True)
class TestInstructorEmailContentList(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
"""
diff --git a/lms/djangoapps/instructor/urls.py b/lms/djangoapps/instructor/urls.py
new file mode 100644
index 000000000000..e420a700a64c
--- /dev/null
+++ b/lms/djangoapps/instructor/urls.py
@@ -0,0 +1,33 @@
+"""
+Instructor API endpoint new urls.
+"""
+
+from django.conf import settings
+from django.conf.urls import url
+
+import lms.djangoapps.instructor.views.api as instructor_api
+
+app_name = 'instructor'
+urlpatterns = [
+ url(
+ r'^v1/course/{}/tasks$'.format(
+ settings.COURSE_ID_PATTERN,
+ ),
+ instructor_api.InstructorTasks.as_view(),
+ name='list_instructor_tasks',
+ ),
+ url(
+ r'^v1/course/{}/reports$'.format(
+ settings.COURSE_ID_PATTERN,
+ ),
+ instructor_api.ReportDownloadsList.as_view(),
+ name='list_report_downloads',
+ ),
+ url(
+ r'^v1/course/{}/reports/problem_responses$'.format(
+ settings.COURSE_ID_PATTERN,
+ ),
+ instructor_api.ProblemResponseReport.as_view(),
+ name='get_problem_responses',
+ ),
+]
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index 959f8cce3cf2..c6dffddc07ad 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -970,23 +970,22 @@ def extract_user_info(user):
return JsonResponse(response_payload)
-@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 get_problem_responses(request, course_id):
+# The non-atomic decorator is required because this view calls a celery
+# task which uses the 'outer_atomic' context manager.
+@view_auth_classes(additional_permission_classes=(IsCourseStaffInstructor,))
+@method_decorator(transaction.non_atomic_requests, name='dispatch')
+@method_decorator(common_exceptions_400, name='post')
+@method_decorator(require_course_permission(permissions.CAN_RESEARCH), name='post')
+class ProblemResponseReport(DeveloperErrorViewMixin, APIView):
"""
- Initiate generation of a CSV file containing all student answers
- to a given problem.
+ **Use Cases**
**Example requests**
- POST /courses/{course_id}/instructor/api/get_problem_responses {
+ POST /api/instructor/v1/course/{course_id}/reports/problem_responses {
"problem_location": "{usage_key1},{usage_key2},{usage_key3}""
}
- POST /courses/{course_id}/instructor/api/get_problem_responses {
+ POST /api/instructor/v1/course/{course_id}/reports/problem_responses {
"problem_location": "{usage_key}",
"problem_types_filter": "problem"
}
@@ -1016,31 +1015,39 @@ def get_problem_responses(request, course_id):
"task_id": "4e49522f-31d9-431a-9cff-dd2a2bf4c85a"
}
```
-
- Responds with BadRequest if any of the provided problem locations are faulty.
"""
- course_key = CourseKey.from_string(course_id)
- # A comma-separated list of problem locations
- # The name of the POST parameter is `problem_location` (not pluralised) in
- # order to preserve backwards compatibility with existing third-party
- # scripts.
- problem_locations = request.POST.get('problem_location', '')
- # A comma-separated list of block types
- problem_types_filter = request.POST.get('problem_types_filter', '')
- report_type = _('problem responses')
- try:
- for problem_location in problem_locations.split(','):
- problem_key = UsageKey.from_string(problem_location).map_into_course(course_key) # lint-amnesty, pylint: disable=unused-variable
- 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.
- task = task_api.submit_calculate_problem_responses_csv(
- request, course_key, problem_locations, problem_types_filter,
- )
- success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type)
+ Responds with BadRequest if any of the provided problem locations are faulty.
+ """
- return JsonResponse({"status": success_status, "task_id": task.task_id})
+ course_key = CourseKey.from_string(course_id)
+ # A comma-separated list of problem locations
+ # The name of the POST parameter is `problem_location` (not pluralised) in
+ # order to preserve backwards compatibility with existing third-party
+ # scripts.
+ problem_locations = request.data.get('problem_location', '')
+ # A comma-separated list of block types
+ problem_types_filter = request.data.get('problem_types_filter', '')
+ report_type = _('problem responses')
+
+ try:
+ for problem_location in problem_locations.split(','):
+ UsageKey.from_string(problem_location).map_into_course(course_key)
+ except InvalidKeyError:
+ return JsonResponseBadRequest(_("Could not find problem with this location."))
+
+ task = task_api.submit_calculate_problem_responses_csv(
+ request, course_key, problem_locations, problem_types_filter,
+ )
+ success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type)
+
+ return JsonResponse({"status": success_status, "task_id": task.task_id})
@require_POST
@@ -1851,50 +1858,83 @@ def list_email_content(request, course_id):
return JsonResponse(response_payload)
-@require_POST
-@ensure_csrf_cookie
-@cache_control(no_cache=True, no_store=True, must_revalidate=True)
-@require_course_permission(permissions.SHOW_TASKS)
-def list_instructor_tasks(request, course_id):
+@view_auth_classes(additional_permission_classes=(IsCourseStaffInstructor,))
+@method_decorator(require_course_permission(permissions.SHOW_TASKS), name='post')
+class InstructorTasks(DeveloperErrorViewMixin, APIView):
"""
- List instructor tasks.
+ **Use Cases**
+
+ Lists currently running instructor tasks
- Takes optional query paremeters.
+ **Parameters**
- 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)
- """
- 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"
- )
+ **Example Requests**:
- 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 = task_api.get_instructor_task_history(course_id, module_state_key, student)
+ POST /api/instructor/v1/course/{course_id}/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"
+ }
+ ]
+ }
+ """
+ @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.data.get('problem_location_str', False))
+ student = request.data.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 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 = task_api.get_instructor_task_history(
+ course_id,
+ module_state_key,
+ student
+ )
+ else:
+ # Specifying for single problem's history
+ tasks = task_api.get_instructor_task_history(
+ course_id,
+ module_state_key
+ )
else:
- # Specifying for single problem's history
- tasks = task_api.get_instructor_task_history(course_id, module_state_key)
- else:
- # If no problem or student, just get currently running tasks
- tasks = task_api.get_running_instructor_tasks(course_id)
+ # If no problem or student, just get currently running tasks
+ tasks = task_api.get_running_instructor_tasks(course_id)
- response_payload = {
- 'tasks': list(map(extract_task_features, tasks)),
- }
- return JsonResponse(response_payload)
+ response_payload = {
+ 'tasks': list(map(extract_task_features, tasks)),
+ }
+ return JsonResponse(response_payload)
@require_POST
@@ -1939,28 +1979,48 @@ def list_entrance_exam_instructor_tasks(request, course_id):
return JsonResponse(response_payload)
-@require_POST
-@ensure_csrf_cookie
-@cache_control(no_cache=True, no_store=True, must_revalidate=True)
-@require_course_permission(permissions.CAN_RESEARCH)
-def list_report_downloads(request, course_id):
+@view_auth_classes(additional_permission_classes=(IsCourseStaffInstructor,))
+@method_decorator(require_course_permission(permissions.CAN_RESEARCH), name='post')
+class ReportDownloadsList(DeveloperErrorViewMixin, APIView):
"""
- List grade CSV files that are available for download for this course.
+ **Use Cases**
+
+ Lists reports available for download
+
+ **Example Requests**:
+
+ POST /api/instructor/v1/course/{course_id}/reports
- Takes the following query parameters:
- - (optional) report_name - name of the report
+ **Response Values**
+ {
+ "downloads": [
+ {
+ "url": "https://1.mock.url",
+ "link": "mock_file_name_1",
+ "name": "mock_file_name_1"
+ }
+ ]
+ }
"""
- 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.
- response_payload = {
- 'downloads': [
- dict(name=name, url=url, link=HTML('{}').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)
+ 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.data.get("report_name", None)
+
+ response_payload = {
+ 'downloads': [
+ dict(name=name, url=url, link=HTML(u'{}').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
diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py
index 8fc74f80dfb3..e46b793a2c80 100644
--- a/lms/djangoapps/instructor/views/api_urls.py
+++ b/lms/djangoapps/instructor/views/api_urls.py
@@ -13,7 +13,6 @@
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)?$', api.get_students_features, name='get_students_features'),
url(r'^get_issued_certificates/$', api.get_issued_certificates, name='get_issued_certificates'),
@@ -31,7 +30,6 @@
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'),
@@ -46,7 +44,6 @@
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'),
@@ -77,4 +74,21 @@
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',
+ ),
]
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index 3d4efaca2a57..83577b41743c 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -354,7 +354,7 @@ def _section_certificates(course):
kwargs={'course_id': course.id}
),
'list_instructor_tasks_url': reverse(
- 'list_instructor_tasks',
+ 'api_instructor:list_instructor_tasks',
kwargs={'course_id': course.id}
),
}
@@ -414,7 +414,10 @@ def _section_course_info(course, access):
'start_date': course.start,
'end_date': course.end,
'num_sections': len(course.children),
- 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': str(course_key)}),
+ 'list_instructor_tasks_url': reverse(
+ 'api_instructor:list_instructor_tasks',
+ kwargs={'course_id': str(course_key)}
+ ),
}
if settings.FEATURES.get('DISPLAY_ANALYTICS_ENROLLMENTS'):
@@ -563,7 +566,10 @@ def _section_student_admin(course, access):
'mark_student_can_skip_entrance_exam',
kwargs={'course_id': str(course_key)},
),
- 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': str(course_key)}),
+ 'list_instructor_tasks_url': reverse(
+ 'api_instructor:list_instructor_tasks',
+ kwargs={'course_id': str(course_key)}
+ ),
'list_entrace_exam_instructor_tasks_url': reverse(
'list_entrance_exam_instructor_tasks',
kwargs={'course_id': str(course_key)}
@@ -607,9 +613,12 @@ 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('get_problem_responses', kwargs={'course_id': str(course_key)}),
- 'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': str(course_key)}),
- 'get_students_features_url': reverse('get_students_features', kwargs={'course_id': str(course_key)}),
+ 'get_problem_responses_url': reverse(
+ 'api_instructor:get_problem_responses',
+ kwargs={'course_id': str(course_key)}
+ ),
+ 'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': six.text_type(course_key)}),
+ 'get_students_features_url': reverse('get_students_features', kwargs={'course_id': six.text_type(course_key)}),
'get_issued_certificates_url': reverse(
'get_issued_certificates', kwargs={'course_id': str(course_key)}
),
@@ -620,11 +629,17 @@ def _section_data_download(course, access):
'list_proctored_results_url': reverse(
'get_proctored_exam_results', kwargs={'course_id': str(course_key)}
),
- 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': str(course_key)}),
- 'list_report_downloads_url': reverse('list_report_downloads', kwargs={'course_id': str(course_key)}),
- 'calculate_grades_csv_url': reverse('calculate_grades_csv', kwargs={'course_id': str(course_key)}),
- 'problem_grade_report_url': reverse('problem_grade_report', kwargs={'course_id': str(course_key)}),
- 'course_has_survey': True if course.course_survey_name else False, # lint-amnesty, pylint: disable=simplifiable-if-expression
+ 'list_instructor_tasks_url': reverse(
+ 'api_instructor:list_instructor_tasks',
+ kwargs={'course_id': str(course_key)}
+ ),
+ 'list_report_downloads_url': reverse(
+ 'api_instructor:list_report_downloads',
+ kwargs={'course_id': str(course_key)}
+ ),
+ 'calculate_grades_csv_url': reverse('calculate_grades_csv', kwargs={'course_id': six.text_type(course_key)}),
+ 'problem_grade_report_url': reverse('problem_grade_report', kwargs={'course_id': six.text_type(course_key)}),
+ 'course_has_survey': True if course.course_survey_name else False,
'course_survey_results_url': reverse(
'get_course_survey_results', kwargs={'course_id': str(course_key)}
),
@@ -686,7 +701,7 @@ def _section_send_email(course, access):
'course_modes': course_modes,
'default_cohort_name': DEFAULT_COHORT_NAME,
'list_instructor_tasks_url': reverse(
- 'list_instructor_tasks', kwargs={'course_id': str(course_key)}
+ 'api_instructor:list_instructor_tasks', kwargs={'course_id': str(course_key)}
),
'email_background_tasks_url': reverse(
'list_background_email_tasks', kwargs={'course_id': str(course_key)}
diff --git a/lms/urls.py b/lms/urls.py
index 7489a9688066..9f38baeab770 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -545,6 +545,9 @@
name='set_course_mode_price',
),
+ # 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(
diff --git a/openedx/core/lib/api/view_utils.py b/openedx/core/lib/api/view_utils.py
index bcc395a7632c..5ec6310229e2 100644
--- a/openedx/core/lib/api/view_utils.py
+++ b/openedx/core/lib/api/view_utils.py
@@ -110,9 +110,16 @@ def get_serializer_context(self):
return result
-def view_auth_classes(is_user=False, is_authenticated=True):
+def view_auth_classes(is_user=False, is_authenticated=True, additional_permission_classes=None):
"""
Function and class decorator that abstracts the authentication and permission checks for api views.
+
+ Parameters
+ is_user (bool): Requires the requesting user to be present in the URL
+ is_authenticated (bool): Requires the requesting user to be authenticated
+ additional_permission_classes (tuple[BasePermission]):
+ Tuple of classes deriving from rest_framework.permissions.BasePermission
+ that should additionally be required of users
"""
def _decorator(func_or_class):
"""
@@ -129,6 +136,8 @@ def _decorator(func_or_class):
func_or_class.permission_classes += (IsAuthenticated,)
if is_user:
func_or_class.permission_classes += (IsUserInUrl,)
+ if additional_permission_classes:
+ func_or_class.permission_classes += additional_permission_classes
return func_or_class
return _decorator