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): ...@@ -13,3 +13,8 @@ class CourseModuleCompletionSerializer(serializers.ModelSerializer):
model = CourseModuleCompletion model = CourseModuleCompletion
fields = ('id', 'user_id', 'course_id', 'content_id', 'created', 'modified') fields = ('id', 'user_id', 'course_id', 'content_id', 'created', 'modified')
read_only = ('id', 'created') 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 ...@@ -12,14 +12,18 @@ from django.core.cache import cache
from django.test import TestCase, Client from django.test import TestCase, Client
from django.test.utils import override_settings 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 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 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_COURSE_OVERVIEW_CONTENT, TEST_COURSE_UPDATES_CONTENT, TEST_COURSE_UPDATES_CONTENT_LEGACY
from .content import TEST_STATIC_TAB1_CONTENT, TEST_STATIC_TAB2_CONTENT from .content import TEST_STATIC_TAB1_CONTENT, TEST_STATIC_TAB2_CONTENT
TEST_API_KEY = str(uuid.uuid4()) TEST_API_KEY = str(uuid.uuid4())
USER_COUNT = 4
class SecureClient(Client): class SecureClient(Client):
""" Django test client using a "secure" connection. """ """ Django test client using a "secure" connection. """
...@@ -40,6 +44,7 @@ class CoursesApiTests(TestCase): ...@@ -40,6 +44,7 @@ class CoursesApiTests(TestCase):
self.base_groups_uri = '/api/groups' self.base_groups_uri = '/api/groups'
self.base_users_uri = '/api/users' self.base_users_uri = '/api/users'
self.test_group_name = 'Alpha Group' self.test_group_name = 'Alpha Group'
self.attempts = 3
self.course = CourseFactory.create( self.course = CourseFactory.create(
start="2014-06-16T14:30:00Z", start="2014-06-16T14:30:00Z",
...@@ -111,6 +116,58 @@ class CoursesApiTests(TestCase): ...@@ -111,6 +116,58 @@ class CoursesApiTests(TestCase):
display_name="readings" 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_course_id = self.course.id
self.test_bogus_course_id = 'foo/bar/baz' self.test_bogus_course_id = 'foo/bar/baz'
self.test_course_name = self.course.display_name self.test_course_name = self.course.display_name
...@@ -682,7 +739,8 @@ class CoursesApiTests(TestCase): ...@@ -682,7 +739,8 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_courses_users_list_get_no_students(self): 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) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0) self.assertGreater(len(response.data), 0)
...@@ -712,7 +770,8 @@ class CoursesApiTests(TestCase): ...@@ -712,7 +770,8 @@ class CoursesApiTests(TestCase):
self.assertGreater(len(response.data), 0) self.assertGreater(len(response.data), 0)
def test_courses_users_list_post_nonexisting_user_allow(self): 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 = {}
post_data['email'] = 'test+pending@tester.com' post_data['email'] = 'test+pending@tester.com'
post_data['allow_pending'] = True post_data['allow_pending'] = True
...@@ -1229,3 +1288,48 @@ class CoursesApiTests(TestCase): ...@@ -1229,3 +1288,48 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 1) self.assertEqual(response.data['count'], 1)
self.assertEqual(len(response.data['results']), 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( ...@@ -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/_:]+)/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/(?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>[^/]+/[^/]+/[^/]+)/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/(?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>[^/]+/[^/]+/[^/]+)/groups/*$', courses_views.CoursesGroupsList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/overview/*$', courses_views.CoursesOverview.as_view()), url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/overview/*$', courses_views.CoursesOverview.as_view()),
......
...@@ -6,29 +6,33 @@ import itertools ...@@ -6,29 +6,33 @@ import itertools
from lxml import etree from lxml import etree
from StringIO import StringIO from StringIO import StringIO
from django.conf import settings
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext_lazy as _ from django.db.models import Avg, Sum
from django.conf import settings
from django.http import Http404 from django.http import Http404
from django.utils.translation import ugettext_lazy as _
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from api_manager.models import CourseGroupRelationship, CourseContentGroupRelationship, GroupProfile, \ from api_manager.models import CourseGroupRelationship, CourseContentGroupRelationship, GroupProfile, \
CourseModuleCompletion CourseModuleCompletion
from api_manager.permissions import SecureAPIView, SecureListAPIView
from api_manager.users.serializers import UserSerializer from api_manager.users.serializers import UserSerializer
from courseware import module_render from courseware import module_render
from courseware.courses import get_course, get_course_about_section, get_course_info_section from courseware.courses import get_course, get_course_about_section, get_course_info_section
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from courseware.models import StudentModule
from courseware.views import get_static_tab_contents from courseware.views import get_static_tab_contents
from student.models import CourseEnrollment, CourseEnrollmentAllowed from student.models import CourseEnrollment, CourseEnrollmentAllowed
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location, InvalidLocationError from xmodule.modulestore import Location, InvalidLocationError
from api_manager.permissions import SecureAPIView, SecureListAPIView from api_manager.permissions import SecureAPIView, SecureListAPIView
from api_manager.utils import generate_base_uri from api_manager.utils import generate_base_uri
from .serializers import CourseModuleCompletionSerializer
from .serializers import CourseModuleCompletionSerializer
from .serializers import GradeSerializer
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -1099,8 +1103,7 @@ class CourseModuleCompletionList(SecureListAPIView): ...@@ -1099,8 +1103,7 @@ class CourseModuleCompletionList(SecureListAPIView):
queryset = CourseModuleCompletion.objects.filter(course_id=course_id) queryset = CourseModuleCompletion.objects.filter(course_id=course_id)
upper_bound = getattr(settings, 'API_LOOKUP_UPPER_BOUND', 100) upper_bound = getattr(settings, 'API_LOOKUP_UPPER_BOUND', 100)
if user_ids: if user_ids:
if ',' in user_ids: user_ids = map(int, user_ids.split(','))[:upper_bound]
user_ids = user_ids.split(",")[:upper_bound]
queryset = queryset.filter(user__in=user_ids) queryset = queryset.filter(user__in=user_ids)
if content_id: if content_id:
...@@ -1127,3 +1130,70 @@ class CourseModuleCompletionList(SecureListAPIView): ...@@ -1127,3 +1130,70 @@ class CourseModuleCompletionList(SecureListAPIView):
return Response(serializer.data, status=status.HTTP_201_CREATED) # pylint: disable=E1101 return Response(serializer.data, status=status.HTTP_201_CREATED) # pylint: disable=E1101
else: else:
return Response({'message': _('Resource already exists')}, status=status.HTTP_409_CONFLICT) 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): ...@@ -40,6 +40,11 @@ class GroupSerializer(serializers.HyperlinkedModelSerializer):
fields = ('id', 'url', 'name') fields = ('id', 'url', 'name')
class GradeSerializer(serializers.Serializer):
""" Serializer for model interactions """
grade = serializers.Field()
class ProjectSerializer(serializers.HyperlinkedModelSerializer): class ProjectSerializer(serializers.HyperlinkedModelSerializer):
""" Serializer for model interactions """ """ Serializer for model interactions """
workgroups = serializers.PrimaryKeyRelatedField(many=True, required=False) workgroups = serializers.PrimaryKeyRelatedField(many=True, required=False)
......
...@@ -9,11 +9,13 @@ import uuid ...@@ -9,11 +9,13 @@ import uuid
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.core.cache import cache 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 django.test.utils import override_settings
from api_manager.models import GroupProfile from api_manager.models import GroupProfile
from projects.models import Project 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()) TEST_API_KEY = str(uuid.uuid4())
...@@ -29,21 +31,37 @@ class SecureClient(Client): ...@@ -29,21 +31,37 @@ class SecureClient(Client):
@override_settings(EDX_API_KEY=TEST_API_KEY) @override_settings(EDX_API_KEY=TEST_API_KEY)
class WorkgroupsApiTests(TestCase): class WorkgroupsApiTests(ModuleStoreTestCase):
""" Test suite for Users API views """ """ Test suite for Users API views """
def setUp(self): def setUp(self):
super(WorkgroupsApiTests, self).setUp()
self.test_server_prefix = 'https://testserver' self.test_server_prefix = 'https://testserver'
self.test_workgroups_uri = '/api/workgroups/' self.test_workgroups_uri = '/api/workgroups/'
self.test_course_id = 'edx/demo/course'
self.test_bogus_course_id = 'foo/bar/baz' 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_bogus_course_content_id = "14x://foo/bar/baz"
self.test_group_id = '1' self.test_group_id = '1'
self.test_bogus_group_id = "2131241123" self.test_bogus_group_id = "2131241123"
self.test_workgroup_name = str(uuid.uuid4()) 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_name = str(uuid.uuid4())
self.test_group = Group.objects.create( self.test_group = Group.objects.create(
name=self.test_group_name name=self.test_group_name
...@@ -66,6 +84,13 @@ class WorkgroupsApiTests(TestCase): ...@@ -66,6 +84,13 @@ class WorkgroupsApiTests(TestCase):
username=self.test_user_username 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() self.client = SecureClient()
cache.clear() cache.clear()
...@@ -300,6 +325,97 @@ class WorkgroupsApiTests(TestCase): ...@@ -300,6 +325,97 @@ class WorkgroupsApiTests(TestCase):
self.assertEqual(response.data[0]['id'], submission_id) self.assertEqual(response.data[0]['id'], submission_id)
self.assertEqual(response.data[0]['user'], self.test_user.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): def test_submissions_list_post_invalid_relationships(self):
data = { data = {
'name': self.test_workgroup_name, 'name': self.test_workgroup_name,
......
...@@ -10,6 +10,13 @@ from rest_framework.decorators import action, link ...@@ -10,6 +10,13 @@ from rest_framework.decorators import action, link
from rest_framework import status from rest_framework import status
from rest_framework.response import Response 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 Project, Workgroup, WorkgroupSubmission
from .models import WorkgroupReview, WorkgroupSubmissionReview, WorkgroupPeerReview from .models import WorkgroupReview, WorkgroupSubmissionReview, WorkgroupPeerReview
from .serializers import UserSerializer, GroupSerializer from .serializers import UserSerializer, GroupSerializer
...@@ -130,6 +137,51 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -130,6 +137,51 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
response_data.append(serializer.data) response_data.append(serializer.data)
return Response(response_data, status=status.HTTP_200_OK) 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): 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