Unverified Commit 43d6cc20 by Albert (AJ) St. Aubin Committed by GitHub

Merge pull request #16359 from edx/aj/LEARNER-2661

Added Entitlement API
parents f835fc8c 113e0af1
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)
......@@ -88,6 +88,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