Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
eb5fc311
Commit
eb5fc311
authored
Nov 22, 2017
by
Albert St. Aubin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactored the API to be part of the Entitlement API, removed it from
the Enrollment API
parent
b0a19e94
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
161 additions
and
164 deletions
+161
-164
common/djangoapps/enrollment/tests/test_views.py
+1
-95
common/djangoapps/enrollment/views.py
+18
-66
common/djangoapps/entitlements/api/v1/urls.py
+12
-1
common/djangoapps/entitlements/api/v1/views.py
+109
-1
common/djangoapps/entitlements/migrations/0003_auto_20171120_1432.py
+20
-0
common/djangoapps/entitlements/models.py
+1
-1
No files found.
common/djangoapps/enrollment/tests/test_views.py
View file @
eb5fc311
...
...
@@ -5,7 +5,6 @@ import datetime
import
itertools
import
json
import
unittest
import
uuid
import
ddt
import
httpretty
...
...
@@ -25,11 +24,9 @@ from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
from
course_modes.models
import
CourseMode
from
course_modes.tests.factories
import
CourseModeFactory
from
entitlements.tests.factories
import
CourseEntitlementFactory
from
enrollment
import
api
from
enrollment.errors
import
CourseEnrollmentError
from
enrollment.views
import
EnrollmentUserThrottle
from
entitlements.models
import
CourseEntitlement
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
openedx.core.djangoapps.embargo.models
import
Country
,
CountryAccessRule
,
RestrictedCourse
from
openedx.core.djangoapps.embargo.test_utils
import
restrict_course
...
...
@@ -38,7 +35,7 @@ from openedx.core.lib.django_test_client_utils import get_absolute_url
from
openedx.features.enterprise_support.tests.mixins.enterprise
import
EnterpriseServiceMockMixin
from
student.models
import
CourseEnrollment
from
student.roles
import
CourseStaffRole
from
student.tests.factories
import
AdminFactory
,
UserFactory
,
TEST_PASSWORD
from
student.tests.factories
import
AdminFactory
,
UserFactory
from
util.models
import
RateLimitConfiguration
from
util.testing
import
UrlResetMixin
...
...
@@ -50,7 +47,6 @@ class EnrollmentTestMixin(object):
def
assert_enrollment_status
(
self
,
course_id
=
None
,
course_uuid
=
None
,
username
=
None
,
expected_status
=
status
.
HTTP_200_OK
,
email_opt_in
=
None
,
...
...
@@ -81,9 +77,6 @@ class EnrollmentTestMixin(object):
'enrollment_attributes'
:
enrollment_attributes
}
if
course_uuid
:
data
[
'course_details'
][
'course_uuid'
]
=
course_uuid
if
is_active
is
not
None
:
data
[
'is_active'
]
=
is_active
...
...
@@ -140,93 +133,6 @@ class EnrollmentTestMixin(object):
self
.
assertEqual
(
actual_mode
,
expected_mode
)
# @override_settings(EDX_API_KEY="i am a key")
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
class
EntitlementEnrollmentTest
(
EnrollmentTestMixin
,
ModuleStoreTestCase
,
APITestCase
):
def
setUp
(
self
):
super
(
EntitlementEnrollmentTest
,
self
)
.
setUp
()
self
.
course
=
CourseFactory
()
self
.
user
=
UserFactory
()
CourseModeFactory
.
create
(
course_id
=
self
.
course
.
id
,
mode_slug
=
CourseMode
.
VERIFIED
,
mode_display_name
=
CourseMode
.
VERIFIED
,
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
def
test_enroll_entitlement
(
self
):
entitlement
=
CourseEntitlementFactory
.
create
(
user
=
self
.
user
,
mode
=
'verified'
)
resp
=
self
.
assert_enrollment_status
(
course_id
=
unicode
(
self
.
course
.
id
),
course_uuid
=
str
(
entitlement
.
course_uuid
),
is_active
=
True
,
mode
=
None
,
max_mongo_calls
=
4
)
data
=
json
.
loads
(
resp
.
content
)
self
.
assertEqual
(
self
.
course
.
display_name_with_default
,
data
[
'course_details'
][
'course_name'
])
# Verify that the enrollment was created correctly
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course
.
id
))
course_mode
,
is_active
=
CourseEnrollment
.
enrollment_mode_for_user
(
self
.
user
,
self
.
course
.
id
)
self
.
assertTrue
(
is_active
)
self
.
assertEqual
(
course_mode
,
entitlement
.
mode
)
entitlement
.
refresh_from_db
()
# Verify the Entitlement settings are correct
self
.
assertIsNotNone
(
entitlement
.
enrollment_course_run
)
self
.
assertEqual
(
entitlement
.
enrollment_course_run
.
course_id
,
self
.
course
.
id
)
def
test_unenroll_entitlement
(
self
):
entitlement
=
CourseEntitlementFactory
.
create
(
user
=
self
.
user
,
mode
=
'verified'
)
# Enroll user
self
.
assert_enrollment_status
(
course_id
=
unicode
(
self
.
course
.
id
),
course_uuid
=
str
(
entitlement
.
course_uuid
),
is_active
=
True
,
mode
=
None
,
max_mongo_calls
=
4
)
# Unenroll the user
resp
=
self
.
assert_enrollment_status
(
course_id
=
unicode
(
self
.
course
.
id
),
course_uuid
=
str
(
entitlement
.
course_uuid
),
is_active
=
False
,
mode
=
None
,
max_mongo_calls
=
4
)
data
=
json
.
loads
(
resp
.
content
)
self
.
assertEqual
(
self
.
course
.
display_name_with_default
,
data
[
'course_details'
][
'course_name'
])
# Verify that the enrollment was created correctly
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course
.
id
))
course_mode
,
is_active
=
CourseEnrollment
.
enrollment_mode_for_user
(
self
.
user
,
self
.
course
.
id
)
self
.
assertFalse
(
is_active
)
self
.
assertEqual
(
course_mode
,
entitlement
.
mode
)
entitlement
.
refresh_from_db
()
self
.
assertIsNone
(
entitlement
.
enrollment_course_run
)
def
test_enroll_no_entitlement
(
self
):
resp
=
self
.
assert_enrollment_status
(
course_id
=
unicode
(
self
.
course
.
id
),
course_uuid
=
str
(
uuid
.
uuid4
()),
is_active
=
True
,
mode
=
None
,
max_mongo_calls
=
4
,
expected_status
=
status
.
HTTP_400_BAD_REQUEST
)
data
=
json
.
loads
(
resp
.
content
)
self
.
assertEqual
(
self
.
course
.
display_name_with_default
,
data
[
'course_details'
][
'course_name'
])
# Verify that the enrollment was created correctly
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course
.
id
))
@attr
(
shard
=
3
)
@override_settings
(
EDX_API_KEY
=
"i am a key"
)
@ddt.ddt
...
...
common/djangoapps/enrollment/views.py
View file @
eb5fc311
...
...
@@ -18,7 +18,6 @@ from rest_framework.views import APIView
from
course_modes.models
import
CourseMode
from
enrollment
import
api
from
enrollment.errors
import
CourseEnrollmentError
,
CourseEnrollmentExistsError
,
CourseModeNotFoundError
from
entitlements.models
import
CourseEntitlement
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.embargo
import
api
as
embargo_api
...
...
@@ -37,7 +36,6 @@ from openedx.features.enterprise_support.api import (
enterprise_enabled
)
from
student.auth
import
user_has_role
from
student.models
import
CourseEnrollment
from
student.models
import
User
from
student.roles
import
CourseStaffRole
,
GlobalStaff
from
util.disable_rate_limit
import
can_disable_rate_limit
...
...
@@ -530,11 +528,25 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
# Get the User, Course ID, and Mode from the request.
username
=
request
.
data
.
get
(
'user'
,
request
.
user
.
username
)
# Note that course_id is actually the Course Run Key
course_id
=
request
.
data
.
get
(
'course_details'
,
{})
.
get
(
'course_id'
)
course_uuid
=
request
.
data
.
get
(
'course_details'
,
{})
.
get
(
'course_uuid'
)
if
not
course_id
:
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
{
"message"
:
u"Course ID must be specified to create a new enrollment."
}
)
try
:
course_id
=
CourseKey
.
from_string
(
course_id
)
except
InvalidKeyError
:
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
{
"message"
:
u"No course '{course_id}' found for enrollment"
.
format
(
course_id
=
course_id
)
}
)
mode
=
request
.
data
.
get
(
'mode'
)
is_active
=
request
.
data
.
get
(
'is_active'
)
has_api_key_permissions
=
self
.
has_api_key_permissions
(
request
)
...
...
@@ -567,46 +579,13 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
}
)
course_entitlement
=
None
if
course_uuid
:
course_entitlement
=
CourseEntitlement
.
get_active_user_course_entitlements
(
user
,
course_uuid
)
if
course_entitlement
and
course_entitlement
.
enrollment_course_run
is
not
None
and
is_active
:
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
{
"message"
:
u"Entitlement for {course_uuid} already has an enrollment applied"
.
format
(
course_uuid
=
course_uuid
)
}
)
if
not
course_id
:
if
course_entitlement
and
course_entitlement
.
enrollment_course_run
is
not
None
:
course_id
=
course_entitlement
.
enrollment_course_run
.
course_id
else
:
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
{
"message"
:
u"Course ID must be specified to create a new enrollment."
}
)
else
:
try
:
course_id
=
CourseKey
.
from_string
(
course_id
)
except
InvalidKeyError
:
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
{
"message"
:
u"No course '{course_id}' found for enrollment"
.
format
(
course_id
=
course_id
)
}
)
embargo_response
=
embargo_api
.
get_embargo_response
(
request
,
course_id
,
user
)
if
embargo_response
:
return
embargo_response
try
:
is_active
=
request
.
data
.
get
(
'is_active'
)
# Check if the requested activation status is None or a Boolean
if
is_active
is
not
None
and
not
isinstance
(
is_active
,
bool
):
return
Response
(
...
...
@@ -633,33 +612,6 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
}
consent_client
.
provide_consent
(
**
kwargs
)
# Add Enrollment for the Entitlement user with the correct Mode
# This should only occur if the User has a Course Entitlement in place.
# As a reault the api_key_permissions do not apply the User may enroll themselves based on the entitlement.
if
course_entitlement
and
is_active
:
mode
=
course_entitlement
.
mode
response
=
api
.
add_enrollment
(
username
,
unicode
(
course_id
),
mode
=
mode
,
is_active
=
True
,
)
CourseEntitlement
.
set_enrollment
(
entitlement
=
course_entitlement
,
enrollment
=
CourseEnrollment
.
get_enrollment
(
user
,
course_id
)
)
log
.
info
(
'Enrolling [
%
s] entitlement for run [
%
s] of Course [
%
s].'
,
username
,
course_id
,
course_uuid
)
elif
course_entitlement
and
is_active
is
not
None
and
not
is_active
:
# Unenroll the course as part of the entitlement
response
=
api
.
update_enrollment
(
username
,
unicode
(
course_id
),
mode
=
mode
,
is_active
=
is_active
,
)
CourseEntitlement
.
set_enrollment
(
course_entitlement
,
None
)
log
.
info
(
'Unenrolling [
%
s] entitlement for run [
%
s] of Course [
%
s].'
,
username
,
course_id
,
course_uuid
)
else
:
enrollment_attributes
=
request
.
data
.
get
(
'enrollment_attributes'
)
enrollment
=
api
.
get_enrollment
(
username
,
unicode
(
course_id
))
mode_changed
=
enrollment
and
mode
is
not
None
and
enrollment
[
'mode'
]
!=
mode
...
...
common/djangoapps/entitlements/api/v1/urls.py
View file @
eb5fc311
from
django.conf.urls
import
url
,
include
from
rest_framework.routers
import
DefaultRouter
from
.views
import
EntitlementViewSet
from
.views
import
EntitlementViewSet
,
EntitlementEnrollmentViewSet
router
=
DefaultRouter
()
router
.
register
(
r'entitlements'
,
EntitlementViewSet
,
base_name
=
'entitlements'
)
enrollments_view
=
EntitlementEnrollmentViewSet
.
as_view
({
'post'
:
'create'
,
'delete'
:
'destroy'
,
})
urlpatterns
=
[
url
(
r''
,
include
(
router
.
urls
)),
url
(
r'entitlements/(?P<uuid>[0-9a-f-]+)/enrollments/$'
,
enrollments_view
,
name
=
'enrollments'
)
]
common/djangoapps/entitlements/api/v1/views.py
View file @
eb5fc311
...
...
@@ -3,13 +3,19 @@ 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
import
permissions
,
viewsets
,
status
from
rest_framework.response
import
Response
from
rest_framework.authentication
import
SessionAuthentication
from
openedx.core.djangoapps.catalog.utils
import
get_course_runs_for_course
from
entitlements.api.v1.filters
import
CourseEntitlementFilter
from
entitlements.api.v1.permissions
import
IsAdminOrAuthenticatedReadOnly
from
entitlements.models
import
CourseEntitlement
from
entitlements.api.v1.serializers
import
CourseEntitlementSerializer
from
entitlements.models
import
CourseEntitlement
from
openedx.core.djangoapps.cors_csrf.authentication
import
SessionAuthenticationCrossDomainCsrf
from
opaque_keys.edx.keys
import
CourseKey
from
student.models
import
CourseEnrollment
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -57,3 +63,105 @@ class EntitlementViewSet(viewsets.ModelViewSet):
)
if
save_model
:
instance
.
save
()
class
EntitlementEnrollmentViewSet
(
viewsets
.
GenericViewSet
):
authentication_classes
=
(
JwtAuthentication
,
SessionAuthentication
,)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
queryset
=
CourseEntitlement
.
objects
.
all
()
serializer_class
=
CourseEntitlementSerializer
def
_enroll_entitlement
(
self
,
entitlement
,
course_session_key
,
user
):
enrollment
=
CourseEnrollment
.
enroll
(
user
=
user
,
course_key
=
course_session_key
,
mode
=
entitlement
.
mode
,
)
CourseEntitlement
.
set_enrollment
(
entitlement
,
enrollment
)
def
_unenroll_entitlement
(
self
,
entitlement
,
course_session_key
,
user
):
CourseEnrollment
.
unenroll
(
user
,
course_session_key
,
skip_refund
=
True
)
CourseEntitlement
.
set_enrollment
(
entitlement
,
None
)
def
create
(
self
,
request
,
uuid
):
course_session_id
=
request
.
data
.
get
(
'course_session_id'
,
None
)
if
not
course_session_id
:
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
"The Course Run ID was not provided."
)
# Verify that the user has an Entitlement for the provided Course UUID.
try
:
entitlement
=
CourseEntitlement
.
objects
.
get
(
uuid
=
uuid
,
user
=
request
.
user
,
expired_at
=
None
)
except
CourseEntitlement
.
DoesNotExist
:
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
"The Entitlement for this UUID does not exist or is Expired."
)
# Verify the course run ID is of the same type as the Course entitlement.
course_run_valid
=
False
course_runs
=
get_course_runs_for_course
(
entitlement
.
course_uuid
)
for
run
in
course_runs
:
if
course_session_id
==
run
.
get
(
'key'
,
''
):
course_run_valid
=
True
if
not
course_run_valid
:
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
"The Course Run ID is not a match for this Course Entitlement."
)
# Determine if this is a Switch session or a simple enroll and handle both.
if
entitlement
.
enrollment_course_run
is
None
:
self
.
_enroll_entitlement
(
entitlement
=
entitlement
,
course_session_key
=
CourseKey
.
from_string
(
course_session_id
),
user
=
request
.
user
)
else
:
if
entitlement
.
enrollment_course_run
.
course_id
!=
course_session_id
:
self
.
_unenroll_entitlement
(
entitlement
=
entitlement
,
course_session_key
=
entitlement
.
enrollment_course_run
.
course_id
,
user
=
request
.
user
)
self
.
_enroll_entitlement
(
entitlement
=
entitlement
,
course_session_key
=
CourseKey
.
from_string
(
course_session_id
),
user
=
request
.
user
)
return
Response
(
status
=
status
.
HTTP_201_CREATED
,
data
=
{
'uuid'
:
entitlement
.
uuid
,
'course_run_id'
:
course_session_id
,
'is_active'
:
True
}
)
def
destroy
(
self
,
request
,
uuid
):
"""
On DELETE call to this API we will unenroll the course enrollment for the provided uuid
"""
try
:
entitlement
=
CourseEntitlement
.
objects
.
get
(
uuid
=
uuid
,
user
=
request
.
user
,
expired_at
=
None
)
except
CourseEntitlement
.
DoesNotExist
:
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
"The Entitlement for this UUID does not exist or is Expired."
)
if
entitlement
.
enrollment_course_run
is
None
:
return
Response
()
self
.
_unenroll_entitlement
(
entitlement
=
entitlement
,
course_session_key
=
entitlement
.
enrollment_course_run
.
course_id
,
user
=
request
.
user
)
return
Response
(
status
=
status
.
HTTP_204_NO_CONTENT
)
common/djangoapps/entitlements/migrations/0003_auto_20171120_1432.py
0 → 100644
View file @
eb5fc311
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
import
uuid
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'entitlements'
,
'0002_auto_20171102_0719'
),
]
operations
=
[
migrations
.
AlterField
(
model_name
=
'courseentitlement'
,
name
=
'uuid'
,
field
=
models
.
UUIDField
(
default
=
uuid
.
uuid4
,
unique
=
True
,
editable
=
False
),
),
]
common/djangoapps/entitlements/models.py
View file @
eb5fc311
...
...
@@ -11,7 +11,7 @@ class CourseEntitlement(TimeStampedModel):
"""
user
=
models
.
ForeignKey
(
settings
.
AUTH_USER_MODEL
)
uuid
=
models
.
UUIDField
(
default
=
uuid_tools
.
uuid4
,
editable
=
False
)
uuid
=
models
.
UUIDField
(
default
=
uuid_tools
.
uuid4
,
editable
=
False
,
unique
=
True
)
course_uuid
=
models
.
UUIDField
(
help_text
=
'UUID for the Course, not the Course Run'
)
expired_at
=
models
.
DateTimeField
(
null
=
True
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment