Merge pull request #19422 from edx/arch/remove-grades-v0

Remove Grades v0 REST API (DEPR-3)
This commit is contained in:
Nimisha Asthagiri
2018-12-17 11:22:35 -05:00
committed by GitHub
7 changed files with 333 additions and 781 deletions

View File

@@ -1,548 +0,0 @@
"""
Tests for the views
"""
from datetime import datetime
from urllib import urlencode
import ddt
from django.urls import reverse
from edx_oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory
from mock import patch
from opaque_keys import InvalidKeyError
from pytz import UTC
from rest_framework import status
from rest_framework.test import APITestCase
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory, StaffFactory
from lms.djangoapps.grades.tests.utils import mock_passing_grade
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
@ddt.ddt
class CurrentGradeViewTest(SharedModuleStoreTestCase, APITestCase):
"""
Tests for the Current Grade View
The following tests assume that the grading policy is the edX default one:
{
"GRADER": [
{
"drop_count": 2,
"min_count": 12,
"short_label": "HW",
"type": "Homework",
"weight": 0.15
},
{
"drop_count": 2,
"min_count": 12,
"type": "Lab",
"weight": 0.15
},
{
"drop_count": 0,
"min_count": 1,
"short_label": "Midterm",
"type": "Midterm Exam",
"weight": 0.3
},
{
"drop_count": 0,
"min_count": 1,
"short_label": "Final",
"type": "Final Exam",
"weight": 0.4
}
],
"GRADE_CUTOFFS": {
"Pass": 0.5
}
}
"""
shard = 4
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
@classmethod
def setUpClass(cls):
super(CurrentGradeViewTest, cls).setUpClass()
cls.course = CourseFactory.create(display_name='test course', run="Testing_course")
with cls.store.bulk_operations(cls.course.id):
chapter = ItemFactory.create(
category='chapter',
parent_location=cls.course.location,
display_name="Chapter 1",
)
# create a problem for each type and minimum count needed by the grading policy
# A section is not considered if the student answers less than "min_count" problems
for grading_type, min_count in (("Homework", 12), ("Lab", 12), ("Midterm Exam", 1), ("Final Exam", 1)):
for num in xrange(min_count):
section = ItemFactory.create(
category='sequential',
parent_location=chapter.location,
due=datetime(2013, 9, 18, 11, 30, 00, tzinfo=UTC),
display_name='Sequential {} {}'.format(grading_type, num),
format=grading_type,
graded=True,
)
vertical = ItemFactory.create(
category='vertical',
parent_location=section.location,
display_name='Vertical {} {}'.format(grading_type, num),
)
ItemFactory.create(
category='problem',
parent_location=vertical.location,
display_name='Problem {} {}'.format(grading_type, num),
)
cls.course_key = cls.course.id
cls.password = 'test'
cls.student = UserFactory(username='dummy', password=cls.password)
cls.other_student = UserFactory(username='foo', password=cls.password)
cls.other_user = UserFactory(username='bar', password=cls.password)
cls.staff = StaffFactory(course_key=cls.course.id, password=cls.password)
cls.global_staff = GlobalStaffFactory.create()
date = datetime(2013, 1, 22, tzinfo=UTC)
for user in (cls.student, cls.other_student, ):
CourseEnrollmentFactory(
course_id=cls.course.id,
user=user,
created=date,
)
cls.namespaced_url = 'grades_api:user_grade_detail'
def setUp(self):
super(CurrentGradeViewTest, self).setUp()
self.client.login(username=self.student.username, password=self.password)
def get_url(self, username):
"""
Helper function to create the url
"""
base_url = reverse(
self.namespaced_url,
kwargs={
'course_id': self.course_key,
}
)
query_string = ''
if username:
query_string = '?' + urlencode(dict(username=username))
return base_url + query_string
def test_anonymous(self):
"""
Test that an anonymous user cannot access the API and an error is received.
"""
self.client.logout()
resp = self.client.get(self.get_url(self.student.username))
self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
def test_self_get_grade(self):
"""
Test that a user can successfully request her own grade.
"""
with check_mongo_calls(3):
resp = self.client.get(self.get_url(self.student.username))
self.assertEqual(resp.status_code, status.HTTP_200_OK)
# redo with block structure now in the cache
with check_mongo_calls(3):
resp = self.client.get(self.get_url(self.student.username))
self.assertEqual(resp.status_code, status.HTTP_200_OK)
# and again, with the username defaulting to the current user
with check_mongo_calls(3):
resp = self.client.get(self.get_url(None))
self.assertEqual(resp.status_code, status.HTTP_200_OK)
def test_nonexistent_user(self):
"""
Test that a request for a nonexistent username returns an error.
"""
resp = self.client.get(self.get_url('IDoNotExist'))
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
self.assertIn('error_code', resp.data)
self.assertEqual(resp.data['error_code'], 'user_mismatch')
def test_other_get_grade(self):
"""
Test that if a user requests the grade for another user, she receives an error.
"""
self.client.logout()
self.client.login(username=self.other_student.username, password=self.password)
resp = self.client.get(self.get_url(self.student.username))
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
self.assertIn('error_code', resp.data)
self.assertEqual(resp.data['error_code'], 'user_mismatch')
def test_self_get_grade_not_enrolled(self):
"""
Test that a user receives an error if she requests
her own grade in a course where she is not enrolled.
"""
# a user not enrolled in the course cannot request her grade
self.client.logout()
self.client.login(username=self.other_user.username, password=self.password)
resp = self.client.get(self.get_url(self.other_user.username))
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
self.assertIn('error_code', resp.data)
self.assertEqual(
resp.data['error_code'],
'user_or_course_does_not_exist'
)
def test_wrong_course_key(self):
"""
Test that a request for an invalid course key returns an error.
"""
def mock_from_string(*args, **kwargs): # pylint: disable=unused-argument
"""Mocked function to always raise an exception"""
raise InvalidKeyError('foo', 'bar')
with patch('opaque_keys.edx.keys.CourseKey.from_string', side_effect=mock_from_string):
resp = self.client.get(self.get_url(self.student.username))
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
self.assertIn('error_code', resp.data)
self.assertEqual(
resp.data['error_code'],
'invalid_course_key'
)
def test_course_does_not_exist(self):
"""
Test that requesting a valid, nonexistent course key returns an error as expected.
"""
base_url = reverse(
self.namespaced_url,
kwargs={
'course_id': 'course-v1:MITx+8.MechCX+2014_T1',
}
)
url = "{0}?username={1}".format(base_url, self.student.username)
resp = self.client.get(url)
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
self.assertIn('error_code', resp.data)
self.assertEqual(
resp.data['error_code'],
'user_or_course_does_not_exist'
)
@ddt.data(
'staff', 'global_staff'
)
def test_staff_can_see_student(self, staff_user):
"""
Ensure that staff members can see her student's grades.
"""
self.client.logout()
self.client.login(username=getattr(self, staff_user).username, password=self.password)
resp = self.client.get(self.get_url(self.student.username))
self.assertEqual(resp.status_code, status.HTTP_200_OK)
expected_data = [{
'username': self.student.username,
'letter_grade': None,
'percent': 0.0,
'course_key': str(self.course_key),
'passed': False
}]
self.assertEqual(resp.data, expected_data)
@ddt.data(
'staff', 'global_staff'
)
def test_staff_requests_nonexistent_user(self, staff_user):
"""
Test that a staff request for a nonexistent username returns an error.
"""
self.client.logout()
self.client.login(username=getattr(self, staff_user).username, password=self.password)
resp = self.client.get(self.get_url('IDoNotExist'))
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
self.assertIn('error_code', resp.data)
self.assertEqual(resp.data['error_code'], 'user_does_not_exist')
def test_no_grade(self):
"""
Test the grade for a user who has not answered any test.
"""
resp = self.client.get(self.get_url(self.student.username))
self.assertEqual(resp.status_code, status.HTTP_200_OK)
expected_data = [{
'username': self.student.username,
'letter_grade': None,
'percent': 0.0,
'course_key': str(self.course_key),
'passed': False
}]
self.assertEqual(resp.data, expected_data)
@ddt.data(
({'letter_grade': None, 'percent': 0.4, 'passed': False}),
({'letter_grade': 'Pass', 'percent': 1, 'passed': True}),
)
def test_grade(self, grade):
"""
Test that the user gets her grade in case she answered tests with an insufficient score.
"""
with mock_passing_grade(letter_grade=grade['letter_grade'], percent=grade['percent']):
resp = self.client.get(self.get_url(self.student.username))
self.assertEqual(resp.status_code, status.HTTP_200_OK)
expected_data = {
'username': self.student.username,
'course_key': str(self.course_key),
}
expected_data.update(grade)
self.assertEqual(resp.data, [expected_data])
@ddt.ddt
class GradingPolicyTestMixin(object):
"""
Mixin class for Grading Policy tests
"""
shard = 4
view_name = None
def setUp(self):
super(GradingPolicyTestMixin, self).setUp()
self.create_user_and_access_token()
def create_user_and_access_token(self):
self.user = GlobalStaffFactory.create()
self.oauth_client = ClientFactory.create()
self.access_token = AccessTokenFactory.create(user=self.user, client=self.oauth_client).token
@classmethod
def create_course_data(cls):
cls.invalid_course_id = 'foo/bar/baz'
cls.course = CourseFactory.create(display_name='An Introduction to API Testing', raw_grader=cls.raw_grader)
cls.course_id = unicode(cls.course.id)
with cls.store.bulk_operations(cls.course.id, emit_signals=False):
cls.sequential = ItemFactory.create(
category="sequential",
parent_location=cls.course.location,
display_name="Lesson 1",
format="Homework",
graded=True
)
factory = MultipleChoiceResponseXMLFactory()
args = {'choices': [False, True, False]}
problem_xml = factory.build_xml(**args)
cls.problem = ItemFactory.create(
category="problem",
parent_location=cls.sequential.location,
display_name="Problem 1",
format="Homework",
data=problem_xml,
)
cls.video = ItemFactory.create(
category="video",
parent_location=cls.sequential.location,
display_name="Video 1",
)
cls.html = ItemFactory.create(
category="html",
parent_location=cls.sequential.location,
display_name="HTML 1",
)
def http_get(self, uri, **headers):
"""
Submit an HTTP GET request
"""
default_headers = {
'HTTP_AUTHORIZATION': 'Bearer ' + self.access_token
}
default_headers.update(headers)
response = self.client.get(uri, follow=True, **default_headers)
return response
def assert_get_for_course(self, course_id=None, expected_status_code=200, **headers):
"""
Submit an HTTP GET request to the view for the given course.
Validates the status_code of the response is as expected.
"""
response = self.http_get(
reverse(self.view_name, kwargs={'course_id': course_id or self.course_id}),
**headers
)
self.assertEqual(response.status_code, expected_status_code)
return response
def get_auth_header(self, user):
"""
Returns Bearer auth header with a generated access token
for the given user.
"""
access_token = AccessTokenFactory.create(user=user, client=self.oauth_client).token
return 'Bearer ' + access_token
def test_get_invalid_course(self):
"""
The view should return a 404 for an invalid course ID.
"""
self.assert_get_for_course(course_id=self.invalid_course_id, expected_status_code=404)
def test_get(self):
"""
The view should return a 200 for a valid course ID.
"""
return self.assert_get_for_course()
def test_not_authenticated(self):
"""
The view should return HTTP status 401 if user is unauthenticated.
"""
self.assert_get_for_course(expected_status_code=401, HTTP_AUTHORIZATION=None)
def test_staff_authorized(self):
"""
The view should return a 200 when provided an access token
for course staff.
"""
user = StaffFactory(course_key=self.course.id)
auth_header = self.get_auth_header(user)
self.assert_get_for_course(HTTP_AUTHORIZATION=auth_header)
def test_not_authorized(self):
"""
The view should return HTTP status 404 when provided an
access token for an unauthorized user.
"""
user = UserFactory()
auth_header = self.get_auth_header(user)
self.assert_get_for_course(expected_status_code=404, HTTP_AUTHORIZATION=auth_header)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_keys(self, modulestore_type):
"""
The view should be addressable by course-keys from both module stores.
"""
course = CourseFactory.create(
start=datetime(2014, 6, 16, 14, 30, tzinfo=UTC),
end=datetime(2015, 1, 16, tzinfo=UTC),
org="MTD",
default_store=modulestore_type,
)
self.assert_get_for_course(course_id=unicode(course.id))
class CourseGradingPolicyTests(GradingPolicyTestMixin, SharedModuleStoreTestCase):
"""
Tests for CourseGradingPolicy view.
"""
shard = 4
view_name = 'grades_api:course_grading_policy'
raw_grader = [
{
"min_count": 24,
"weight": 0.2,
"type": "Homework",
"drop_count": 0,
"short_label": "HW"
},
{
"min_count": 4,
"weight": 0.8,
"type": "Exam",
"drop_count": 0,
"short_label": "Exam"
}
]
@classmethod
def setUpClass(cls):
super(CourseGradingPolicyTests, cls).setUpClass()
cls.create_course_data()
def test_get(self):
"""
The view should return grading policy for a course.
"""
response = super(CourseGradingPolicyTests, self).test_get()
expected = [
{
"count": 24,
"weight": 0.2,
"assignment_type": "Homework",
"dropped": 0
},
{
"count": 4,
"weight": 0.8,
"assignment_type": "Exam",
"dropped": 0
}
]
self.assertListEqual(response.data, expected)
class CourseGradingPolicyMissingFieldsTests(GradingPolicyTestMixin, SharedModuleStoreTestCase):
"""
Tests for CourseGradingPolicy view when fields are missing.
"""
shard = 4
view_name = 'grades_api:course_grading_policy'
# Raw grader with missing keys
raw_grader = [
{
"min_count": 24,
"weight": 0.2,
"type": "Homework",
"drop_count": 0,
"short_label": "HW"
},
{
# Deleted "min_count" key
"weight": 0.8,
"type": "Exam",
"drop_count": 0,
"short_label": "Exam"
}
]
@classmethod
def setUpClass(cls):
super(CourseGradingPolicyMissingFieldsTests, cls).setUpClass()
cls.create_course_data()
def test_get(self):
"""
The view should return grading policy for a course.
"""
response = super(CourseGradingPolicyMissingFieldsTests, self).test_get()
expected = [
{
"count": 24,
"weight": 0.2,
"assignment_type": "Homework",
"dropped": 0
},
{
"count": None,
"weight": 0.8,
"assignment_type": "Exam",
"dropped": 0
}
]
self.assertListEqual(response.data, expected)

View File

@@ -5,23 +5,9 @@ Grades API URLs.
from django.conf import settings
from django.conf.urls import include, url
from lms.djangoapps.grades.api import views
app_name = 'lms.djangoapps.grades'
urlpatterns = [
url(
r'^v0/course_grade/{course_id}/users/$'.format(
course_id=settings.COURSE_ID_PATTERN,
),
views.UserGradeView.as_view(), name='user_grade_detail'
),
url(
r'^v0/courses/{course_id}/policy/$'.format(
course_id=settings.COURSE_ID_PATTERN,
),
views.CourseGradingPolicy.as_view(), name='course_grading_policy'
),
url(r'^v1/', include('grades.api.v1.urls', namespace='v1'))
]

View File

@@ -0,0 +1,259 @@
"""
Tests for the views
"""
from datetime import datetime
import ddt
from django.urls import reverse
from edx_oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory
from pytz import UTC
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory, StaffFactory
from student.tests.factories import UserFactory
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@ddt.ddt
class GradingPolicyTestMixin(object):
"""
Mixin class for Grading Policy tests
"""
shard = 4
view_name = None
def setUp(self):
super(GradingPolicyTestMixin, self).setUp()
self.create_user_and_access_token()
def create_user_and_access_token(self):
self.user = GlobalStaffFactory.create()
self.oauth_client = ClientFactory.create()
self.access_token = AccessTokenFactory.create(user=self.user, client=self.oauth_client).token
@classmethod
def create_course_data(cls):
cls.invalid_course_id = 'foo/bar/baz'
cls.course = CourseFactory.create(display_name='An Introduction to API Testing', raw_grader=cls.raw_grader)
cls.course_id = unicode(cls.course.id)
with cls.store.bulk_operations(cls.course.id, emit_signals=False):
cls.sequential = ItemFactory.create(
category="sequential",
parent_location=cls.course.location,
display_name="Lesson 1",
format="Homework",
graded=True
)
factory = MultipleChoiceResponseXMLFactory()
args = {'choices': [False, True, False]}
problem_xml = factory.build_xml(**args)
cls.problem = ItemFactory.create(
category="problem",
parent_location=cls.sequential.location,
display_name="Problem 1",
format="Homework",
data=problem_xml,
)
cls.video = ItemFactory.create(
category="video",
parent_location=cls.sequential.location,
display_name="Video 1",
)
cls.html = ItemFactory.create(
category="html",
parent_location=cls.sequential.location,
display_name="HTML 1",
)
def http_get(self, uri, **headers):
"""
Submit an HTTP GET request
"""
default_headers = {
'HTTP_AUTHORIZATION': 'Bearer ' + self.access_token
}
default_headers.update(headers)
response = self.client.get(uri, follow=True, **default_headers)
return response
def assert_get_for_course(self, course_id=None, expected_status_code=200, **headers):
"""
Submit an HTTP GET request to the view for the given course.
Validates the status_code of the response is as expected.
"""
response = self.http_get(
reverse(self.view_name, kwargs={'course_id': course_id or self.course_id}),
**headers
)
self.assertEqual(response.status_code, expected_status_code)
return response
def get_auth_header(self, user):
"""
Returns Bearer auth header with a generated access token
for the given user.
"""
access_token = AccessTokenFactory.create(user=user, client=self.oauth_client).token
return 'Bearer ' + access_token
def test_get_invalid_course(self):
"""
The view should return a 404 for an invalid course ID.
"""
self.assert_get_for_course(course_id=self.invalid_course_id, expected_status_code=404)
def test_get(self):
"""
The view should return a 200 for a valid course ID.
"""
return self.assert_get_for_course()
def test_not_authenticated(self):
"""
The view should return HTTP status 401 if user is unauthenticated.
"""
self.assert_get_for_course(expected_status_code=401, HTTP_AUTHORIZATION=None)
def test_staff_authorized(self):
"""
The view should return a 200 when provided an access token
for course staff.
"""
user = StaffFactory(course_key=self.course.id)
auth_header = self.get_auth_header(user)
self.assert_get_for_course(HTTP_AUTHORIZATION=auth_header)
def test_not_authorized(self):
"""
The view should return HTTP status 404 when provided an
access token for an unauthorized user.
"""
user = UserFactory()
auth_header = self.get_auth_header(user)
self.assert_get_for_course(expected_status_code=403, HTTP_AUTHORIZATION=auth_header)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_keys(self, modulestore_type):
"""
The view should be addressable by course-keys from both module stores.
"""
course = CourseFactory.create(
start=datetime(2014, 6, 16, 14, 30, tzinfo=UTC),
end=datetime(2015, 1, 16, tzinfo=UTC),
org="MTD",
default_store=modulestore_type,
)
self.assert_get_for_course(course_id=unicode(course.id))
class CourseGradingPolicyTests(GradingPolicyTestMixin, SharedModuleStoreTestCase):
"""
Tests for CourseGradingPolicy view.
"""
shard = 4
view_name = 'grades_api:v1:course_grading_policy'
raw_grader = [
{
"min_count": 24,
"weight": 0.2,
"type": "Homework",
"drop_count": 0,
"short_label": "HW"
},
{
"min_count": 4,
"weight": 0.8,
"type": "Exam",
"drop_count": 0,
"short_label": "Exam"
}
]
@classmethod
def setUpClass(cls):
super(CourseGradingPolicyTests, cls).setUpClass()
cls.create_course_data()
def test_get(self):
"""
The view should return grading policy for a course.
"""
response = super(CourseGradingPolicyTests, self).test_get()
expected = [
{
"count": 24,
"weight": 0.2,
"assignment_type": "Homework",
"dropped": 0
},
{
"count": 4,
"weight": 0.8,
"assignment_type": "Exam",
"dropped": 0
}
]
self.assertListEqual(response.data, expected)
class CourseGradingPolicyMissingFieldsTests(GradingPolicyTestMixin, SharedModuleStoreTestCase):
"""
Tests for CourseGradingPolicy view when fields are missing.
"""
shard = 4
view_name = 'grades_api:v1:course_grading_policy'
# Raw grader with missing keys
raw_grader = [
{
"min_count": 24,
"weight": 0.2,
"type": "Homework",
"drop_count": 0,
"short_label": "HW"
},
{
# Deleted "min_count" key
"weight": 0.8,
"type": "Exam",
"drop_count": 0,
"short_label": "Exam"
}
]
@classmethod
def setUpClass(cls):
super(CourseGradingPolicyMissingFieldsTests, cls).setUpClass()
cls.create_course_data()
def test_get(self):
"""
The view should return grading policy for a course.
"""
response = super(CourseGradingPolicyMissingFieldsTests, self).test_get()
expected = [
{
"count": 24,
"weight": 0.2,
"assignment_type": "Homework",
"dropped": 0
},
{
"count": None,
"weight": 0.8,
"assignment_type": "Exam",
"dropped": 0
}
]
self.assertListEqual(response.data, expected)

View File

@@ -3,7 +3,6 @@ from django.conf import settings
from django.conf.urls import url
from lms.djangoapps.grades.api.v1 import gradebook_views, views
from lms.djangoapps.grades.api.views import CourseGradingPolicy
app_name = 'lms.djangoapps.grades'
@@ -21,7 +20,7 @@ urlpatterns = [
),
url(
r'^policy/courses/{course_id}/$'.format(course_id=settings.COURSE_ID_PATTERN),
CourseGradingPolicy.as_view(),
views.CourseGradingPolicy.as_view(),
name='course_grading_policy'
),
url(

View File

@@ -2,9 +2,15 @@
import logging
from contextlib import contextmanager
from rest_framework import status
from rest_framework.generics import ListAPIView
from rest_framework.response import Response
from edx_rest_framework_extensions import permissions
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.grades.api.serializers import GradingPolicySerializer
from lms.djangoapps.grades.api.v1.utils import (
CourseEnrollmentPagination,
GradeViewMixin,
@@ -14,7 +20,9 @@ from lms.djangoapps.grades.api.v1.utils import (
)
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from lms.djangoapps.grades.models import PersistentCourseGrade
from opaque_keys import InvalidKeyError
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
from xmodule.modulestore.django import modulestore
log = logging.getLogger(__name__)
@@ -136,3 +144,68 @@ class CourseGradesView(GradeViewMixin, PaginatedAPIView):
user_grades.append(self._serialize_user_grade(user, course_key, course_grade))
return self.get_paginated_response(user_grades)
class CourseGradingPolicy(GradeViewMixin, ListAPIView):
"""
**Use Case**
Get the course grading policy.
**Example requests**:
GET /api/grades/v1/policy/courses/{course_id}/
**Response Values**
* assignment_type: The type of the assignment, as configured by course
staff. For example, course staff might make the assignment types Homework,
Quiz, and Exam.
* count: The number of assignments of the type.
* dropped: Number of assignments of the type that are dropped.
* weight: The weight, or effect, of the assignment type on the learner's
final grade.
"""
allow_empty = False
authentication_classes = (
JwtAuthentication,
OAuth2AuthenticationAllowInactiveUser,
SessionAuthenticationAllowInactiveUser,
)
def _get_course(self, request, course_id):
"""
Returns the course after parsing the id, checking access, and checking existence.
"""
try:
course_key = get_course_key(request, course_id)
except InvalidKeyError:
raise self.api_error(
status_code=status.HTTP_400_BAD_REQUEST,
developer_message='The provided course key cannot be parsed.',
error_code='invalid_course_key'
)
if not has_access(request.user, 'staff', course_key):
raise self.api_error(
status_code=status.HTTP_403_FORBIDDEN,
developer_message='The course does not exist.',
error_code='user_or_course_does_not_exist',
)
course = modulestore().get_course(course_key, depth=0)
if not course:
raise self.api_error(
status_code=status.HTTP_404_NOT_FOUND,
developer_message='The course does not exist.',
error_code='user_or_course_does_not_exist',
)
return course
def get(self, request, course_id, *args, **kwargs): # pylint: disable=arguments-differ
course = self._get_course(request, course_id)
return Response(GradingPolicySerializer(course.raw_grader, many=True).data)

View File

@@ -1,217 +0,0 @@
""" API v0 views. """
import logging
from django.contrib.auth import get_user_model
from django.http import Http404
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from rest_framework import status
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.generics import GenericAPIView, ListAPIView
from rest_framework.response import Response
from courseware.access import has_access
from lms.djangoapps.courseware import courses
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
from lms.djangoapps.grades.api.serializers import GradingPolicySerializer
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
from student.roles import CourseStaffRole
log = logging.getLogger(__name__)
USER_MODEL = get_user_model()
@view_auth_classes()
class GradeViewMixin(DeveloperErrorViewMixin):
"""
Mixin class for Grades related views.
"""
def _get_course(self, course_key_string, user, access_action):
"""
Returns the course for the given course_key_string after
verifying the requested access to the course by the given user.
"""
try:
course_key = CourseKey.from_string(course_key_string)
except InvalidKeyError:
raise self.api_error(
status_code=status.HTTP_404_NOT_FOUND,
developer_message='The provided course key cannot be parsed.',
error_code='invalid_course_key'
)
try:
return courses.get_course_with_access(
user,
access_action,
course_key,
check_if_enrolled=True,
)
except Http404:
log.info('Course with ID "%s" not found', course_key_string)
except CourseAccessRedirect:
log.info('User %s does not have access to course with ID "%s"', user.username, course_key_string)
raise self.api_error(
status_code=status.HTTP_404_NOT_FOUND,
developer_message='The user, the course or both do not exist.',
error_code='user_or_course_does_not_exist',
)
def _get_effective_user(self, request, course):
"""
Returns the user object corresponding to the request's 'username' parameter,
or the current request.user if no 'username' was provided.
Verifies that the request.user has access to the requested users's grades.
Returns a 403 error response if access is denied, or a 404 error response if the user does not exist.
"""
# Use the request user's if none provided.
if 'username' in request.GET:
username = request.GET.get('username')
else:
username = request.user.username
if request.user.username == username:
# Any user may request her own grades
return request.user
# Only a user with staff access may request grades for a user other than herself.
if not has_access(request.user, CourseStaffRole.ROLE, course):
log.info(
'User %s tried to access the grade for user %s.',
request.user.username,
username
)
raise self.api_error(
status_code=status.HTTP_403_FORBIDDEN,
developer_message='The user requested does not match the logged in user.',
error_code='user_mismatch'
)
try:
return USER_MODEL.objects.get(username=username)
except USER_MODEL.DoesNotExist:
raise self.api_error(
status_code=status.HTTP_404_NOT_FOUND,
developer_message='The user matching the requested username does not exist.',
error_code='user_does_not_exist'
)
def perform_authentication(self, request):
"""
Ensures that the user is authenticated (e.g. not an AnonymousUser), unless DEBUG mode is enabled.
"""
super(GradeViewMixin, self).perform_authentication(request)
if request.user.is_anonymous:
raise AuthenticationFailed
class UserGradeView(GradeViewMixin, GenericAPIView):
"""
**Use Case**
* Get the current course grades for a user in a course.
The currently logged-in user may request her own grades, or a user with staff access to the course may request
any enrolled user's grades.
**Example Request**
GET /api/grades/v0/course_grade/{course_id}/users/?username={username}
**GET Parameters**
A GET request may include the following parameters.
* course_id: (required) A string representation of a Course ID.
* username: (optional) A string representation of a user's username.
Defaults to the currently logged-in user's username.
**GET Response Values**
If the request for information about the course grade
is successful, an HTTP 200 "OK" response is returned.
The HTTP 200 response has the following values.
* username: A string representation of a user's username passed in the request.
* course_id: A string representation of a Course ID.
* passed: Boolean representing whether the course has been
passed according the course's grading policy.
* percent: A float representing the overall grade for the course
* letter_grade: A letter grade as defined in grading_policy (e.g. 'A' 'B' 'C' for 6.002x) or None
**Example GET Response**
[{
"username": "bob",
"course_key": "edX/DemoX/Demo_Course",
"passed": false,
"percent": 0.03,
"letter_grade": None,
}]
"""
def get(self, request, course_id):
"""
Gets a course progress status.
Args:
request (Request): Django request object.
course_id (string): URI element specifying the course location.
Return:
A JSON serialized representation of the requesting user's current grade status.
"""
course = self._get_course(course_id, request.user, 'load')
grade_user = self._get_effective_user(request, course)
course_grade = CourseGradeFactory().read(grade_user, course)
return Response([{
'username': grade_user.username,
'course_key': course_id,
'passed': course_grade.passed,
'percent': course_grade.percent,
'letter_grade': course_grade.letter_grade,
}])
class CourseGradingPolicy(GradeViewMixin, ListAPIView):
"""
**Use Case**
Get the course grading policy.
**Example requests**:
GET /api/grades/v0/policy/{course_id}/
**Response Values**
* assignment_type: The type of the assignment, as configured by course
staff. For example, course staff might make the assignment types Homework,
Quiz, and Exam.
* count: The number of assignments of the type.
* dropped: Number of assignments of the type that are dropped.
* weight: The weight, or effect, of the assignment type on the learner's
final grade.
"""
allow_empty = False
def get(self, request, course_id, **kwargs):
course = self._get_course(course_id, request.user, 'staff')
return Response(GradingPolicySerializer(course.raw_grader, many=True).data)