diff --git a/common/djangoapps/entitlements/__init__.py b/common/djangoapps/entitlements/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/entitlements/admin.py b/common/djangoapps/entitlements/admin.py new file mode 100644 index 0000000000..16c9000b2f --- /dev/null +++ b/common/djangoapps/entitlements/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin +from .models import CourseEntitlement + + +@admin.register(CourseEntitlement) +class EntitlementAdmin(admin.ModelAdmin): + list_display = ('user', + 'uuid', + 'course_uuid', + 'created', + 'modified', + 'expired_at', + 'mode', + 'enrollment_course_run', + 'order_number') diff --git a/common/djangoapps/entitlements/migrations/0001_initial.py b/common/djangoapps/entitlements/migrations/0001_initial.py new file mode 100644 index 0000000000..af7d5b9407 --- /dev/null +++ b/common/djangoapps/entitlements/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone +from django.conf import settings +import model_utils.fields +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('student', '0013_delete_historical_enrollment_records'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='CourseEntitlement', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)), + ('course_uuid', models.UUIDField()), + ('expired_at', models.DateTimeField(null=True)), + ('mode', models.CharField(default=b'audit', max_length=100)), + ('order_number', models.CharField(max_length=128, null=True)), + ('enrollment_course_run', models.ForeignKey(to='student.CourseEnrollment', null=True)), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/common/djangoapps/entitlements/migrations/__init__.py b/common/djangoapps/entitlements/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/entitlements/models.py b/common/djangoapps/entitlements/models.py new file mode 100644 index 0000000000..d38d15fd80 --- /dev/null +++ b/common/djangoapps/entitlements/models.py @@ -0,0 +1,97 @@ +import uuid as uuid_tools + +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): + """ + Represents a Student's Entitlement to a Course Run for a given Course. + """ + + user = models.ForeignKey(User) + 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) + 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) diff --git a/common/djangoapps/entitlements/tests/test_data.py b/common/djangoapps/entitlements/tests/test_data.py new file mode 100644 index 0000000000..3f1c1fde20 --- /dev/null +++ b/common/djangoapps/entitlements/tests/test_data.py @@ -0,0 +1,107 @@ +""" +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) diff --git a/lms/envs/common.py b/lms/envs/common.py index 5cc5766c0f..42d78afc1b 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2128,6 +2128,9 @@ INSTALLED_APPS = [ # Enrollment API 'enrollment', + # Entitlement API + 'entitlements', + # Bulk Enrollment API 'bulk_enroll',