Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
ecommerce
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
ecommerce
Commits
faae1e32
Commit
faae1e32
authored
Aug 31, 2015
by
Renzo Lucioni
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #307 from edx/renzo/publish-credit-course
Support publication of Courses with a credit seat
parents
8fd7e228
e2cb8f00
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
206 additions
and
10 deletions
+206
-10
ecommerce/courses/models.py
+2
-2
ecommerce/courses/publishers.py
+66
-5
ecommerce/courses/tests/test_publishers.py
+101
-0
ecommerce/extensions/api/serializers.py
+6
-1
ecommerce/extensions/api/v2/tests/views/test_publication.py
+23
-0
ecommerce/extensions/api/v2/views.py
+7
-1
ecommerce/extensions/catalogue/management/commands/migrate_course.py
+1
-1
No files found.
ecommerce/courses/models.py
View file @
faae1e32
...
...
@@ -56,9 +56,9 @@ class Course(models.Model):
super
(
Course
,
self
)
.
save
(
force_insert
,
force_update
,
using
,
update_fields
)
self
.
_create_parent_seat
()
def
publish_to_lms
(
self
):
def
publish_to_lms
(
self
,
access_token
=
None
):
""" Publish Course and Products to LMS. """
return
LMSPublisher
()
.
publish
(
self
)
return
LMSPublisher
()
.
publish
(
self
,
access_token
=
access_token
)
@classmethod
def
is_mode_verified
(
cls
,
mode
):
...
...
ecommerce/courses/publishers.py
View file @
faae1e32
...
...
@@ -5,11 +5,15 @@ from django.conf import settings
import
requests
from
ecommerce.courses.utils
import
mode_for_seat
from
ecommerce.settings.base
import
get_lms_url
logger
=
logging
.
getLogger
(
__name__
)
class
LMSPublisher
(
object
):
timeout
=
settings
.
COMMERCE_API_TIMEOUT
def
get_seat_expiration
(
self
,
seat
):
if
not
seat
.
expires
or
'professional'
in
getattr
(
seat
.
attr
,
'certificate_type'
,
''
):
return
None
...
...
@@ -30,14 +34,46 @@ class LMSPublisher(object):
'expires'
:
self
.
get_seat_expiration
(
seat
),
}
def
publish
(
self
,
course
):
def
_publish_creditcourse
(
self
,
course_id
,
access_token
):
"""Creates or updates a CreditCourse object on the LMS."""
url
=
get_lms_url
(
'api/credit/v1/courses/'
)
data
=
{
'course_key'
:
course_id
,
'enabled'
:
True
}
headers
=
{
'Content-Type'
:
'application/json'
,
'Authorization'
:
'Bearer '
+
access_token
}
kwargs
=
{
'url'
:
url
,
'data'
:
json
.
dumps
(
data
),
'headers'
:
headers
,
'timeout'
:
self
.
timeout
}
response
=
requests
.
post
(
**
kwargs
)
if
response
.
status_code
==
400
:
# The CreditCourse already exists. Try updating it.
kwargs
[
'url'
]
+=
course_id
.
strip
(
'/'
)
+
'/'
response
=
requests
.
put
(
**
kwargs
)
return
response
def
publish
(
self
,
course
,
access_token
=
None
):
""" Publish course commerce data to LMS.
Uses the Commerce API to publish course modes, prices, and SKUs to LMS.
Uses the Commerce API to publish course modes, prices, and SKUs to LMS. Uses
CreditCourse API endpoints to publish CreditCourse data to LMS when necessary.
Args:
Arg
ument
s:
course (Course): Course to be published.
Keyword Arguments:
access_token (str): Access token used when publishing CreditCourse data to the LMS.
Returns:
True, if publish operation succeeded; otherwise, False.
"""
...
...
@@ -50,6 +86,32 @@ class LMSPublisher(object):
name
=
course
.
name
verification_deadline
=
self
.
get_course_verification_deadline
(
course
)
modes
=
[
self
.
serialize_seat_for_commerce_api
(
seat
)
for
seat
in
course
.
seat_products
]
has_credit
=
'credit'
in
[
mode
[
'name'
]
for
mode
in
modes
]
if
has_credit
:
if
access_token
is
not
None
:
try
:
response
=
self
.
_publish_creditcourse
(
course_id
,
access_token
)
if
response
.
status_code
in
(
200
,
201
):
logger
.
info
(
u'Successfully published CreditCourse for [
%
s] to LMS.'
,
course_id
)
else
:
logger
.
error
(
u'Failed to publish CreditCourse for [
%
s] to LMS. Status was [
%
d]. Body was [
%
s].'
,
course_id
,
response
.
status_code
,
response
.
content
)
return
False
except
:
# pylint: disable=bare-except
logger
.
exception
(
u'Failed to publish CreditCourse for [
%
s] to LMS.'
,
course_id
)
return
False
else
:
logger
.
error
(
u'Unable to publish CreditCourse for [
%
s] to LMS. No access token available.'
,
course_id
)
return
False
data
=
{
'id'
:
course_id
,
'name'
:
name
,
...
...
@@ -58,7 +120,6 @@ class LMSPublisher(object):
}
url
=
'{}/courses/{}/'
.
format
(
settings
.
COMMERCE_API_URL
.
rstrip
(
'/'
),
course_id
)
timeout
=
settings
.
COMMERCE_API_TIMEOUT
headers
=
{
'Content-Type'
:
'application/json'
,
...
...
@@ -66,7 +127,7 @@ class LMSPublisher(object):
}
try
:
response
=
requests
.
put
(
url
,
data
=
json
.
dumps
(
data
),
headers
=
headers
,
timeout
=
timeout
)
response
=
requests
.
put
(
url
,
data
=
json
.
dumps
(
data
),
headers
=
headers
,
timeout
=
self
.
timeout
)
status_code
=
response
.
status_code
if
status_code
in
(
200
,
201
):
logger
.
info
(
u'Successfully published commerce data for [
%
s].'
,
course_id
)
...
...
ecommerce/courses/tests/test_publishers.py
View file @
faae1e32
...
...
@@ -13,6 +13,8 @@ from testfixtures import LogCapture
from
ecommerce.courses.models
import
Course
from
ecommerce.courses.publishers
import
LMSPublisher
from
ecommerce.extensions.catalogue.tests.mixins
import
CourseCatalogTestMixin
from
ecommerce.settings.base
import
get_lms_url
EDX_API_KEY
=
'edx'
JSON
=
'application/json'
...
...
@@ -37,6 +39,28 @@ class LMSPublisherTests(CourseCatalogTestMixin, TestCase):
httpretty
.
register_uri
(
httpretty
.
PUT
,
url
,
status
=
status
,
body
=
json
.
dumps
(
body
),
content_type
=
JSON
)
def
_mock_credit_api
(
self
,
creation_status
,
update_status
):
self
.
assertTrue
(
httpretty
.
is_enabled
,
'httpretty must be enabled to mock Credit API calls.'
)
url
=
get_lms_url
(
'api/credit/v1/courses/'
)
httpretty
.
register_uri
(
httpretty
.
POST
,
url
,
status
=
creation_status
,
body
=
'{}'
,
content_type
=
JSON
)
if
update_status
is
not
None
:
url
+=
self
.
course
.
id
.
strip
(
'/'
)
+
'/'
httpretty
.
register_uri
(
httpretty
.
PUT
,
url
,
status
=
update_status
,
body
=
'{}'
,
content_type
=
JSON
)
@ddt.data
(
''
,
None
)
def
test_commerce_api_url_not_set
(
self
,
setting_value
):
""" If the Commerce API is not setup, the method should log an INFO message and return """
...
...
@@ -139,3 +163,80 @@ class LMSPublisherTests(CourseCatalogTestMixin, TestCase):
'expires'
:
None
}
self
.
assertDictEqual
(
actual
,
expected
)
@httpretty.activate
@ddt.data
(
(
201
,
None
,
201
),
(
400
,
200
,
200
)
)
@ddt.unpack
def
test_credit_publication_success
(
self
,
creation_status
,
update_status
,
commerce_status
):
"""
Verify that course publication succeeds if the Credit API responds
with 2xx status codes when publishing CreditCourse data to the LMS.
"""
self
.
course
.
create_or_update_seat
(
'credit'
,
True
,
100
,
credit_provider
=
'Harvard'
,
credit_hours
=
1
)
self
.
_mock_credit_api
(
creation_status
,
update_status
)
self
.
_mock_commerce_api
(
commerce_status
)
access_token
=
'access_token'
published
=
self
.
publisher
.
publish
(
self
.
course
,
access_token
=
access_token
)
self
.
assertTrue
(
published
)
# Retrieve the latest request to the Credit API.
if
creation_status
==
400
:
latest_request
=
httpretty
.
httpretty
.
latest_requests
[
1
]
else
:
latest_request
=
httpretty
.
httpretty
.
latest_requests
[
0
]
# Verify the headers passed to the Credit API were correct.
expected
=
{
'Content-Type'
:
JSON
,
'Authorization'
:
'Bearer '
+
access_token
}
self
.
assertDictContainsSubset
(
expected
,
latest_request
.
headers
)
# Verify the data passed to the Credit API was correct.
expected
=
{
'course_key'
:
self
.
course
.
id
,
'enabled'
:
True
}
actual
=
json
.
loads
(
latest_request
.
body
)
self
.
assertEqual
(
expected
,
actual
)
@httpretty.activate
def
test_credit_publication_failure
(
self
):
"""
Verify that course publication fails if the Credit API does not respond
with 2xx status codes when publishing CreditCourse data to the LMS.
"""
self
.
course
.
create_or_update_seat
(
'credit'
,
True
,
100
,
credit_provider
=
'Harvard'
,
credit_hours
=
1
)
self
.
_mock_credit_api
(
400
,
418
)
published
=
self
.
publisher
.
publish
(
self
.
course
,
access_token
=
'access_token'
)
self
.
assertFalse
(
published
)
def
test_credit_publication_no_access_token
(
self
):
"""
Verify that course publication fails if no access token is provided
when publishing CreditCourse data to the LMS.
"""
self
.
course
.
create_or_update_seat
(
'credit'
,
True
,
100
,
credit_provider
=
'Harvard'
,
credit_hours
=
1
)
published
=
self
.
publisher
.
publish
(
self
.
course
,
access_token
=
None
)
self
.
assertFalse
(
published
)
def
test_credit_publication_exception
(
self
):
"""
Verify that course publication fails if an exception is raised
while publishing CreditCourse data to the LMS.
"""
self
.
course
.
create_or_update_seat
(
'credit'
,
True
,
100
,
credit_provider
=
'Harvard'
,
credit_hours
=
1
)
with
mock
.
patch
.
object
(
LMSPublisher
,
'_publish_creditcourse'
)
as
mock_publish_creditcourse
:
mock_publish_creditcourse
.
side_effect
=
Exception
published
=
self
.
publisher
.
publish
(
self
.
course
,
access_token
=
'access_token'
)
self
.
assertFalse
(
published
)
ecommerce/extensions/api/serializers.py
View file @
faae1e32
...
...
@@ -162,6 +162,11 @@ class AtomicPublicationSerializer(serializers.Serializer): # pylint: disable=ab
verification_deadline
=
serializers
.
DateTimeField
(
required
=
False
,
allow_null
=
True
)
products
=
serializers
.
ListField
()
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
AtomicPublicationSerializer
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
access_token
=
kwargs
[
'context'
]
.
pop
(
'access_token'
)
def
validate_products
(
self
,
products
):
"""Validate product data."""
for
product
in
products
:
...
...
@@ -236,7 +241,7 @@ class AtomicPublicationSerializer(serializers.Serializer): # pylint: disable=ab
credit_hours
=
credit_hours
,
)
published
=
course
.
publish_to_lms
()
published
=
course
.
publish_to_lms
(
access_token
=
self
.
access_token
)
if
published
:
return
created
,
None
,
None
else
:
...
...
ecommerce/extensions/api/v2/tests/views/test_publication.py
View file @
faae1e32
...
...
@@ -74,6 +74,29 @@ class AtomicPublicationTests(CourseCatalogTestMixin, UserMixin, TestCase):
'value'
:
True
}
]
},
{
'product_class'
:
'Seat'
,
'expires'
:
EXPIRES_STRING
,
'price'
:
100.00
,
'attribute_values'
:
[
{
'name'
:
'certificate_type'
,
'value'
:
'credit'
},
{
'name'
:
'id_verification_required'
,
'value'
:
True
},
{
'name'
:
'credit_provider'
,
'value'
:
'Harvard'
},
{
'name'
:
'credit_hours'
,
'value'
:
1
}
]
}
]
}
...
...
ecommerce/extensions/api/v2/views.py
View file @
faae1e32
...
...
@@ -514,7 +514,8 @@ class CourseViewSet(NonDestroyableModelViewSet):
'because the switch [publish_course_modes_to_lms] is disabled.'
if
waffle
.
switch_is_active
(
'publish_course_modes_to_lms'
):
published
=
course
.
publish_to_lms
()
access_token
=
getattr
(
request
.
user
,
'access_token'
,
None
)
published
=
course
.
publish_to_lms
(
access_token
=
access_token
)
if
published
:
msg
=
'Course [{course_id}] was successfully published to LMS.'
else
:
...
...
@@ -538,6 +539,11 @@ class AtomicPublicationView(generics.CreateAPIView, generics.UpdateAPIView):
permission_classes
=
(
IsAuthenticated
,
IsAdminUser
,)
serializer_class
=
serializers
.
AtomicPublicationSerializer
def
get_serializer_context
(
self
):
context
=
super
(
AtomicPublicationView
,
self
)
.
get_serializer_context
()
context
[
'access_token'
]
=
self
.
request
.
user
.
access_token
return
context
def
post
(
self
,
request
,
*
args
,
**
kwargs
):
return
self
.
_save_and_publish
(
request
.
data
)
...
...
ecommerce/extensions/catalogue/management/commands/migrate_course.py
View file @
faae1e32
...
...
@@ -202,7 +202,7 @@ class Command(BaseCommand):
if
options
.
get
(
'commit'
,
False
):
logger
.
info
(
'Course [
%
s] was saved to the database.'
,
course
.
id
)
if
waffle
.
switch_is_active
(
'publish_course_modes_to_lms'
):
course
.
publish_to_lms
()
course
.
publish_to_lms
(
access_token
=
access_token
)
else
:
logger
.
info
(
'Data was not published to LMS because the switch '
'[publish_course_modes_to_lms] is disabled.'
)
...
...
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