Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions lms/djangoapps/instructor/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
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)
119 changes: 101 additions & 18 deletions lms/djangoapps/instructor/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
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,
Expand Down Expand Up @@ -141,7 +142,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': {},
}
Expand Down Expand Up @@ -177,7 +178,7 @@
'get_enrollment_report',
'get_exec_summary_report',
'get_grading_config',
'get_problem_responses',
'api_instructor:get_problem_responses',
'get_proctored_exam_results',
'get_registration_codes',
'get_student_enrollment_status',
Expand All @@ -191,8 +192,8 @@
'list_entrance_exam_instructor_tasks',
'list_financial_report_downloads',
'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',
Expand Down Expand Up @@ -448,17 +449,17 @@ 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', {}),
('list_financial_report_downloads', {}),
('calculate_grades_csv', {}),
('get_students_features', {}),
('get_enrollment_report', {}),
('get_students_who_may_enroll', {}),
('get_exec_summary_report', {}),
('get_proctored_exam_results', {}),
('get_problem_responses', {}),
('api_instructor:get_problem_responses', {}),
('export_ora2_data', {}),
('rescore_problem',
{'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.user.email}),
Expand Down Expand Up @@ -538,7 +539,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):
"""
Expand All @@ -557,7 +558,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
)
Expand Down Expand Up @@ -593,7 +594,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
)
Expand Down Expand Up @@ -2898,7 +2899,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': unicode(self.course.id)}
)
problem_location = ''
Expand Down Expand Up @@ -2933,7 +2934,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': unicode(self.course.id)}
)
problem_location = ''
Expand All @@ -2953,7 +2954,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': unicode(self.course.id)}
)
task_type = 'problem_responses_csv'
Expand Down Expand Up @@ -3280,7 +3281,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': text_type(self.course.id)})
url = reverse('api_instructor: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(
Expand All @@ -3294,7 +3295,7 @@ def test_list_report_downloads_error(self, mock_error):
self.assertEqual(res_json, {"downloads": []})

def test_list_report_downloads(self):
url = reverse('list_report_downloads', kwargs={'course_id': text_type(self.course.id)})
url = reverse('api_instructor: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'),
Expand Down Expand Up @@ -4099,7 +4100,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': text_type(self.course.id)})
url = reverse('api_instructor: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'
Expand Down Expand Up @@ -4141,7 +4142,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': text_type(self.course.id)})
url = reverse('api_instructor: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'
Expand All @@ -4164,7 +4165,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': text_type(self.course.id)})
url = reverse('api_instructor: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'
Expand All @@ -4186,6 +4187,88 @@ def test_list_instructor_tasks_problem_student(self, act):
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):
"""
Expand Down
33 changes: 33 additions & 0 deletions lms/djangoapps/instructor/urls.py
Original file line number Diff line number Diff line change
@@ -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


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',
),
]
Loading