Commit 649ccb53 by Zia Fazal Committed by Jonathan Piacenti

cousemodulecompletion implentation

switch to APIView to support url conventions

merged with master
parent 8c411041
""" Django REST Framework Serializers """
from api_manager.models import CourseModuleCompletion
from rest_framework import serializers
class CourseModuleCompletionSerializer(serializers.ModelSerializer):
""" Serializer for CourseModuleCompletion model interactions """
user_id = serializers.Field(source='user.id')
class Meta:
""" Serializer/field specification """
model = CourseModuleCompletion
fields = ('id', 'user_id', 'course_id', 'content_id', 'created', 'modified')
read_only = ('id', 'created')
......@@ -1132,3 +1132,100 @@ class CoursesApiTests(TestCase):
response = self.do_get('{}?enrolled={}&type={}'.format(test_uri_users, 'true', 'project'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
def test_coursemodulecompletions_detail_delete(self):
data = {
'email': 'test@example.com',
'username': 'test_user',
'password': 'test_pass',
'first_name': 'John',
'last_name': 'Doe'
}
response = self.do_post(self.base_users_uri, data)
self.assertEqual(response.status_code, 201)
created_user_id = response.data['id']
detail_uri = '{}/{}/completions/{}/{}'.format(self.base_courses_uri, self.course.id, self.course_content.id,
created_user_id)
response = self.do_post(detail_uri, {})
self.assertEqual(response.status_code, 201)
coursemodulecomp_id = response.data['id']
self.assertGreater(coursemodulecomp_id, 0)
self.assertEqual(response.data['user_id'], created_user_id)
self.assertEqual(response.data['course_id'], self.course.id)
self.assertEqual(response.data['content_id'], self.course_content.id)
self.assertIsNotNone(response.data['created'])
self.assertIsNotNone(response.data['modified'])
# test to create course completion with same attributes
response = self.do_post(detail_uri, {})
self.assertEqual(response.status_code, 409)
# test for delete
response = self.do_delete(detail_uri)
self.assertEqual(response.status_code, 204)
response = self.do_get('{}/{}/completions?user_id={}&content_id={}'.format(self.base_courses_uri,
self.course.id,
created_user_id,
self.course_content.id))
self.assertEqual(response.status_code, 404)
#test deletion of non existing course module completion
non_existing_uri = '{}/{}/completions/{}/{}'.format(self.base_courses_uri, self.course.id,
self.course_content.id, '3323432')
response = self.do_delete(non_existing_uri)
self.assertEqual(response.status_code, 404)
def test_coursemodulecompletions_filters(self):
completion_uri = '{}/{}/completions/'.format(self.base_courses_uri, self.course.id)
for i in xrange(1, 3):
data = {
'email': 'test{}@example.com'.format(i),
'username': 'test_user{}'.format(i),
'password': 'test_pass',
'first_name': 'John{}'.format(i),
'last_name': 'Doe{}'.format(i)
}
response = self.do_post(self.base_users_uri, data)
self.assertEqual(response.status_code, 201)
created_user_id = response.data['id']
for i in xrange(1, 26):
content_id = self.course_content.id + str(i)
response = self.do_post('{}{}/{}'.format(completion_uri, content_id, created_user_id), {})
self.assertEqual(response.status_code, 201)
#filter course module completion by user
user_filter_uri = '{}?user_id={}&page_size=10&page=3'.format(completion_uri, created_user_id)
response = self.do_get(user_filter_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 25)
self.assertEqual(len(response.data['results']), 5)
self.assertEqual(response.data['num_pages'], 3)
#filter course module completion by multiple user ids
user_filter_uri = '{}?user_id={}'.format(completion_uri, str(created_user_id) + ',3,4')
response = self.do_get(user_filter_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 25)
self.assertEqual(len(response.data['results']), 20)
self.assertEqual(response.data['num_pages'], 2)
#filter course module completion by user who has not completed any course module
user_filter_uri = '{}?user_id={}'.format(completion_uri, 1)
response = self.do_get(user_filter_uri)
self.assertEqual(response.status_code, 404)
#filter course module completion by course_id
course_filter_uri = '{}?course_id={}&page_size=10'.format(completion_uri, self.course.id)
response = self.do_get(course_filter_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 25)
self.assertEqual(len(response.data['results']), 10)
#filter course module completion by content_id
content_filter_uri = '{}?content_id={}'.format(completion_uri, self.course_content.id + str(1))
response = self.do_get(content_filter_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 1)
self.assertEqual(len(response.data['results']), 1)
......@@ -26,6 +26,9 @@ urlpatterns = patterns(
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/static_tabs/*$', courses_views.CoursesStaticTabsList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/users/(?P<user_id>[0-9]+)$', courses_views.CoursesUsersDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/users/*$', courses_views.CoursesUsersList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/completions/*$', courses_views.CourseModuleCompletionList.as_view(), name='completion-list'),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/completions/(?P<content_id>[a-zA-Z0-9/_:]+)/(?P<user_id>[0-9]+)$',
courses_views.CourseModuleCompletionDetail.as_view(), name='completion-detail'),
)
urlpatterns = format_suffix_patterns(urlpatterns)
......@@ -8,11 +8,15 @@ from StringIO import StringIO
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.http import Http404
from rest_framework import status
from rest_framework.response import Response
from api_manager.models import CourseGroupRelationship, CourseContentGroupRelationship, GroupProfile
from api_manager.models import CourseGroupRelationship, CourseContentGroupRelationship, GroupProfile, \
CourseModuleCompletion
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
......@@ -21,7 +25,8 @@ 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
from api_manager.permissions import SecureAPIView, SecureListAPIView
from .serializers import CourseModuleCompletionSerializer
log = logging.getLogger(__name__)
......@@ -1055,3 +1060,96 @@ class CourseContentUsersList(SecureAPIView):
serializer = UserSerializer(queryset, many=True)
return Response(serializer.data) # pylint: disable=E1101
class CourseModuleCompletionList(SecureListAPIView):
"""
### The CourseModuleCompletionList allows clients to view user's course module completion entities
to monitor a user's progression throughout the duration of a course,
- URI: ```/api/courses/{course_id}/completions```
- GET: Returns a JSON representation of the course, content and user and timestamps
- GET Example:
{
"count":"1",
"num_pages": "1",
"previous": null
"next": null
"results": [
{
"id": 2,
"user_id": "3",
"course_id": "32fgdf",
"content_id": "324dfgd",
"created": "2014-06-10T13:14:49.878Z",
"modified": "2014-06-10T13:14:49.914Z"
}
]
}
Filters can also be applied
```/api/courses/{course_id}/completions/?user_id={user_id}```
```/api/courses/{course_id}/completions/?content_id={content_id}```
```/api/courses/{course_id}/completions/?user_id={user_id}&content_id={content_id}```
### Use Cases/Notes:
* Use GET operation to retrieve list of course completions by user
* Use GET operation to verify user has completed specific course module
"""
serializer_class = CourseModuleCompletionSerializer
def get_queryset(self):
"""
GET /api/courses/{course_id}/completions/
"""
user_ids = self.request.QUERY_PARAMS.get('user_id', None)
content_id = self.request.QUERY_PARAMS.get('content_id', None)
course_id = self.kwargs['course_id']
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]
queryset = queryset.filter(user__in=user_ids)
if content_id:
queryset = queryset.filter(content_id=content_id)
if not queryset.exists() and (user_ids or content_id):
raise Http404
return queryset
class CourseModuleCompletionDetail(SecureAPIView):
"""
### The CourseModuleCompletionDetail view allows clients to interact with a
specific Course-Content completion entity
- URI: ```/api/courses/{course_id}/completions/{content_id}/{user_id}```
- POST: Creates a Course-Module completion entity
- DELETE: Removes an existing Course-Module completion entity from the system
### Use Cases/Notes:
* Use this operation to save or remove Course-Module completion entity
"""
def post(self, request, course_id, content_id, user_id):
"""
POST /api/courses/{course_id}/completions/{content_id}/{user_id}
"""
completion, created = CourseModuleCompletion.objects.get_or_create(user_id=user_id,
course_id=course_id,
content_id=content_id)
serializer = CourseModuleCompletionSerializer(completion)
if created:
return Response(serializer.data, status=status.HTTP_201_CREATED) # pylint: disable=E1101
else:
return Response({'message': _('Resource already exists')}, status=status.HTTP_409_CONFLICT)
def delete(self, request, course_id, content_id, user_id):
"""
DELETE /api/courses/{course_id}/completions/{content_id}/{user_id}
"""
try:
completion = CourseModuleCompletion.objects.get(user_id=user_id, course_id=course_id, content_id=content_id)
completion.delete()
except ObjectDoesNotExist:
raise Http404
response_data = {'uri': _generate_base_uri(request)}
return Response(response_data, status=status.HTTP_204_NO_CONTENT)
......@@ -138,3 +138,14 @@ class Organization(TimeStampedModel):
name = models.CharField(max_length=255)
workgroups = models.ManyToManyField(Workgroup, related_name="organizations")
users = models.ManyToManyField(User, related_name="organizations")
class CourseModuleCompletion(TimeStampedModel):
"""
The CourseModuleCompletion model contains user, course, module information
to monitor a user's progression throughout the duration of a course,
we need to observe and record completions of the individual course modules.
"""
user = models.ForeignKey(User, db_index=True, related_name="course_completions")
course_id = models.CharField(max_length=255, db_index=True)
content_id = models.CharField(max_length=255, db_index=True)
......@@ -7,6 +7,7 @@ from api_manager.utils import get_client_ip_address, address_exists_in_network
from rest_framework import permissions, generics, filters, pagination, serializers
from rest_framework.views import APIView
log = logging.getLogger(__name__)
......@@ -87,10 +88,11 @@ class IdsInFilterBackend(filters.BaseFilterBackend):
Parse querystring to get ids and the filter the queryset
Max of 100 values are allowed for performance reasons
"""
upper_bound = getattr(settings, 'API_LOOKUP_UPPER_BOUND', 100)
ids = request.QUERY_PARAMS.get('ids')
if ids:
if ',' in ids:
ids = ids.split(",")[:100]
ids = ids.split(",")[:upper_bound]
return queryset.filter(id__in=ids)
return queryset
......@@ -106,13 +108,35 @@ class SecureAPIView(APIView):
permission_classes = (ApiKeyHeaderPermission, )
class SecureListAPIView(generics.ListAPIView):
class PermissionMixin(object):
"""
Inherited from ListAPIView
Mixin to set custom permission_classes
"""
permission_classes = (ApiKeyHeaderPermission, IPAddressRestrictedPermission)
class FilterBackendMixin(object):
"""
Mixin to set custom filter_backends
"""
filter_backends = (filters.DjangoFilterBackend, IdsInFilterBackend,)
class PaginationMixin(object):
"""
Mixin to set custom pagination support
"""
pagination_serializer_class = CustomPaginationSerializer
paginate_by = getattr(settings, 'API_PAGE_SIZE', 20)
paginate_by_param = 'page_size'
max_paginate_by = 100
class SecureListAPIView(PermissionMixin,
FilterBackendMixin,
PaginationMixin,
generics.ListAPIView):
"""
Inherited from ListAPIView
"""
pass
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