Commit 113e0af1 by Albert St. Aubin

Added Entitlement API

The Entitlement API will allow for GET, POST, DELETE(Revoke), and
variations of these methods of managing and retrieving data about
Learner Entitlements.
[LEARNER-2661]
parent e503ed86
from django.contrib import admin
from .models import CourseEntitlement
......
from django.conf.urls import include, url
urlpatterns = [
url(r'^v1/', include('entitlements.api.v1.urls', namespace='v1')),
]
from django_filters import rest_framework as filters
from entitlements.models import CourseEntitlement
class CharListFilter(filters.CharFilter):
""" Filters a field via a comma-delimited list of values. """
def filter(self, qs, value): # pylint: disable=method-hidden
if value not in (None, ''):
value = value.split(',')
return super(CharListFilter, self).filter(qs, value)
class UUIDListFilter(CharListFilter):
""" Filters a field via a comma-delimited list of UUIDs. """
def __init__(self, name='uuid', label=None, widget=None, method=None, lookup_expr='in', required=False,
distinct=False, exclude=False, **kwargs):
super(UUIDListFilter, self).__init__(
name=name,
label=label,
widget=widget,
method=method,
lookup_expr=lookup_expr,
required=required,
distinct=distinct,
exclude=exclude,
**kwargs
)
class CourseEntitlementFilter(filters.FilterSet):
uuid = UUIDListFilter()
course_uuid = UUIDListFilter()
class Meta:
model = CourseEntitlement
fields = ('uuid',)
from django.contrib.auth import get_user_model
from rest_framework import serializers
from entitlements.models import CourseEntitlement
class CourseEntitlementSerializer(serializers.ModelSerializer):
user = serializers.SlugRelatedField(slug_field='username', queryset=get_user_model().objects.all())
class Meta:
model = CourseEntitlement
fields = (
'user',
'uuid',
'course_uuid',
'expired_at',
'created',
'modified',
'mode',
'order_number'
)
import unittest
from django.conf import settings
from django.test import RequestFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from entitlements.api.v1.serializers import CourseEntitlementSerializer
from entitlements.tests.factories import CourseEntitlementFactory
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EntitlementsSerializerTests(ModuleStoreTestCase):
def setUp(self):
super(EntitlementsSerializerTests, self).setUp()
def test_data(self):
entitlement = CourseEntitlementFactory()
request = RequestFactory().get('')
serializer = CourseEntitlementSerializer(entitlement, context={'request': request})
expected = {
'user': entitlement.user.username,
'uuid': str(entitlement.uuid),
'expired_at': entitlement.expired_at,
'course_uuid': str(entitlement.course_uuid),
'mode': entitlement.mode,
'order_number': entitlement.order_number,
'created': entitlement.created.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
'modified': entitlement.modified.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
}
assert serializer.data == expected
import json
import unittest
import uuid
from django.conf import settings
from django.core.urlresolvers import reverse
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from entitlements.tests.factories import CourseEntitlementFactory
from entitlements.models import CourseEntitlement
from entitlements.api.v1.serializers import CourseEntitlementSerializer
from student.tests.factories import CourseEnrollmentFactory, UserFactory, TEST_PASSWORD
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EntitlementViewSetTest(ModuleStoreTestCase):
ENTITLEMENTS_DETAILS_PATH = 'entitlements_api:v1:entitlements-detail'
def setUp(self):
super(EntitlementViewSetTest, self).setUp()
self.user = UserFactory(is_staff=True)
self.client.login(username=self.user.username, password=TEST_PASSWORD)
self.course = CourseFactory()
self.entitlements_list_url = reverse('entitlements_api:v1:entitlements-list')
def _get_data_set(self, user, course_uuid):
"""
Get a basic data set for an entitlement
"""
return {
"user": user.username,
"mode": "verified",
"course_uuid": course_uuid,
"order_number": "EDX-1001"
}
def test_auth_required(self):
self.client.logout()
response = self.client.get(self.entitlements_list_url)
assert response.status_code == 401
def test_staff_user_required(self):
not_staff_user = UserFactory()
self.client.login(username=not_staff_user.username, password=UserFactory._DEFAULT_PASSWORD)
response = self.client.get(self.entitlements_list_url)
assert response.status_code == 403
def test_add_entitlement_with_missing_data(self):
entitlement_data_missing_parts = self._get_data_set(self.user, str(uuid.uuid4()))
entitlement_data_missing_parts.pop('mode')
entitlement_data_missing_parts.pop('course_uuid')
response = self.client.post(
self.entitlements_list_url,
data=json.dumps(entitlement_data_missing_parts),
content_type='application/json',
)
assert response.status_code == 400
def test_add_entitlement(self):
course_uuid = uuid.uuid4()
entitlement_data = self._get_data_set(self.user, str(course_uuid))
response = self.client.post(
self.entitlements_list_url,
data=json.dumps(entitlement_data),
content_type='application/json',
)
assert response.status_code == 201
results = response.data
course_entitlement = CourseEntitlement.objects.get(
user=self.user,
course_uuid=course_uuid
)
assert results == CourseEntitlementSerializer(course_entitlement).data
def test_get_entitlements(self):
entitlements = CourseEntitlementFactory.create_batch(2)
response = self.client.get(
self.entitlements_list_url,
content_type='application/json',
)
assert response.status_code == 200
results = response.data.get('results', [])
assert results == CourseEntitlementSerializer(entitlements, many=True).data
def test_get_entitlement_by_uuid(self):
entitlement = CourseEntitlementFactory()
CourseEntitlementFactory.create_batch(2)
CourseEntitlementFactory()
url = reverse(self.ENTITLEMENTS_DETAILS_PATH, args=[str(entitlement.uuid)])
response = self.client.get(
url,
content_type='application/json',
)
assert response.status_code == 200
results = response.data
assert results == CourseEntitlementSerializer(entitlement).data
def test_delete_and_revoke_entitlement(self):
course_entitlement = CourseEntitlementFactory()
url = reverse(self.ENTITLEMENTS_DETAILS_PATH, args=[str(course_entitlement.uuid)])
response = self.client.delete(
url,
content_type='application/json',
)
assert response.status_code == 204
course_entitlement.refresh_from_db()
assert course_entitlement.expired_at is not None
def test_revoke_unenroll_entitlement(self):
course_entitlement = CourseEntitlementFactory()
url = reverse(self.ENTITLEMENTS_DETAILS_PATH, args=[str(course_entitlement.uuid)])
enrollment = CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
course_entitlement.refresh_from_db()
course_entitlement.enrollment_course_run = enrollment
course_entitlement.save()
assert course_entitlement.enrollment_course_run is not None
response = self.client.delete(
url,
content_type='application/json',
)
assert response.status_code == 204
course_entitlement.refresh_from_db()
assert course_entitlement.expired_at is not None
assert course_entitlement.enrollment_course_run is None
from django.conf.urls import url, include
from rest_framework.routers import DefaultRouter
from .views import EntitlementViewSet
router = DefaultRouter()
router.register(r'entitlements', EntitlementViewSet, base_name='entitlements')
urlpatterns = [
url(r'', include(router.urls)),
]
import logging
from django.utils import timezone
from django_filters.rest_framework import DjangoFilterBackend
from edx_rest_framework_extensions.authentication import JwtAuthentication
from rest_framework import permissions, viewsets
from rest_framework.authentication import SessionAuthentication
from entitlements.api.v1.filters import CourseEntitlementFilter
from entitlements.models import CourseEntitlement
from entitlements.api.v1.serializers import CourseEntitlementSerializer
from student.models import CourseEnrollment
log = logging.getLogger(__name__)
class EntitlementViewSet(viewsets.ModelViewSet):
authentication_classes = (JwtAuthentication, SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated, permissions.IsAdminUser,)
queryset = CourseEntitlement.objects.all().select_related('user')
lookup_value_regex = '[0-9a-f-]+'
lookup_field = 'uuid'
serializer_class = CourseEntitlementSerializer
filter_backends = (DjangoFilterBackend,)
filter_class = CourseEntitlementFilter
def perform_destroy(self, instance):
"""
This method is an override and is called by the DELETE method
"""
save_model = False
if instance.expired_at is None:
instance.expired_at = timezone.now()
log.info('Set expired_at to [%s] for course entitlement [%s]', instance.expired_at, instance.uuid)
save_model = True
if instance.enrollment_course_run is not None:
CourseEnrollment.unenroll(
user=instance.user,
course_id=instance.enrollment_course_run.course_id,
skip_refund=True
)
enrollment = instance.enrollment_course_run
instance.enrollment_course_run = None
save_model = True
log.info(
'Unenrolled user [%s] from course run [%s] as part of revocation of course entitlement [%s]',
instance.user.username,
enrollment.course_id,
instance.uuid
)
if save_model:
instance.save()
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
"""
Migration to remove default Mode and to move comments to Help Text
"""
dependencies = [
('entitlements', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='courseentitlement',
name='course_uuid',
field=models.UUIDField(help_text=b'UUID for the Course, not the Course Run'),
),
migrations.AlterField(
model_name='courseentitlement',
name='enrollment_course_run',
field=models.ForeignKey(to='student.CourseEnrollment', help_text=b'The current Course enrollment for this entitlement. If NULL the Learner has not enrolled.', null=True),
),
migrations.AlterField(
model_name='courseentitlement',
name='expired_at',
field=models.DateTimeField(help_text=b'The date that an entitlement expired, if NULL the entitlement has not expired.', null=True),
),
migrations.AlterField(
model_name='courseentitlement',
name='mode',
field=models.CharField(help_text=b'The mode of the Course that will be applied on enroll.', max_length=100),
),
]
import uuid as uuid_tools
from django.conf import settings
from django.db import models
from model_utils.models import TimeStampedModel
from django.contrib.auth.models import User
from course_modes.models import CourseMode
class CourseEntitlement(TimeStampedModel):
......@@ -11,87 +10,17 @@ class CourseEntitlement(TimeStampedModel):
Represents a Student's Entitlement to a Course Run for a given Course.
"""
user = models.ForeignKey(User)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
uuid = models.UUIDField(default=uuid_tools.uuid4, editable=False)
course_uuid = models.UUIDField()
# The date that an the entitlement expired
# if NULL the entitlement has not expired
expired_at = models.DateTimeField(null=True)
# The mode of the Course that will be applied
mode = models.CharField(default=CourseMode.DEFAULT_MODE_SLUG, max_length=100)
# The ID of the course enrollment for this Entitlement
# if NULL the entitlement is not in use
enrollment_course_run = models.ForeignKey('student.CourseEnrollment', null=True)
course_uuid = models.UUIDField(help_text='UUID for the Course, not the Course Run')
expired_at = models.DateTimeField(
null=True,
help_text='The date that an entitlement expired, if NULL the entitlement has not expired.'
)
mode = models.CharField(max_length=100, help_text='The mode of the Course that will be applied on enroll.')
enrollment_course_run = models.ForeignKey(
'student.CourseEnrollment',
null=True,
help_text='The current Course enrollment for this entitlement. If NULL the Learner has not enrolled.'
)
order_number = models.CharField(max_length=128, null=True)
@classmethod
def entitlements_for_user(cls, user):
"""
Retrieve all the Entitlements for a User
Arguments:
user: A Django User object identifying the current user
Returns:
All of the Entitlements for the User
"""
return cls.objects.filter(user=user)
@classmethod
def get_user_course_entitlement(cls, user, course_uuid):
"""
Retrieve The entitlement for the given parent course id if it exists for the User
Arguments:
user: A Django User object identifying the current user
course_uuid(string): The parent course uuid
Returns:
The single entitlement for the requested parent course id
"""
return cls.objects.filter(user=user, course_uuid=course_uuid).first()
@classmethod
def update_or_create_new_entitlement(cls, user, course_uuid, entitlement_data):
"""
Updates or creates a new Course Entitlement
Arguments:
user: A Django User object identifying the current user
course_uuid(string): The parent course uuid
entitlement_data(dict): The dictionary containing all the data for the entitlement
e.g. entitlement_data = {
'user': user,
'course_uuid': course_uuid
'enroll_end_date': '2017-09-14 11:47:58.000000',
'mode': 'verified',
}
Returns:
stored_entitlement: The new or updated CourseEntitlement object
is_created (bool): Boolean representing whether or not the Entitlement was created or updated
"""
stored_entitlement, is_created = cls.objects.update_or_create(
user=user,
course_uuid=course_uuid,
defaults=entitlement_data
)
return stored_entitlement, is_created
@classmethod
def update_entitlement_enrollment(cls, user, course_uuid, course_run_enrollment):
"""
Sets the enrollment course for a given entitlement
Arguments:
user: A Django User object identifying the current user
course_uuid(string): The parent course uuid
course_run_enrollment (CourseEnrollment): The CourseEnrollment object to store, None to clear the Enrollment
"""
return cls.objects.filter(
user=user,
course_uuid=course_uuid
).update(enrollment_course_run_id=course_run_enrollment)
import string
import uuid
import factory
from factory.fuzzy import FuzzyChoice, FuzzyText
from entitlements.models import CourseEntitlement
from student.tests.factories import UserFactory
class CourseEntitlementFactory(factory.django.DjangoModelFactory):
class Meta(object):
model = CourseEntitlement
course_uuid = uuid.uuid4()
mode = FuzzyChoice(['verified', 'profesional'])
user = factory.SubFactory(UserFactory)
order_number = FuzzyText(prefix='TEXTX', chars=string.digits)
"""
Test the Data Aggregation Layer for Course Entitlements.
"""
import unittest
import uuid
import ddt
from django.conf import settings
from entitlements.models import CourseEntitlement
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import CourseEnrollmentFactory
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EntitlementDataTest(ModuleStoreTestCase):
"""
Test course entitlement data aggregation.
"""
USERNAME = "Bob"
EMAIL = "bob@example.com"
PASSWORD = "edx"
def setUp(self):
"""Create a course and user, then log in. """
super(EntitlementDataTest, self).setUp()
self.course = CourseFactory.create()
self.course_uuid = uuid.uuid4()
self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
self.client.login(username=self.USERNAME, password=self.PASSWORD)
def _add_entitlement_for_user(self, course, user, parent_uuid):
entitlement_data = {
'user': user,
'course_uuid': parent_uuid,
'mode': 'verified',
}
stored_entitlement, is_created = CourseEntitlement.update_or_create_new_entitlement(
user,
parent_uuid,
entitlement_data
)
return stored_entitlement, is_created
def test_get_entitlement_info(self):
stored_entitlement, is_created = self._add_entitlement_for_user(self.course, self.user, self.course_uuid)
self.assertTrue(is_created)
# Get the Entitlement and verify the data
entitlement = CourseEntitlement.get_user_course_entitlement(self.user, self.course_uuid)
self.assertEqual(entitlement.course_uuid, self.course_uuid)
self.assertEqual(entitlement.mode, 'verified')
self.assertIsNone(entitlement.enrollment_course_run)
def test_get_course_entitlements(self):
course2 = CourseFactory.create()
stored_entitlement, is_created = self._add_entitlement_for_user(self.course, self.user, self.course_uuid)
self.assertTrue(is_created)
course2_uuid = uuid.uuid4()
stored_entitlement2, is_created2 = self._add_entitlement_for_user(course2, self.user, course2_uuid)
self.assertTrue(is_created2)
# Get the Entitlement and verify the data
entitlement_list = CourseEntitlement.entitlements_for_user(self.user)
self.assertEqual(2, len(entitlement_list))
self.assertEqual(self.course_uuid, entitlement_list[0].course_uuid)
self.assertEqual(course2_uuid, entitlement_list[1].course_uuid)
def test_set_enrollment(self):
stored_entitlement, is_created = self._add_entitlement_for_user(self.course, self.user, self.course_uuid)
self.assertTrue(is_created)
# Entitlement set not enroll the user in the Course run
enrollment = CourseEnrollmentFactory(
user=self.user,
course_id=self.course.id,
mode="verified",
)
CourseEntitlement.update_entitlement_enrollment(self.user, self.course_uuid, enrollment)
entitlement = CourseEntitlement.get_user_course_entitlement(self.user, self.course_uuid)
self.assertIsNotNone(entitlement.enrollment_course_run)
def test_remove_enrollment(self):
stored_entitlement, is_created = self._add_entitlement_for_user(self.course, self.user, self.course_uuid)
self.assertTrue(is_created)
# Entitlement set not enroll the user in the Course run
enrollment = CourseEnrollmentFactory(
user=self.user,
course_id=self.course.id,
mode="verified",
)
CourseEntitlement.update_entitlement_enrollment(self.user, self.course_uuid, enrollment)
entitlement = CourseEntitlement.get_user_course_entitlement(self.user, self.course_uuid)
self.assertIsNotNone(entitlement.enrollment_course_run)
CourseEntitlement.update_entitlement_enrollment(self.user, self.course_uuid, None)
entitlement = CourseEntitlement.get_user_course_entitlement(self.user, self.course_uuid)
self.assertIsNone(entitlement.enrollment_course_run)
......@@ -64,6 +64,9 @@ urlpatterns = (
# Enrollment API RESTful endpoints
url(r'^api/enrollment/v1/', include('enrollment.urls')),
# Entitlement API RESTful endpoints
url(r'^api/entitlements/', include('entitlements.api.urls', namespace='entitlements_api')),
# Courseware search endpoints
url(r'^search/', include('search.urls')),
......
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