diff --git a/common/djangoapps/enrollment/__init__.py b/common/djangoapps/enrollment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/enrollment/api.py b/common/djangoapps/enrollment/api.py new file mode 100644 index 0000000000..0c65f4de66 --- /dev/null +++ b/common/djangoapps/enrollment/api.py @@ -0,0 +1,223 @@ +""" +Enrollment API for creating, updating, and deleting enrollments. Also provides access to enrollment information at a +course level, such as available course modes. + +""" +from enrollment import data + + +class CourseEnrollmentError(Exception): + """ Generic Course Enrollment Error. + + Describes any error that may occur when reading or updating enrollment information for a student or a course. + + """ + pass + + +def get_enrollments(student_id): + """ Retrieves all the courses a student is enrolled in. + + Takes a student and retrieves all relative enrollments. Includes information regarding how the student is enrolled + in the the course. + + Args: + student_id (str): The ID of the student we want to retrieve course enrollment information for. + + Returns: + A list of enrollment information for the given student. + + Examples: + >>> get_enrollments("Bob") + [ + { + course_id: "edX/DemoX/2014T2", + is_active: True, + mode: "honor", + student: "Bob", + course_modes: [ + "audit", + "honor" + ], + enrollment_start: 2014-04-07, + enrollment_end: 2014-06-07, + invite_only: False + }, + { + course_id: "edX/edX-Insider/2014T2", + is_active: True, + mode: "honor", + student: "Bob", + course_modes: [ + "audit", + "honor", + "verified" + ], + enrollment_start: 2014-05-01, + enrollment_end: 2014-06-01, + invite_only: True + }, + ] + + """ + return data.get_course_enrollments(student_id) + + +def get_enrollment(student_id, course_id): + """ Retrieves all enrollment information for the student in respect to a specific course. + + Gets all the course enrollment information specific to a student in a course. + + Args: + student_id (str): The student to get course enrollment information for. + course_id (str): The course to get enrollment information for. + + Returns: + A serializable dictionary of the course enrollment. + + Example: + >>> add_enrollment("Bob", "edX/DemoX/2014T2") + { + course_id: "edX/DemoX/2014T2", + is_active: True, + mode: "honor", + student: "Bob", + course_modes: [ + "audit", + "honor" + ], + enrollment_start: 2014-04-07, + enrollment_end: 2014-06-07, + invite_only: False + } + + """ + return data.get_course_enrollment(student_id, course_id) + + +def add_enrollment(student_id, course_id, mode='honor', is_active=True): + """ Enrolls a student in a course. + + Enrolls a student in a course. If the mode is not specified, this will default to 'honor'. + + Args: + student_id (str): The student to enroll. + course_id (str): The course to enroll the student in. + mode (str): Optional argument for the type of enrollment to create. Ex. 'audit', 'honor', 'verified', + 'professional'. If not specified, this defaults to 'honor'. + is_active (boolean): Optional argument for making the new enrollment inactive. If not specified, is_active + defaults to True. + + Returns: + A serializable dictionary of the new course enrollment. + + Example: + >>> add_enrollment("Bob", "edX/DemoX/2014T2", mode="audit") + { + course_id: "edX/DemoX/2014T2", + is_active: True, + mode: "audit", + student: "Bob", + course_modes: [ + "audit", + "honor" + ], + enrollment_start: 2014-04-07, + enrollment_end: 2014-06-07, + invite_only: False + } + """ + return data.update_course_enrollment(student_id, course_id, mode=mode, is_active=is_active) + + +def deactivate_enrollment(student_id, course_id): + """ Un-enrolls a student in a course + + Deactivate the enrollment of a student in a course. We will not remove the enrollment data, but simply flag it + as inactive. + + Args: + student_id (str): The student associated with the deactivated enrollment. + course_id (str): The course associated with the deactivated enrollment. + + Returns: + A serializable dictionary representing the deactivated course enrollment for the student. + + Example: + >>> deactivate_enrollment("Bob", "edX/DemoX/2014T2") + { + course_id: "edX/DemoX/2014T2", + mode: "honor", + is_active: False, + student: "Bob", + course_modes: [ + "audit", + "honor" + ], + enrollment_start: 2014-04-07, + enrollment_end: 2014-06-07, + invite_only: False + } + """ + return data.update_course_enrollment(student_id, course_id, is_active=False) + + +def update_enrollment(student_id, course_id, mode): + """ Updates the course mode for the enrolled user. + + Update a course enrollment for the given student and course. + + Args: + student_id (str): The student associated with the updated enrollment. + course_id (str): The course associated with the updated enrollment. + mode (str): The new course mode for this enrollment. + + Returns: + A serializable dictionary representing the updated enrollment. + + Example: + >>> update_enrollment("Bob", "edX/DemoX/2014T2", "honor") + { + course_id: "edX/DemoX/2014T2", + mode: "honor", + is_active: True, + student: "Bob", + course_modes: [ + "audit", + "honor" + ], + enrollment_start: 2014-04-07, + enrollment_end: 2014-06-07, + invite_only: False + } + + """ + return data.update_course_enrollment(student_id, course_id, mode) + + +def get_course_enrollment_details(course_id): + """ Get the course modes for course. Also get enrollment start and end date, invite only, etc. + + Given a course_id, return a serializable dictionary of properties describing course enrollment information. + + Args: + course_id (str): The Course to get enrollment information for. + + Returns: + A serializable dictionary of course enrollment information. + + Example: + >>> get_course_enrollment_details("edX/DemoX/2014T2") + { + course_id: "edX/DemoX/2014T2", + course_modes: [ + "audit", + "honor" + ], + enrollment_start: 2014-04-01, + enrollment_end: 2014-06-01, + invite_only: False + } + + """ + pass diff --git a/common/djangoapps/enrollment/data.py b/common/djangoapps/enrollment/data.py new file mode 100644 index 0000000000..b6686f6731 --- /dev/null +++ b/common/djangoapps/enrollment/data.py @@ -0,0 +1,48 @@ +""" +Data Aggregation Layer of the Enrollment API. Collects all enrollment specific data into a single +source to be used throughout the API. + +""" +from django.contrib.auth.models import User +from opaque_keys.edx.keys import CourseKey +from enrollment.serializers import CourseEnrollmentSerializer +from student.models import CourseEnrollment + + +def get_course_enrollments(student_id): + qset = CourseEnrollment.objects.filter( + user__username=student_id, is_active=True + ).order_by('created') + return CourseEnrollmentSerializer(qset).data + + +def get_course_enrollment(student_id, course_id): + course_key = CourseKey.from_string(course_id) + try: + enrollment = CourseEnrollment.objects.get( + user__username=student_id, course_id=course_key + ) + return CourseEnrollmentSerializer(enrollment).data + except CourseEnrollment.DoesNotExist: + return None + + +def update_course_enrollment(student_id, course_id, mode=None, is_active=None): + course_key = CourseKey.from_string(course_id) + student = User.objects.get(username=student_id) + if not CourseEnrollment.is_enrolled(student, course_key): + enrollment = CourseEnrollment.enroll(student, course_key) + else: + enrollment = CourseEnrollment.objects.get(user=student, course_id=course_key) + + enrollment.update_enrollment(is_active=is_active, mode=mode) + enrollment.save() + return CourseEnrollmentSerializer(enrollment).data + + +def get_course_enrollment_info(course_id): + pass + + +def get_course_enrollments_info(student_id): + pass diff --git a/common/djangoapps/enrollment/models.py b/common/djangoapps/enrollment/models.py new file mode 100644 index 0000000000..3f0363134b --- /dev/null +++ b/common/djangoapps/enrollment/models.py @@ -0,0 +1,4 @@ +""" +A models.py is required to make this an app (until we move to Django 1.7) + +""" diff --git a/common/djangoapps/enrollment/serializers.py b/common/djangoapps/enrollment/serializers.py new file mode 100644 index 0000000000..324c03b058 --- /dev/null +++ b/common/djangoapps/enrollment/serializers.py @@ -0,0 +1,47 @@ +""" +Serializers for all Course Enrollment related return objects. + +""" +from rest_framework import serializers +from student.models import CourseEnrollment +from course_modes.models import CourseMode + + +class CourseField(serializers.RelatedField): + """Custom field to wrap a CourseDescriptor object. Read-only.""" + + def to_native(self, course): + course_id = unicode(course.id) + course_modes = ModeSerializer(CourseMode.modes_for_course(course.id)).data + + return { + "course_id": course_id, + "enrollment_start": course.enrollment_start, + "enrollment_end": course.enrollment_end, + "invite_only": course.invitation_only, + "course_modes": course_modes, + } + + +class CourseEnrollmentSerializer(serializers.ModelSerializer): + """ + Serializes CourseEnrollment models + + """ + course = CourseField() + + class Meta: # pylint: disable=C0111 + model = CourseEnrollment + fields = ('created', 'mode', 'is_active', 'course') + lookup_field = 'username' + + +class ModeSerializer(serializers.Serializer): + """Serializes a course's 'Mode' tuples""" + slug = serializers.CharField(max_length=100) + name = serializers.CharField(max_length=255) + min_price = serializers.IntegerField() + suggested_prices = serializers.CharField(max_length=255) + currency = serializers.CharField(max_length=8) + expiration_datetime = serializers.DateTimeField() + description = serializers.CharField() diff --git a/common/djangoapps/enrollment/tests/__init__.py b/common/djangoapps/enrollment/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/enrollment/urls.py b/common/djangoapps/enrollment/urls.py new file mode 100644 index 0000000000..9b8dd077aa --- /dev/null +++ b/common/djangoapps/enrollment/urls.py @@ -0,0 +1,18 @@ +""" +URLs for the Enrollment API + +""" +from django.conf import settings +from django.conf.urls import patterns, url + +from .views import get_course_enrollment, list_student_enrollments + +urlpatterns = patterns( + 'enrollment.views', + url(r'^student$', list_student_enrollments, name='courseenrollments'), + url( + r'^course/{course_key}$'.format(course_key=settings.COURSE_ID_PATTERN), + get_course_enrollment, + name='courseenrollment' + ), +) diff --git a/common/djangoapps/enrollment/views.py b/common/djangoapps/enrollment/views.py new file mode 100644 index 0000000000..76a2b34e90 --- /dev/null +++ b/common/djangoapps/enrollment/views.py @@ -0,0 +1,38 @@ +""" +The Enrollment API Views should be simple, lean HTTP endpoints for API access. This should +consist primarily of authentication, request validation, and serialization. + +""" +from rest_framework.authentication import OAuth2Authentication, SessionAuthentication +from rest_framework.decorators import api_view, authentication_classes, permission_classes, throttle_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.throttling import UserRateThrottle +from enrollment import api + + +class EnrollmentUserThrottle(UserRateThrottle): + rate = '50/second' # TODO Limit significantly after performance testing. + + +@api_view(['GET']) +@authentication_classes((OAuth2Authentication, SessionAuthentication)) +@permission_classes((IsAuthenticated,)) +@throttle_classes([EnrollmentUserThrottle]) +def list_student_enrollments(request): + return Response(api.get_enrollments(request.user.username)) + + +@api_view(['GET', 'POST']) +@authentication_classes((OAuth2Authentication, SessionAuthentication)) +@permission_classes((IsAuthenticated,)) +@throttle_classes([EnrollmentUserThrottle]) +def get_course_enrollment(request, course_id=None): + if 'mode' in request.DATA: + return Response(api.update_enrollment(request.user.username, course_id, request.DATA['mode'])) + elif 'deactivate' in request.DATA: + return Response(api.deactivate_enrollment(request.user.username, course_id)) + elif course_id and request.method == 'POST': + return Response(api.add_enrollment(request.user.username, course_id)) + else: + return Response(api.get_enrollment(request.user.username, course_id)) diff --git a/lms/urls.py b/lms/urls.py index c2986732b7..caafbb9d0d 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -73,6 +73,9 @@ urlpatterns = ('', # nopep8 # Feedback Form endpoint url(r'^submit_feedback$', 'util.views.submit_feedback'), + # Enrollment API RESTful endpoints + url(r'^enrollment/v0/', include('enrollment.urls')), + ) if settings.FEATURES["ENABLE_MOBILE_REST_API"]: