Merge pull request #20702 from edx/bom/enrollment-readme
Enrollments README and refactor
This commit is contained in:
@@ -54,16 +54,16 @@ from six.moves.urllib.parse import urlencode
|
||||
from slumber.exceptions import HttpClientError, HttpServerError
|
||||
from user_util import user_util
|
||||
|
||||
import openedx.core.djangoapps.django_comment_common.comment_client as cc
|
||||
from course_modes.models import CourseMode, get_cosmetic_verified_display_price
|
||||
from courseware.models import (
|
||||
CourseDynamicUpgradeDeadlineConfiguration,
|
||||
DynamicUpgradeDeadlineConfiguration,
|
||||
OrgDynamicUpgradeDeadlineConfiguration
|
||||
)
|
||||
from enrollment.api import _default_course_mode
|
||||
from lms.djangoapps.certificates.models import GeneratedCertificate
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
import openedx.core.djangoapps.django_comment_common.comment_client as cc
|
||||
from openedx.core.djangoapps.enrollments.api import _default_course_mode
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.xmodule_django.models import NoneToEmptyManager
|
||||
from openedx.core.djangolib.model_mixins import DeletableByUserValue
|
||||
|
||||
@@ -11,10 +11,10 @@ from opaque_keys.edx.django.models import CourseKeyField
|
||||
from six import text_type
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from enrollment.api import validate_course_mode
|
||||
from enrollment.errors import CourseModeNotFoundError
|
||||
from openedx.core.djangoapps.course_groups.cohorts import get_cohort_by_name
|
||||
from openedx.core.djangoapps.course_groups.models import CourseUserGroup
|
||||
from openedx.core.djangoapps.enrollments.api import validate_course_mode
|
||||
from openedx.core.djangoapps.enrollments.errors import CourseModeNotFoundError
|
||||
from openedx.core.lib.html_to_text import html_to_text
|
||||
from openedx.core.lib.mail_utils import wrap_message
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole
|
||||
|
||||
@@ -25,10 +25,10 @@ from bulk_email.models import BulkEmailFlag, Optout
|
||||
from bulk_email.tasks import _get_course_email_context, _get_source_address
|
||||
from course_modes.models import CourseMode
|
||||
from courseware.tests.factories import InstructorFactory, StaffFactory
|
||||
from enrollment.api import update_enrollment
|
||||
from lms.djangoapps.instructor_task.subtasks import update_subtask_status
|
||||
from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort
|
||||
from openedx.core.djangoapps.course_groups.models import CourseCohort
|
||||
from openedx.core.djangoapps.enrollments.api import update_enrollment
|
||||
from student.models import CourseEnrollment
|
||||
from student.roles import CourseStaffRole
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
|
||||
@@ -14,10 +14,10 @@ from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from bulk_enroll.serializers import BulkEnrollmentSerializer
|
||||
from enrollment.views import EnrollmentUserThrottle
|
||||
from instructor.views.api import students_update_enrollment
|
||||
from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, get_cohort_by_name
|
||||
from openedx.core.djangoapps.course_groups.models import CourseUserGroup
|
||||
from openedx.core.djangoapps.enrollments.views import EnrollmentUserThrottle
|
||||
from openedx.core.lib.api.authentication import OAuth2Authentication
|
||||
from openedx.core.lib.api.permissions import IsStaff
|
||||
from util.disable_rate_limit import can_disable_rate_limit
|
||||
|
||||
@@ -14,8 +14,8 @@ from django.test.utils import override_settings
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from enrollment.api import get_enrollment
|
||||
from openedx.core.djangoapps.embargo.test_utils import restrict_course
|
||||
from openedx.core.djangoapps.enrollments.api import get_enrollment
|
||||
from openedx.core.lib.django_test_client_utils import get_absolute_url
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.tests import EnrollmentEventTestMixin
|
||||
|
||||
@@ -14,11 +14,11 @@ from six import text_type
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from courseware import courses
|
||||
from enrollment.api import add_enrollment
|
||||
from enrollment.views import EnrollmentCrossDomainSessionAuth
|
||||
from entitlements.models import CourseEntitlement
|
||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
|
||||
from openedx.core.djangoapps.embargo import api as embargo_api
|
||||
from openedx.core.djangoapps.enrollments.api import add_enrollment
|
||||
from openedx.core.djangoapps.enrollments.views import EnrollmentCrossDomainSessionAuth
|
||||
from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in
|
||||
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
@@ -28,7 +28,6 @@ from django.conf import settings
|
||||
from django.db.models import Prefetch
|
||||
from django.urls import reverse
|
||||
from django.http import Http404, QueryDict
|
||||
from enrollment.api import get_course_enrollment_details
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from fs.errors import ResourceNotFound
|
||||
from lms.djangoapps.certificates import api as certs_api
|
||||
@@ -37,6 +36,7 @@ from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.enrollments.api import get_course_enrollment_details
|
||||
from openedx.core.lib.api.view_utils import LazySequence
|
||||
from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG
|
||||
from path import Path as path
|
||||
|
||||
@@ -59,7 +59,6 @@ from courseware.models import BaseStudentModuleHistory, StudentModule
|
||||
from courseware.url_helpers import get_redirect_url
|
||||
from courseware.user_state_client import DjangoXBlockUserStateClient
|
||||
from edxmako.shortcuts import marketing_link, render_to_response, render_to_string
|
||||
from enrollment.api import add_enrollment
|
||||
from ipware.ip import get_ip
|
||||
from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException
|
||||
from lms.djangoapps.certificates import api as certs_api
|
||||
@@ -80,6 +79,7 @@ from openedx.core.djangoapps.credit.api import (
|
||||
is_credit_course,
|
||||
is_user_eligible_for_credit
|
||||
)
|
||||
from openedx.core.djangoapps.enrollments.api import add_enrollment
|
||||
from openedx.core.djangoapps.models.course_details import CourseDetails
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from openedx.core.djangoapps.programs.utils import ProgramMarketingDataExtender
|
||||
|
||||
@@ -18,12 +18,12 @@ from six import text_type
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from enrollment.api import get_enrollments, update_enrollment
|
||||
from enrollment.errors import CourseModeNotFoundError
|
||||
from enrollment.serializers import ModeSerializer
|
||||
from lms.djangoapps.support.decorators import require_support_permission
|
||||
from lms.djangoapps.support.serializers import ManualEnrollmentSerializer
|
||||
from lms.djangoapps.verify_student.models import VerificationDeadline
|
||||
from openedx.core.djangoapps.enrollments.api import get_enrollments, update_enrollment
|
||||
from openedx.core.djangoapps.enrollments.errors import CourseModeNotFoundError
|
||||
from openedx.core.djangoapps.enrollments.serializers import ModeSerializer
|
||||
from student.models import ENROLLED_TO_ENROLLED, CourseEnrollment, ManualEnrollmentAudit
|
||||
from util.json_request import JsonResponse
|
||||
|
||||
|
||||
@@ -2156,7 +2156,7 @@ INSTALLED_APPS = [
|
||||
'course_modes.apps.CourseModesConfig',
|
||||
|
||||
# Enrollment API
|
||||
'enrollment',
|
||||
'openedx.core.djangoapps.enrollments',
|
||||
|
||||
# Entitlement API
|
||||
'entitlements.apps.EntitlementsConfig',
|
||||
|
||||
@@ -92,7 +92,7 @@ urlpatterns = [
|
||||
url(r'^submit_feedback$', util_views.submit_feedback),
|
||||
|
||||
# Enrollment API RESTful endpoints
|
||||
url(r'^api/enrollment/v1/', include('enrollment.urls')),
|
||||
url(r'^api/enrollment/v1/', include('openedx.core.djangoapps.enrollments.urls')),
|
||||
|
||||
# Entitlement API RESTful endpoints
|
||||
url(r'^api/entitlements/', include('entitlements.api.urls', namespace='entitlements_api')),
|
||||
|
||||
16
openedx/core/djangoapps/enrollments/README.rst
Normal file
16
openedx/core/djangoapps/enrollments/README.rst
Normal file
@@ -0,0 +1,16 @@
|
||||
Status: Maintenance
|
||||
|
||||
Responsibilities
|
||||
================
|
||||
The enrollments app provides basic CRUD functionality and APIs for managing Course-Run enrollments.
|
||||
Enrollments in Programs is managed by the ``lms/djangoapps/program_enrollments\`` app.
|
||||
|
||||
Direction: Keep
|
||||
===============
|
||||
|
||||
|
||||
Glossary
|
||||
========
|
||||
|
||||
More Documentation
|
||||
==================
|
||||
@@ -13,11 +13,11 @@ from django.core.cache import cache
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from enrollment import errors
|
||||
from openedx.core.djangoapps.enrollments import errors
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_DATA_API = 'enrollment.data'
|
||||
DEFAULT_DATA_API = 'openedx.core.djangoapps.enrollments.data'
|
||||
|
||||
|
||||
def get_enrollments(user_id, include_inactive=False):
|
||||
@@ -324,7 +324,7 @@ def get_course_enrollment_details(course_id, include_expired=False):
|
||||
cached_enrollment_data = None
|
||||
try:
|
||||
cached_enrollment_data = cache.get(cache_key)
|
||||
except Exception:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# The cache backend could raise an exception (for example, memcache keys that contain spaces)
|
||||
log.exception(u"Error occurred while retrieving course enrollment details from the cache")
|
||||
|
||||
@@ -11,15 +11,15 @@ from django.db import transaction
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from six import text_type
|
||||
|
||||
from enrollment.errors import (
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.enrollments.errors import (
|
||||
CourseEnrollmentClosedError,
|
||||
CourseEnrollmentExistsError,
|
||||
CourseEnrollmentFullError,
|
||||
InvalidEnrollmentAttribute,
|
||||
UserNotFoundError
|
||||
)
|
||||
from enrollment.serializers import CourseEnrollmentSerializer, CourseSerializer
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.enrollments.serializers import CourseEnrollmentSerializer, CourseSerializer
|
||||
from openedx.core.lib.exceptions import CourseNotFoundError
|
||||
from student.models import (
|
||||
AlreadyEnrolledError,
|
||||
@@ -348,6 +348,7 @@ def get_user_roles(user_id):
|
||||
:param user_id: The id of the selected user.
|
||||
:return: All roles for all courses that this user has.
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
user = _get_user(user_id)
|
||||
if not hasattr(user, '_roles'):
|
||||
user._roles = RoleCache(user)
|
||||
@@ -28,7 +28,7 @@ class CourseEnrollmentsApiListForm(Form):
|
||||
try:
|
||||
return CourseKey.from_string(course_id)
|
||||
except InvalidKeyError:
|
||||
raise ValidationError("'{}' is not a valid course id.".format(course_id))
|
||||
raise ValidationError(u"'{}' is not a valid course id.".format(course_id))
|
||||
return course_id
|
||||
|
||||
def clean_username(self):
|
||||
@@ -40,7 +40,7 @@ class CourseEnrollmentsApiListForm(Form):
|
||||
usernames = usernames_csv_string.split(',')
|
||||
if len(usernames) > self.MAX_USERNAME_COUNT:
|
||||
raise ValidationError(
|
||||
"Too many usernames in a single request - {}. A maximum of {} is allowed".format(
|
||||
u"Too many usernames in a single request - {}. A maximum of {} is allowed".format(
|
||||
len(usernames),
|
||||
self.MAX_USERNAME_COUNT,
|
||||
)
|
||||
@@ -4,8 +4,8 @@ Management command for enrolling a user into a course via the enrollment api
|
||||
from __future__ import absolute_import
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management.base import BaseCommand
|
||||
from enrollment.data import CourseEnrollmentExistsError
|
||||
from enrollment.api import add_enrollment
|
||||
from openedx.core.djangoapps.enrollments.data import CourseEnrollmentExistsError
|
||||
from openedx.core.djangoapps.enrollments.api import add_enrollment
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -1,15 +1,15 @@
|
||||
""" Test the change_enrollment command line script."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
import ddt
|
||||
import unittest
|
||||
from uuid import uuid4
|
||||
import ddt
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
|
||||
from enrollment.api import get_enrollment
|
||||
from openedx.core.djangoapps.enrollments.api import get_enrollment
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
@@ -20,7 +20,7 @@ class StringListField(serializers.CharField):
|
||||
[1,2,3]
|
||||
|
||||
"""
|
||||
def field_to_native(self, obj, field_name):
|
||||
def field_to_native(self, obj, field_name): # pylint: disable=unused-argument
|
||||
"""
|
||||
Serialize the object's class name.
|
||||
"""
|
||||
@@ -99,7 +99,7 @@ class CourseEnrollmentsApiListSerializer(CourseEnrollmentSerializer):
|
||||
fields = CourseEnrollmentSerializer.Meta.fields + ('course_id', )
|
||||
|
||||
|
||||
class ModeSerializer(serializers.Serializer):
|
||||
class ModeSerializer(serializers.Serializer): # pylint: disable=abstract-method
|
||||
"""Serializes a course's 'Mode' tuples
|
||||
|
||||
Returns a serialized representation of the modes available for course enrollment. The course
|
||||
@@ -12,14 +12,16 @@ from django.test.utils import override_settings
|
||||
from mock import Mock, patch
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from enrollment import api
|
||||
from enrollment.errors import CourseModeNotFoundError, EnrollmentApiLoadError, EnrollmentNotFoundError
|
||||
from enrollment.tests import fake_data_api
|
||||
from openedx.core.djangoapps.enrollments import api
|
||||
from openedx.core.djangoapps.enrollments.errors import (
|
||||
CourseModeNotFoundError, EnrollmentApiLoadError, EnrollmentNotFoundError,
|
||||
)
|
||||
from openedx.core.djangoapps.enrollments.tests import fake_data_api
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@override_settings(ENROLLMENT_DATA_API="enrollment.tests.fake_data_api")
|
||||
@override_settings(ENROLLMENT_DATA_API="openedx.core.djangoapps.enrollments.tests.fake_data_api")
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class EnrollmentTest(CacheIsolationTestCase):
|
||||
"""
|
||||
@@ -71,7 +73,7 @@ class EnrollmentTest(CacheIsolationTestCase):
|
||||
def test_enroll_no_mode_success(self, course_modes, expected_mode):
|
||||
# Add a fake course enrollment information to the fake data API
|
||||
fake_data_api.add_course(self.COURSE_ID, course_modes=course_modes)
|
||||
with patch('enrollment.api.CourseMode.modes_for_course') as mock_modes_for_course:
|
||||
with patch('openedx.core.djangoapps.enrollments.api.CourseMode.modes_for_course') as mock_modes_for_course:
|
||||
mock_course_modes = [Mock(slug=mode) for mode in course_modes]
|
||||
mock_modes_for_course.return_value = mock_course_modes
|
||||
# Enroll in the course and verify the URL we get sent to
|
||||
@@ -17,14 +17,14 @@ from six.moves import range
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from enrollment import data
|
||||
from enrollment.errors import (
|
||||
from openedx.core.djangoapps.enrollments import data
|
||||
from openedx.core.djangoapps.enrollments.errors import (
|
||||
CourseEnrollmentClosedError,
|
||||
CourseEnrollmentExistsError,
|
||||
CourseEnrollmentFullError,
|
||||
UserNotFoundError
|
||||
)
|
||||
from enrollment.serializers import CourseEnrollmentSerializer
|
||||
from openedx.core.djangoapps.enrollments.serializers import CourseEnrollmentSerializer
|
||||
from openedx.core.lib.exceptions import CourseNotFoundError
|
||||
from student.models import AlreadyEnrolledError, CourseEnrollment, CourseFullError, EnrollmentClosedError
|
||||
from student.tests.factories import CourseAccessRoleFactory, UserFactory
|
||||
@@ -385,7 +385,9 @@ class EnrollmentDataTest(ModuleStoreTestCase):
|
||||
|
||||
def test_get_roles(self):
|
||||
"""Create a role for a user, then get it"""
|
||||
expected_role = CourseAccessRoleFactory.create(course_id=self.course.id, user=self.user, role="SuperCoolTestRole")
|
||||
expected_role = CourseAccessRoleFactory.create(
|
||||
course_id=self.course.id, user=self.user, role="SuperCoolTestRole",
|
||||
)
|
||||
roles = data.get_user_roles(self.user.username)
|
||||
self.assertEqual(roles, {expected_role})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable=missing-docstring,redefined-outer-name
|
||||
"""
|
||||
Tests for user enrollment.
|
||||
"""
|
||||
@@ -28,13 +29,13 @@ from six.moves import range
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from enrollment import api, data
|
||||
from enrollment.errors import CourseEnrollmentError
|
||||
from enrollment.views import EnrollmentUserThrottle
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.course_groups import cohorts
|
||||
from openedx.core.djangoapps.embargo.models import Country, CountryAccessRule, RestrictedCourse
|
||||
from openedx.core.djangoapps.embargo.test_utils import restrict_course
|
||||
from openedx.core.djangoapps.enrollments import api, data
|
||||
from openedx.core.djangoapps.enrollments.errors import CourseEnrollmentError
|
||||
from openedx.core.djangoapps.enrollments.views import EnrollmentUserThrottle
|
||||
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
|
||||
from openedx.core.djangoapps.user_api.models import RetirementState, UserOrgTag, UserRetirementStatus
|
||||
from openedx.core.lib.django_test_client_utils import get_absolute_url
|
||||
@@ -105,7 +106,7 @@ class EnrollmentTestMixin(object):
|
||||
|
||||
# Verify that the modulestore is queried as expected.
|
||||
with check_mongo_calls_range(min_finds=min_mongo_calls, max_finds=max_mongo_calls):
|
||||
with patch('enrollment.views.audit_log') as mock_audit_log:
|
||||
with patch('openedx.core.djangoapps.enrollments.views.audit_log') as mock_audit_log:
|
||||
url = reverse('courseenrollments')
|
||||
response = self.client.post(url, json.dumps(data), content_type='application/json', **extra)
|
||||
self.assertEqual(response.status_code, expected_status)
|
||||
@@ -237,7 +238,10 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente
|
||||
# Create an enrollment
|
||||
self.assert_enrollment_status()
|
||||
resp = self.client.get(
|
||||
reverse('courseenrollment', kwargs={'username': self.user.username, "course_id": six.text_type(self.course.id)})
|
||||
reverse(
|
||||
'courseenrollment',
|
||||
kwargs={'username': self.user.username, "course_id": six.text_type(self.course.id)},
|
||||
)
|
||||
)
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
data = json.loads(resp.content)
|
||||
@@ -473,7 +477,12 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente
|
||||
(None, None, None, None),
|
||||
(datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=pytz.UTC), None, "2015-01-02T03:04:05Z", None),
|
||||
(None, datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=pytz.UTC), None, "2015-01-02T03:04:05Z"),
|
||||
(datetime.datetime(2014, 6, 7, 8, 9, 10, tzinfo=pytz.UTC), datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=pytz.UTC), "2014-06-07T08:09:10Z", "2015-01-02T03:04:05Z"),
|
||||
(
|
||||
datetime.datetime(2014, 6, 7, 8, 9, 10, tzinfo=pytz.UTC),
|
||||
datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=pytz.UTC),
|
||||
"2014-06-07T08:09:10Z",
|
||||
"2015-01-02T03:04:05Z",
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_get_course_details_course_dates(self, start_datetime, end_datetime, expected_start, expected_end):
|
||||
@@ -528,7 +537,10 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente
|
||||
def test_get_enrollment_internal_error(self, mock_get_enrollment):
|
||||
mock_get_enrollment.side_effect = CourseEnrollmentError("Something bad happened.")
|
||||
resp = self.client.get(
|
||||
reverse('courseenrollment', kwargs={'username': self.user.username, "course_id": six.text_type(self.course.id)})
|
||||
reverse(
|
||||
'courseenrollment',
|
||||
kwargs={'username': self.user.username, "course_id": six.text_type(self.course.id)},
|
||||
)
|
||||
)
|
||||
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -576,7 +588,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente
|
||||
try:
|
||||
throttle.parse_rate(throttle.get_rate())
|
||||
except ImproperlyConfigured:
|
||||
self.fail("No throttle rate set for {}".format(user_scope))
|
||||
self.fail(u"No throttle rate set for {}".format(user_scope))
|
||||
|
||||
def test_create_enrollment_with_cohort(self):
|
||||
"""Enroll in the course, and also add to a cohort."""
|
||||
@@ -588,7 +600,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente
|
||||
|
||||
self.assert_enrollment_status(cohort=cohort_name)
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
|
||||
_, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
|
||||
self.assertTrue(is_active)
|
||||
self.assertEqual(cohorts.get_cohort(self.user, self.course.id, assign=False).name, cohort_name)
|
||||
|
||||
@@ -636,7 +648,11 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente
|
||||
|
||||
# Passes the include_expired parameter to the API call
|
||||
v_response = self.client.get(
|
||||
reverse('courseenrollmentdetails', kwargs={"course_id": six.text_type(self.course.id)}), {'include_expired': True}
|
||||
reverse(
|
||||
'courseenrollmentdetails',
|
||||
kwargs={"course_id": six.text_type(self.course.id)}
|
||||
),
|
||||
{'include_expired': True},
|
||||
)
|
||||
v_data = json.loads(v_response.content)
|
||||
|
||||
@@ -644,7 +660,9 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente
|
||||
self.assertEqual(len(v_data['course_modes']), 2)
|
||||
|
||||
# Omits the include_expired parameter from the API call
|
||||
h_response = self.client.get(reverse('courseenrollmentdetails', kwargs={"course_id": six.text_type(self.course.id)}))
|
||||
h_response = self.client.get(
|
||||
reverse('courseenrollmentdetails', kwargs={"course_id": six.text_type(self.course.id)}),
|
||||
)
|
||||
h_data = json.loads(h_response.content)
|
||||
|
||||
# Ensure that only one course mode is returned and that it is honor
|
||||
@@ -1236,8 +1254,8 @@ class EnrollmentCrossDomainTest(ModuleStoreTestCase):
|
||||
})
|
||||
resp = self.client.get(url, HTTP_REFERER=self.REFERER)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertIn('prod-edx-csrftoken', resp.cookies) # pylint: disable=no-member
|
||||
return resp.cookies['prod-edx-csrftoken'].value # pylint: disable=no-member
|
||||
self.assertIn('prod-edx-csrftoken', resp.cookies)
|
||||
return resp.cookies['prod-edx-csrftoken'].value
|
||||
|
||||
def _cross_domain_post(self, csrf_cookie):
|
||||
"""Perform a cross-domain POST request. """
|
||||
@@ -1306,7 +1324,7 @@ class UnenrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase):
|
||||
"""
|
||||
Helper method to create a RetirementStatus with useful defaults
|
||||
"""
|
||||
pending_state = RetirementState.objects.create(
|
||||
RetirementState.objects.create(
|
||||
state_name='PENDING',
|
||||
state_execution_order=1,
|
||||
is_dead_end_state=False,
|
||||
@@ -1494,7 +1512,7 @@ class UserRoleTest(ModuleStoreTestCase):
|
||||
self._assert_roles([expected_role2], False, course_id=text_type(self.course2.id))
|
||||
|
||||
def test_roles_exception(self):
|
||||
with patch('enrollment.api.get_user_roles') as mock_get_user_roles:
|
||||
with patch('openedx.core.djangoapps.enrollments.api.get_user_roles') as mock_get_user_roles:
|
||||
mock_get_user_roles.side_effect = Exception()
|
||||
response = self.client.get(reverse('roles'))
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
@@ -14,17 +14,19 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.utils.decorators import method_decorator
|
||||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
||||
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
|
||||
from enrollment import api
|
||||
from enrollment.errors import CourseEnrollmentError, CourseEnrollmentExistsError, CourseModeNotFoundError
|
||||
from enrollment.forms import CourseEnrollmentsApiListForm
|
||||
from enrollment.paginators import CourseEnrollmentsApiListPagination
|
||||
from enrollment.serializers import CourseEnrollmentsApiListSerializer
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx.core.djangoapps.cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf
|
||||
from openedx.core.djangoapps.cors_csrf.decorators import ensure_csrf_cookie_cross_domain
|
||||
from openedx.core.djangoapps.course_groups.cohorts import CourseUserGroup, add_user_to_cohort, get_cohort_by_name
|
||||
from openedx.core.djangoapps.embargo import api as embargo_api
|
||||
from openedx.core.djangoapps.enrollments import api
|
||||
from openedx.core.djangoapps.enrollments.errors import (
|
||||
CourseEnrollmentError, CourseEnrollmentExistsError, CourseModeNotFoundError,
|
||||
)
|
||||
from openedx.core.djangoapps.enrollments.forms import CourseEnrollmentsApiListForm
|
||||
from openedx.core.djangoapps.enrollments.paginators import CourseEnrollmentsApiListPagination
|
||||
from openedx.core.djangoapps.enrollments.serializers import CourseEnrollmentsApiListSerializer
|
||||
from openedx.core.djangoapps.user_api.accounts.permissions import CanRetireUser
|
||||
from openedx.core.djangoapps.user_api.models import UserRetirementStatus
|
||||
from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in
|
||||
@@ -164,10 +166,13 @@ class EnrollmentView(APIView, ApiKeyPermissionMixIn):
|
||||
* user: The ID of the user.
|
||||
"""
|
||||
|
||||
authentication_classes = (JwtAuthentication, OAuth2AuthenticationAllowInactiveUser,
|
||||
SessionAuthenticationAllowInactiveUser,)
|
||||
permission_classes = ApiKeyHeaderPermissionIsAuthenticated,
|
||||
throttle_classes = EnrollmentUserThrottle,
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
OAuth2AuthenticationAllowInactiveUser,
|
||||
SessionAuthenticationAllowInactiveUser,
|
||||
)
|
||||
permission_classes = (ApiKeyHeaderPermissionIsAuthenticated,)
|
||||
throttle_classes = (EnrollmentUserThrottle,)
|
||||
|
||||
# Since the course about page on the marketing site uses this API to auto-enroll users,
|
||||
# we need to support cross-domain CSRF.
|
||||
@@ -236,11 +241,13 @@ class EnrollmentUserRolesView(APIView):
|
||||
logged-in user, filtered by course_id if given, along with
|
||||
whether or not the user is global staff
|
||||
"""
|
||||
authentication_classes = (JwtAuthentication,
|
||||
OAuth2AuthenticationAllowInactiveUser,
|
||||
EnrollmentCrossDomainSessionAuth)
|
||||
permission_classes = ApiKeyHeaderPermissionIsAuthenticated,
|
||||
throttle_classes = EnrollmentUserThrottle,
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
OAuth2AuthenticationAllowInactiveUser,
|
||||
EnrollmentCrossDomainSessionAuth,
|
||||
)
|
||||
permission_classes = (ApiKeyHeaderPermissionIsAuthenticated,)
|
||||
throttle_classes = (EnrollmentUserThrottle,)
|
||||
|
||||
@method_decorator(ensure_csrf_cookie_cross_domain)
|
||||
def get(self, request):
|
||||
@@ -252,7 +259,7 @@ class EnrollmentUserRolesView(APIView):
|
||||
roles_data = api.get_user_roles(request.user.username)
|
||||
if course_id:
|
||||
roles_data = [role for role in roles_data if text_type(role.course_id) == course_id]
|
||||
except Exception:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
data={
|
||||
@@ -339,7 +346,7 @@ class EnrollmentCourseDetailView(APIView):
|
||||
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
throttle_classes = EnrollmentUserThrottle,
|
||||
throttle_classes = (EnrollmentUserThrottle,)
|
||||
|
||||
def get(self, request, course_id=None):
|
||||
"""Read enrollment information for a particular course.
|
||||
@@ -405,7 +412,7 @@ class UnenrollmentView(APIView):
|
||||
returned along with a list of all courses from which the user was unenrolled.
|
||||
"""
|
||||
authentication_classes = (JwtAuthentication,)
|
||||
permission_classes = (permissions.IsAuthenticated, CanRetireUser)
|
||||
permission_classes = (permissions.IsAuthenticated, CanRetireUser,)
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
@@ -603,10 +610,13 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
|
||||
|
||||
* user: The username of the user.
|
||||
"""
|
||||
authentication_classes = (JwtAuthentication, OAuth2AuthenticationAllowInactiveUser,
|
||||
EnrollmentCrossDomainSessionAuth,)
|
||||
permission_classes = ApiKeyHeaderPermissionIsAuthenticated,
|
||||
throttle_classes = EnrollmentUserThrottle,
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
OAuth2AuthenticationAllowInactiveUser,
|
||||
EnrollmentCrossDomainSessionAuth,
|
||||
)
|
||||
permission_classes = (ApiKeyHeaderPermissionIsAuthenticated,)
|
||||
throttle_classes = (EnrollmentUserThrottle,)
|
||||
|
||||
# Since the course about page on the marketing site
|
||||
# uses this API to auto-enroll users, we need to support
|
||||
@@ -652,6 +662,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
|
||||
return Response(filtered_data)
|
||||
|
||||
def post(self, request):
|
||||
# pylint: disable=too-many-statements
|
||||
"""Enrolls the currently logged-in user in a course.
|
||||
|
||||
Server-to-server calls may deactivate or modify the mode of existing enrollments. All other requests
|
||||
@@ -734,8 +745,8 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
|
||||
try:
|
||||
enterprise_api_client.post_enterprise_course_enrollment(username, text_type(course_id), None)
|
||||
except EnterpriseApiException as error:
|
||||
log.exception("An unexpected error occurred while creating the new EnterpriseCourseEnrollment "
|
||||
"for user [%s] in course run [%s]", username, course_id)
|
||||
log.exception(u"An unexpected error occurred while creating the new EnterpriseCourseEnrollment "
|
||||
u"for user [%s] in course run [%s]", username, course_id)
|
||||
raise CourseEnrollmentError(text_type(error))
|
||||
kwargs = {
|
||||
'username': username,
|
||||
@@ -766,7 +777,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
|
||||
log.warning(msg)
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg})
|
||||
|
||||
if len(missing_attrs) > 0:
|
||||
if missing_attrs:
|
||||
msg = u"Missing enrollment attributes: requested mode={} required attributes={}".format(
|
||||
mode, REQUIRED_ATTRIBUTES.get(mode)
|
||||
)
|
||||
@@ -805,7 +816,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
|
||||
org = course_id.org
|
||||
update_email_opt_in(request.user, org, email_opt_in)
|
||||
|
||||
log.info('The user [%s] has already been enrolled in course run [%s].', username, course_id)
|
||||
log.info(u'The user [%s] has already been enrolled in course run [%s].', username, course_id)
|
||||
return Response(response)
|
||||
except CourseModeNotFoundError as error:
|
||||
return Response(
|
||||
@@ -824,11 +835,11 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
|
||||
}
|
||||
)
|
||||
except CourseEnrollmentExistsError as error:
|
||||
log.warning('An enrollment already exists for user [%s] in course run [%s].', username, course_id)
|
||||
log.warning(u'An enrollment already exists for user [%s] in course run [%s].', username, course_id)
|
||||
return Response(data=error.enrollment)
|
||||
except CourseEnrollmentError:
|
||||
log.exception("An error occurred while creating the new course enrollment for user "
|
||||
"[%s] in course run [%s]", username, course_id)
|
||||
log.exception(u"An error occurred while creating the new course enrollment for user "
|
||||
u"[%s] in course run [%s]", username, course_id)
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
data={
|
||||
@@ -839,11 +850,11 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
|
||||
}
|
||||
)
|
||||
except CourseUserGroup.DoesNotExist:
|
||||
log.exception('Missing cohort [%s] in course run [%s]', cohort_name, course_id)
|
||||
log.exception(u'Missing cohort [%s] in course run [%s]', cohort_name, course_id)
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
data={
|
||||
"message": "An error occured while adding to cohort [%s]" % cohort_name
|
||||
"message": u"An error occured while adding to cohort [%s]" % cohort_name
|
||||
})
|
||||
finally:
|
||||
# Assumes that the ecommerce service uses an API key to authenticate.
|
||||
@@ -930,8 +941,8 @@ class CourseEnrollmentsApiListView(DeveloperErrorViewMixin, ListAPIView):
|
||||
OAuth2AuthenticationAllowInactiveUser,
|
||||
SessionAuthenticationAllowInactiveUser,
|
||||
)
|
||||
permission_classes = (permissions.IsAdminUser, )
|
||||
throttle_classes = (EnrollmentUserThrottle, )
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
throttle_classes = (EnrollmentUserThrottle,)
|
||||
serializer_class = CourseEnrollmentsApiListSerializer
|
||||
pagination_class = CourseEnrollmentsApiListPagination
|
||||
|
||||
@@ -10,7 +10,7 @@ import pytz
|
||||
from django.test import TestCase
|
||||
from social_django.models import UserSocialAuth
|
||||
|
||||
from enrollment import api
|
||||
from openedx.core.djangoapps.enrollments import api
|
||||
from openedx.core.djangoapps.user_api.models import RetirementState, UserRetirementStatus
|
||||
from student.models import get_retired_email_by_email, get_retired_username_by_username
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
Reference in New Issue
Block a user