Commit 17dd85f6 by Matt Drayer Committed by Jonathan Piacenti

mattdrayer/api-coursegrades: Added support for viewing a set of grades for a…

mattdrayer/api-coursegrades: Added support for viewing a set of grades for a course.  Includes filters/aggregations.
parent e98e2aba
......@@ -13,3 +13,8 @@ class CourseModuleCompletionSerializer(serializers.ModelSerializer):
model = CourseModuleCompletion
fields = ('id', 'user_id', 'course_id', 'content_id', 'created', 'modified')
read_only = ('id', 'created')
class GradeSerializer(serializers.Serializer):
""" Serializer for model interactions """
grade = serializers.Field()
......@@ -12,14 +12,18 @@ from django.core.cache import cache
from django.test import TestCase, Client
from django.test.utils import override_settings
from capa.tests.response_xml_factory import StringResponseXMLFactory
from courseware.tests.factories import StudentModuleFactory
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore import Location
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from .content import TEST_COURSE_OVERVIEW_CONTENT, TEST_COURSE_UPDATES_CONTENT, TEST_COURSE_UPDATES_CONTENT_LEGACY
from .content import TEST_STATIC_TAB1_CONTENT, TEST_STATIC_TAB2_CONTENT
TEST_API_KEY = str(uuid.uuid4())
USER_COUNT = 4
class SecureClient(Client):
""" Django test client using a "secure" connection. """
......@@ -40,6 +44,7 @@ class CoursesApiTests(TestCase):
self.base_groups_uri = '/api/groups'
self.base_users_uri = '/api/users'
self.test_group_name = 'Alpha Group'
self.attempts = 3
self.course = CourseFactory.create(
start="2014-06-16T14:30:00Z",
......@@ -111,6 +116,58 @@ class CoursesApiTests(TestCase):
display_name="readings"
)
self.sub_section = ItemFactory.create(
parent_location=self.course_content.location,
category="sequential",
display_name=u"test subsection",
)
unit = ItemFactory.create(
parent_location=self.sub_section.location,
category="vertical",
metadata={'graded': True, 'format': 'Homework'},
display_name=u"test unit",
)
self.users = [UserFactory.create(username="testuser" + str(__)) for __ in xrange(USER_COUNT)]
for user in self.users:
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
for i in xrange(USER_COUNT - 1):
category = 'mentoring'
module_type = 'mentoring'
if i % 2 is 0:
category = 'group-project'
module_type = 'group-project'
self.item = ItemFactory.create(
parent_location=unit.location,
category=category,
data=StringResponseXMLFactory().build_xml(answer='foo'),
metadata={'rerandomize': 'always'},
display_name=u"test problem" + str(i)
)
for j, user in enumerate(self.users):
the_grade = j * 0.75
StudentModuleFactory.create(
grade=the_grade,
max_grade=1 if i < j else 0.5,
student=user,
course_id=self.course.id,
module_state_key=Location(self.item.location).url(),
state=json.dumps({'attempts': self.attempts}),
module_type=module_type
)
for j, user in enumerate(self.users):
StudentModuleFactory.create(
course_id=self.course.id,
module_type='sequential',
module_state_key=Location(self.item.location).url(),
)
self.test_course_id = self.course.id
self.test_bogus_course_id = 'foo/bar/baz'
self.test_course_name = self.course.display_name
......@@ -682,7 +739,8 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.status_code, 404)
def test_courses_users_list_get_no_students(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
course = CourseFactory.create(display_name="TEST COURSE", org='TESTORG')
test_uri = self.base_courses_uri + '/' + course.id + '/users'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
......@@ -712,7 +770,8 @@ class CoursesApiTests(TestCase):
self.assertGreater(len(response.data), 0)
def test_courses_users_list_post_nonexisting_user_allow(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
course = CourseFactory.create(display_name="TEST COURSE", org='TESTORG2')
test_uri = self.base_courses_uri + '/' + course.id + '/users'
post_data = {}
post_data['email'] = 'test+pending@tester.com'
post_data['allow_pending'] = True
......@@ -1229,3 +1288,48 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 1)
self.assertEqual(len(response.data['results']), 1)
def test_courses_grades_list_get(self):
# Retrieve the list of grades for this course
# All the course/item/user scaffolding was handled in Setup
test_uri = '{}/{}/grades'.format(self.base_courses_uri, self.test_course_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(response.data['average_grade'], 0)
self.assertGreater(response.data['points_scored'], 0)
self.assertGreater(response.data['points_possible'], 0)
self.assertGreater(response.data['course_average_grade'], 0)
self.assertGreater(response.data['course_points_scored'], 0)
self.assertGreater(response.data['course_points_possible'], 0)
self.assertGreater(len(response.data['grades']), 0)
# Filter by user_id
user_filter_uri = '{}?user_id=1,3'.format(test_uri)
response = self.do_get(user_filter_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(response.data['average_grade'], 0)
self.assertGreater(response.data['points_scored'], 0)
self.assertGreater(response.data['points_possible'], 0)
self.assertGreater(response.data['course_average_grade'], 0)
self.assertGreater(response.data['course_points_scored'], 0)
self.assertGreater(response.data['course_points_possible'], 0)
self.assertGreater(len(response.data['grades']), 0)
# Filter by content_id
content_filter_uri = '{}?content_id={}'.format(test_uri, Location(self.item.location).url())
response = self.do_get(content_filter_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(response.data['average_grade'], 0)
self.assertGreater(response.data['points_scored'], 0)
self.assertGreater(response.data['points_possible'], 0)
self.assertGreater(response.data['course_average_grade'], 0)
self.assertGreater(response.data['course_points_scored'], 0)
self.assertGreater(response.data['course_points_possible'], 0)
self.assertGreater(len(response.data['grades']), 0)
def test_courses_grades_list_get_invalid_course(self):
# Retrieve the list of grades for this course
# All the course/item/user scaffolding was handled in Setup
test_uri = '{}/{}/grades'.format(self.base_courses_uri, self.test_bogus_course_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
......@@ -18,6 +18,7 @@ urlpatterns = patterns(
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/content/(?P<content_id>[a-zA-Z0-9/_:]+)/users/*$', courses_views.CourseContentUsersList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/content/(?P<content_id>[a-zA-Z0-9/_:]+)$', courses_views.CourseContentDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/content/*$', courses_views.CourseContentList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/grades/*$', courses_views.CoursesGradesList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/groups/(?P<group_id>[0-9]+)$', courses_views.CoursesGroupsDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/groups/*$', courses_views.CoursesGroupsList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/overview/*$', courses_views.CoursesOverview.as_view()),
......
......@@ -6,29 +6,33 @@ import itertools
from lxml import etree
from StringIO import StringIO
from django.conf import settings
from django.contrib.auth.models import Group, User
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from django.db.models import Avg, Sum
from django.http import Http404
from django.utils.translation import ugettext_lazy as _
from rest_framework import status
from rest_framework.response import Response
from api_manager.models import CourseGroupRelationship, CourseContentGroupRelationship, GroupProfile, \
CourseModuleCompletion
from api_manager.permissions import SecureAPIView, SecureListAPIView
from api_manager.users.serializers import UserSerializer
from courseware import module_render
from courseware.courses import get_course, get_course_about_section, get_course_info_section
from courseware.model_data import FieldDataCache
from courseware.models import StudentModule
from courseware.views import get_static_tab_contents
from student.models import CourseEnrollment, CourseEnrollmentAllowed
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location, InvalidLocationError
from api_manager.permissions import SecureAPIView, SecureListAPIView
from api_manager.utils import generate_base_uri
from .serializers import CourseModuleCompletionSerializer
from .serializers import CourseModuleCompletionSerializer
from .serializers import GradeSerializer
log = logging.getLogger(__name__)
......@@ -1099,8 +1103,7 @@ class CourseModuleCompletionList(SecureListAPIView):
queryset = CourseModuleCompletion.objects.filter(course_id=course_id)
upper_bound = getattr(settings, 'API_LOOKUP_UPPER_BOUND', 100)
if user_ids:
if ',' in user_ids:
user_ids = user_ids.split(",")[:upper_bound]
user_ids = map(int, user_ids.split(','))[:upper_bound]
queryset = queryset.filter(user__in=user_ids)
if content_id:
......@@ -1127,3 +1130,70 @@ class CourseModuleCompletionList(SecureListAPIView):
return Response(serializer.data, status=status.HTTP_201_CREATED) # pylint: disable=E1101
else:
return Response({'message': _('Resource already exists')}, status=status.HTTP_409_CONFLICT)
class CoursesGradesList(SecureListAPIView):
"""
### The CoursesGradesList view allows clients to retrieve a list of grades for the specified Course
- URI: ```/api/courses/{course_id}/grades/```
- GET: Returns a JSON representation (array) of the set of grade objects
### Use Cases/Notes:
* Example: Display a graph of all of the grades awarded for a given course
"""
def get(self, request, course_id):
"""
GET /api/courses/{course_id}/grades?user_ids=1,2&content_ids=i4x://1/2/3,i4x://a/b/c
"""
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if not existing_course:
return Response({}, status=status.HTTP_404_NOT_FOUND)
queryset = StudentModule.objects.filter(
course_id__exact=course_id,
grade__isnull=False,
max_grade__isnull=False,
max_grade__gt=0
)
upper_bound = getattr(settings, 'API_LOOKUP_UPPER_BOUND', 100)
user_ids = self.request.QUERY_PARAMS.get('user_id', None)
if user_ids:
user_ids = map(int, user_ids.split(','))[:upper_bound]
queryset = queryset.filter(student__in=user_ids)
content_id = self.request.QUERY_PARAMS.get('content_id', None)
if content_id:
queryset = queryset.filter(module_state_key=content_id)
queryset_grade_avg = queryset.aggregate(Avg('grade'))
queryset_grade_sum = queryset.aggregate(Sum('grade'))
queryset_maxgrade_sum = queryset.aggregate(Sum('max_grade'))
course_queryset = StudentModule.objects.filter(
course_id__exact=course_id,
grade__isnull=False,
max_grade__isnull=False,
max_grade__gt=0
)
course_queryset_grade_avg = course_queryset.aggregate(Avg('grade'))
course_queryset_grade_sum = course_queryset.aggregate(Sum('grade'))
course_queryset_maxgrade_sum = course_queryset.aggregate(Sum('max_grade'))
response_data = {}
base_uri = generate_base_uri(request)
response_data['uri'] = base_uri
response_data['average_grade'] = queryset_grade_avg['grade__avg']
response_data['points_scored'] = queryset_grade_sum['grade__sum']
response_data['points_possible'] = queryset_maxgrade_sum['max_grade__sum']
response_data['course_average_grade'] = course_queryset_grade_avg['grade__avg']
response_data['course_points_scored'] = course_queryset_grade_sum['grade__sum']
response_data['course_points_possible'] = course_queryset_maxgrade_sum['max_grade__sum']
response_data['grades'] = []
for row in queryset:
serializer = GradeSerializer(row)
response_data['grades'].append(serializer.data)
return Response(response_data, status=status.HTTP_200_OK)
......@@ -40,6 +40,11 @@ class GroupSerializer(serializers.HyperlinkedModelSerializer):
fields = ('id', 'url', 'name')
class GradeSerializer(serializers.Serializer):
""" Serializer for model interactions """
grade = serializers.Field()
class ProjectSerializer(serializers.HyperlinkedModelSerializer):
""" Serializer for model interactions """
workgroups = serializers.PrimaryKeyRelatedField(many=True, required=False)
......
......@@ -9,11 +9,13 @@ import uuid
from django.contrib.auth.models import Group, User
from django.core.cache import cache
from django.test import TestCase, Client
from django.test import Client
from django.test.utils import override_settings
from api_manager.models import GroupProfile
from projects.models import Project
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
TEST_API_KEY = str(uuid.uuid4())
......@@ -29,21 +31,37 @@ class SecureClient(Client):
@override_settings(EDX_API_KEY=TEST_API_KEY)
class WorkgroupsApiTests(TestCase):
class WorkgroupsApiTests(ModuleStoreTestCase):
""" Test suite for Users API views """
def setUp(self):
super(WorkgroupsApiTests, self).setUp()
self.test_server_prefix = 'https://testserver'
self.test_workgroups_uri = '/api/workgroups/'
self.test_course_id = 'edx/demo/course'
self.test_bogus_course_id = 'foo/bar/baz'
self.test_course_content_id = "i4x://blah"
self.test_bogus_course_content_id = "14x://foo/bar/baz"
self.test_group_id = '1'
self.test_bogus_group_id = "2131241123"
self.test_workgroup_name = str(uuid.uuid4())
self.test_course = CourseFactory.create(
start="2014-06-16T14:30:00Z",
end="2015-01-16T14:30:00Z"
)
self.test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
self.test_group_project = ItemFactory.create(
category="group_project",
parent_location=self.test_course.location,
data=self.test_data,
due="2014-05-16T14:30:00Z",
display_name="Group Project"
)
self.test_course_id = self.test_course.id
self.test_course_content_id = self.test_group_project.id
self.test_group_name = str(uuid.uuid4())
self.test_group = Group.objects.create(
name=self.test_group_name
......@@ -66,6 +84,13 @@ class WorkgroupsApiTests(TestCase):
username=self.test_user_username
)
self.test_user_email2 = str(uuid.uuid4())
self.test_user_username2 = str(uuid.uuid4())
self.test_user2 = User.objects.create(
email=self.test_user_email2,
username=self.test_user_username2
)
self.client = SecureClient()
cache.clear()
......@@ -300,6 +325,97 @@ class WorkgroupsApiTests(TestCase):
self.assertEqual(response.data[0]['id'], submission_id)
self.assertEqual(response.data[0]['user'], self.test_user.id)
def test_workgroups_grades_post(self):
data = {
'name': self.test_workgroup_name,
'project': self.test_project.id
}
response = self.do_post(self.test_workgroups_uri, data)
self.assertEqual(response.status_code, 201)
workgroup_id = response.data['id']
users_uri = '{}{}/users/'.format(self.test_workgroups_uri, workgroup_id)
data = {"id": self.test_user.id}
response = self.do_post(users_uri, data)
self.assertEqual(response.status_code, 201)
data = {"id": self.test_user2.id}
response = self.do_post(users_uri, data)
self.assertEqual(response.status_code, 201)
grade_data = {
'course_id': self.test_course_id,
'content_id': self.test_course_content_id,
'grade': 0.85,
'max_grade': 0.75,
}
grades_uri = '{}{}/grades/'.format(self.test_workgroups_uri, workgroup_id)
response = self.do_post(grades_uri, grade_data)
self.assertEqual(response.status_code, 201)
# Confirm the grades for the users
course_grades_uri = '/api/courses/{}/grades'.format(self.test_course_id)
response = self.do_get(course_grades_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data['grades']), 0)
def test_workgroups_grades_post_invalid_requests(self):
data = {
'name': self.test_workgroup_name,
'project': self.test_project.id
}
response = self.do_post(self.test_workgroups_uri, data)
self.assertEqual(response.status_code, 201)
workgroup_id = response.data['id']
users_uri = '{}{}/users/'.format(self.test_workgroups_uri, workgroup_id)
data = {"id": self.test_user.id}
response = self.do_post(users_uri, data)
self.assertEqual(response.status_code, 201)
data = {"id": self.test_user2.id}
response = self.do_post(users_uri, data)
self.assertEqual(response.status_code, 201)
grades_uri = '{}{}/grades/'.format(self.test_workgroups_uri, workgroup_id)
grade_data = {
'content_id': self.test_course_content_id,
'grade': 0.85,
'max_grade': 0.75,
}
response = self.do_post(grades_uri, grade_data)
self.assertEqual(response.status_code, 400)
grade_data = {
'course_id': self.test_bogus_course_id,
'content_id': self.test_course_content_id,
'grade': 0.85,
'max_grade': 0.75,
}
response = self.do_post(grades_uri, grade_data)
self.assertEqual(response.status_code, 400)
grade_data = {
'course_id': self.test_course_id,
'grade': 0.85,
'max_grade': 0.75,
}
response = self.do_post(grades_uri, grade_data)
self.assertEqual(response.status_code, 400)
grade_data = {
'course_id': self.test_course_id,
'content_id': self.test_course_content_id,
'max_grade': 0.75,
}
response = self.do_post(grades_uri, grade_data)
self.assertEqual(response.status_code, 400)
grade_data = {
'course_id': self.test_course_id,
'content_id': self.test_course_content_id,
'grade': 0.85,
}
response = self.do_post(grades_uri, grade_data)
self.assertEqual(response.status_code, 400)
def test_submissions_list_post_invalid_relationships(self):
data = {
'name': self.test_workgroup_name,
......
......@@ -10,6 +10,13 @@ from rest_framework.decorators import action, link
from rest_framework import status
from rest_framework.response import Response
from xblock.fields import Scope
from xblock.runtime import KeyValueStore
from courseware.courses import get_course
from courseware.model_data import FieldDataCache
from xmodule.modulestore import Location
from .models import Project, Workgroup, WorkgroupSubmission
from .models import WorkgroupReview, WorkgroupSubmissionReview, WorkgroupPeerReview
from .serializers import UserSerializer, GroupSerializer
......@@ -130,6 +137,51 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
response_data.append(serializer.data)
return Response(response_data, status=status.HTTP_200_OK)
@action()
def grades(self, request, pk):
"""
Submit a grade for a Workgroup. The grade will be applied to all members of the workgroup
"""
# Ensure we received all of the necessary information
course_id = request.DATA.get('course_id')
if course_id is None:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
try:
course_descriptor = get_course(course_id)
except ValueError:
course_descriptor = None
if not course_descriptor:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
content_id = request.DATA.get('content_id')
if content_id is None:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
grade = request.DATA.get('grade')
if grade is None:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
max_grade = request.DATA.get('max_grade')
if max_grade is None:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
if grade > max_grade:
max_grade = grade
users = User.objects.filter(workgroups=pk)
for user in users:
key = KeyValueStore.Key(
scope=Scope.user_state,
user_id=user.id,
block_scope_id=Location(content_id),
field_name='grade'
)
field_data_cache = FieldDataCache([course_descriptor], course_id, user)
student_module = field_data_cache.find_or_create(key)
student_module.grade = grade
student_module.max_grade = max_grade
student_module.save()
return Response({}, status=status.HTTP_201_CREATED)
class ProjectsViewSet(viewsets.ModelViewSet):
"""
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment