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
09405a75
Commit
09405a75
authored
Aug 05, 2015
by
Clinton Blackburn
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #9193 from edx/patch/2015-08-04
ECOM Patch
parents
69be9000
69e9ac1a
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
153 additions
and
55 deletions
+153
-55
lms/djangoapps/commerce/api/v1/models.py
+26
-4
lms/djangoapps/commerce/api/v1/serializers.py
+29
-1
lms/djangoapps/commerce/api/v1/tests/test_views.py
+93
-50
lms/djangoapps/commerce/api/v1/views.py
+5
-0
No files found.
lms/djangoapps/commerce/api/v1/models.py
View file @
09405a75
""" API v1 models. """
from
itertools
import
groupby
import
logging
import
logging
from
django.db
import
transaction
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
course_modes.models
import
CourseMode
from
verify_student.models
import
VerificationDeadline
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -17,11 +18,25 @@ class Course(object):
modes
=
None
_deleted_modes
=
None
def
__init__
(
self
,
id
,
modes
):
# pylint: disable=invalid-name,redefined-builtin
def
__init__
(
self
,
id
,
modes
,
verification_deadline
=
None
):
# pylint: disable=invalid-name,redefined-builtin
self
.
id
=
CourseKey
.
from_string
(
unicode
(
id
))
# pylint: disable=invalid-name
self
.
modes
=
list
(
modes
)
self
.
verification_deadline
=
verification_deadline
self
.
_deleted_modes
=
[]
@property
def
name
(
self
):
""" Return course name. """
course_id
=
CourseKey
.
from_string
(
unicode
(
self
.
id
))
# pylint: disable=invalid-name
try
:
return
CourseOverview
.
get_from_id
(
course_id
)
.
display_name
except
CourseOverview
.
DoesNotExist
:
# NOTE (CCB): Ideally, the course modes table should only contain data for courses that exist in
# modulestore. If that is not the case, say for local development/testing, carry on without failure.
log
.
warning
(
'Failed to retrieve CourseOverview for [
%
s]. Using empty course name.'
,
course_id
)
return
None
def
get_mode_display_name
(
self
,
mode
):
""" Returns display name for the given mode. """
slug
=
mode
.
mode_slug
.
strip
()
.
lower
()
...
...
@@ -42,6 +57,10 @@ class Course(object):
@transaction.commit_on_success
def
save
(
self
,
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
""" Save the CourseMode objects to the database. """
# Update the verification deadline for the course (not the individual modes)
VerificationDeadline
.
set_deadline
(
self
.
id
,
self
.
verification_deadline
)
for
mode
in
self
.
modes
:
mode
.
course_id
=
self
.
id
mode
.
mode_display_name
=
self
.
get_mode_display_name
(
mode
)
...
...
@@ -53,6 +72,8 @@ class Course(object):
def
update
(
self
,
attrs
):
""" Update the model with external data (usually passed via API call). """
self
.
verification_deadline
=
attrs
.
get
(
'verification_deadline'
)
existing_modes
=
{
mode
.
mode_slug
:
mode
for
mode
in
self
.
modes
}
merged_modes
=
set
()
merged_mode_keys
=
set
()
...
...
@@ -87,7 +108,8 @@ class Course(object):
course_modes
=
CourseMode
.
objects
.
filter
(
course_id
=
course_id
)
if
course_modes
:
return
cls
(
unicode
(
course_id
),
list
(
course_modes
))
verification_deadline
=
VerificationDeadline
.
deadline_for_course
(
course_id
)
return
cls
(
course_id
,
list
(
course_modes
),
verification_deadline
=
verification_deadline
)
return
None
...
...
lms/djangoapps/commerce/api/v1/serializers.py
View file @
09405a75
""" API v1 serializers. """
from
datetime
import
datetime
import
pytz
from
rest_framework
import
serializers
from
commerce.api.v1.models
import
Course
...
...
@@ -25,11 +28,36 @@ class CourseModeSerializer(serializers.ModelSerializer):
class
CourseSerializer
(
serializers
.
Serializer
):
""" Course serializer. """
id
=
serializers
.
CharField
()
# pylint: disable=invalid-name
name
=
serializers
.
CharField
(
read_only
=
True
)
verification_deadline
=
serializers
.
DateTimeField
(
blank
=
True
)
modes
=
CourseModeSerializer
(
many
=
True
,
allow_add_remove
=
True
)
def
validate
(
self
,
attrs
):
""" Ensure the verification deadline occurs AFTER the course mode enrollment deadlines. """
verification_deadline
=
attrs
.
get
(
'verification_deadline'
,
None
)
if
verification_deadline
:
upgrade_deadline
=
None
# Find the earliest upgrade deadline
for
mode
in
attrs
[
'modes'
]:
expires
=
mode
.
expiration_datetime
if
expires
:
# If we don't already have an upgrade_deadline value, use datetime.max so that we can actually
# complete the comparison.
upgrade_deadline
=
min
(
expires
,
upgrade_deadline
or
datetime
.
max
.
replace
(
tzinfo
=
pytz
.
utc
))
# In cases where upgrade_deadline is None (e.g. the verified professional mode), allow a verification
# deadline to be set anyway.
if
upgrade_deadline
is
not
None
and
verification_deadline
<
upgrade_deadline
:
raise
serializers
.
ValidationError
(
'Verification deadline must be after the course mode upgrade deadlines.'
)
return
attrs
def
restore_object
(
self
,
attrs
,
instance
=
None
):
if
instance
is
None
:
return
Course
(
attrs
[
'id'
],
attrs
[
'modes'
])
return
Course
(
attrs
[
'id'
],
attrs
[
'modes'
]
,
attrs
[
'verification_deadline'
]
)
instance
.
update
(
attrs
)
return
instance
lms/djangoapps/commerce/api/v1/tests/test_views.py
View file @
09405a75
...
...
@@ -8,12 +8,14 @@ from django.conf import settings
from
django.contrib.auth.models
import
Permission
from
django.core.urlresolvers
import
reverse
from
django.test.utils
import
override_settings
import
pytz
from
rest_framework.utils.encoders
import
JSONEncoder
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
course_modes.models
import
CourseMode
from
student.tests.factories
import
UserFactory
from
verify_student.models
import
VerificationDeadline
PASSWORD
=
'test'
JSON_CONTENT_TYPE
=
'application/json'
...
...
@@ -31,20 +33,38 @@ class CourseApiViewTestMixin(object):
self
.
course_mode
=
CourseMode
.
objects
.
create
(
course_id
=
self
.
course
.
id
,
mode_slug
=
u'verified'
,
min_price
=
100
,
currency
=
u'USD'
,
sku
=
u'ABC123'
)
@staticmethod
def
_serialize_course_mode
(
course_mode
):
@classmethod
def
_serialize_datetime
(
cls
,
dt
):
# pylint: disable=invalid-name
""" Serializes datetime values using Django REST Framework's encoder.
Use this to simplify equality assertions.
"""
if
dt
:
return
JSONEncoder
()
.
default
(
dt
)
return
None
@classmethod
def
_serialize_course_mode
(
cls
,
course_mode
):
""" Serialize a CourseMode to a dict. """
# encode the datetime (if nonempty) using DRF's encoder, simplifying
# equality assertions.
expires
=
course_mode
.
expiration_datetime
if
expires
is
not
None
:
expires
=
JSONEncoder
()
.
default
(
expires
)
return
{
u'name'
:
course_mode
.
mode_slug
,
u'currency'
:
course_mode
.
currency
.
lower
(),
u'price'
:
course_mode
.
min_price
,
u'sku'
:
course_mode
.
sku
,
u'expires'
:
expires
,
u'expires'
:
cls
.
_serialize_datetime
(
course_mode
.
expiration_datetime
),
}
@classmethod
def
_serialize_course
(
cls
,
course
,
modes
=
None
,
verification_deadline
=
None
):
""" Serializes a course to a Python dict. """
modes
=
modes
or
[]
verification_deadline
=
verification_deadline
or
VerificationDeadline
.
deadline_for_course
(
course
.
id
)
return
{
u'id'
:
unicode
(
course
.
id
),
u'name'
:
unicode
(
course
.
display_name
),
u'verification_deadline'
:
cls
.
_serialize_datetime
(
verification_deadline
),
u'modes'
:
[
cls
.
_serialize_course_mode
(
mode
)
for
mode
in
modes
]
}
...
...
@@ -66,12 +86,7 @@ class CourseListViewTests(CourseApiViewTestMixin, ModuleStoreTestCase):
self
.
assertEqual
(
response
.
status_code
,
200
)
actual
=
json
.
loads
(
response
.
content
)
expected
=
[
{
u'id'
:
unicode
(
self
.
course
.
id
),
u'modes'
:
[
self
.
_serialize_course_mode
(
self
.
course_mode
)]
}
]
expected
=
[
self
.
_serialize_course
(
self
.
course
,
[
self
.
course_mode
])]
self
.
assertListEqual
(
actual
,
expected
)
...
...
@@ -85,6 +100,9 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
self
.
user
=
UserFactory
.
create
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
PASSWORD
)
permission
=
Permission
.
objects
.
get
(
name
=
'Can change course mode'
)
self
.
user
.
user_permissions
.
add
(
permission
)
@ddt.data
(
'get'
,
'post'
,
'put'
)
def
test_authentication_required
(
self
,
method
):
""" Verify only authenticated users can access the view. """
...
...
@@ -94,6 +112,7 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
@ddt.data
(
'post'
,
'put'
)
def
test_authorization_required
(
self
,
method
):
self
.
user
.
user_permissions
.
clear
()
""" Verify create/edit operations require appropriate permissions. """
response
=
getattr
(
self
.
client
,
method
)(
self
.
path
,
content_type
=
JSON_CONTENT_TYPE
)
self
.
assertEqual
(
response
.
status_code
,
403
)
...
...
@@ -104,10 +123,7 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
self
.
assertEqual
(
response
.
status_code
,
200
)
actual
=
json
.
loads
(
response
.
content
)
expected
=
{
u'id'
:
unicode
(
self
.
course
.
id
),
u'modes'
:
[
self
.
_serialize_course_mode
(
self
.
course_mode
)]
}
expected
=
self
.
_serialize_course
(
self
.
course
,
[
self
.
course_mode
])
self
.
assertEqual
(
actual
,
expected
)
def
test_retrieve_invalid_course
(
self
):
...
...
@@ -116,40 +132,75 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
response
=
self
.
client
.
get
(
path
,
content_type
=
JSON_CONTENT_TYPE
)
self
.
assertEqual
(
response
.
status_code
,
404
)
def
test_update
(
self
):
""" Verify the view supports updating a course. """
permission
=
Permission
.
objects
.
get
(
name
=
'Can change course mode'
)
self
.
user
.
user_permissions
.
add
(
permission
)
expiration_datetime
=
datetime
.
now
()
def
_get_update_response_and_expected_data
(
self
,
mode_expiration
,
verification_deadline
):
""" Returns expected data and response for course update. """
expected_course_mode
=
CourseMode
(
mode_slug
=
u'verified'
,
min_price
=
200
,
currency
=
u'USD'
,
sku
=
u'ABC123'
,
expiration_datetime
=
expiration_datetime
expiration_datetime
=
mode_expiration
)
expected
=
{
u'id'
:
unicode
(
self
.
course
.
id
),
u'modes'
:
[
self
.
_serialize_course_mode
(
expected_course_mode
)]
}
expected
=
self
.
_serialize_course
(
self
.
course
,
[
expected_course_mode
],
verification_deadline
)
# Sanity check: The API should return HTTP status 200 for updates
response
=
self
.
client
.
put
(
self
.
path
,
json
.
dumps
(
expected
),
content_type
=
JSON_CONTENT_TYPE
)
return
response
,
expected
def
test_update
(
self
):
""" Verify the view supports updating a course. """
# Sanity check: Ensure no verification deadline is set
self
.
assertIsNone
(
VerificationDeadline
.
deadline_for_course
(
self
.
course
.
id
))
# Generate the expected data
verification_deadline
=
datetime
(
year
=
2020
,
month
=
12
,
day
=
31
,
tzinfo
=
pytz
.
utc
)
expiration_datetime
=
datetime
.
now
(
pytz
.
utc
)
response
,
expected
=
self
.
_get_update_response_and_expected_data
(
expiration_datetime
,
verification_deadline
)
# Sanity check: The API should return HTTP status 200 for updates
self
.
assertEqual
(
response
.
status_code
,
200
)
# Verify the course and modes are returned as JSON
actual
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
actual
,
expected
)
# Verify the verification deadline is updated
self
.
assertEqual
(
VerificationDeadline
.
deadline_for_course
(
self
.
course
.
id
),
verification_deadline
)
def
test_update_invalid_dates
(
self
):
"""
Verify the API does not allow the verification deadline to be set before the course mode upgrade deadlines.
"""
expiration_datetime
=
datetime
.
now
(
pytz
.
utc
)
verification_deadline
=
datetime
(
year
=
1915
,
month
=
5
,
day
=
7
,
tzinfo
=
pytz
.
utc
)
response
,
__
=
self
.
_get_update_response_and_expected_data
(
expiration_datetime
,
verification_deadline
)
self
.
assertEqual
(
response
.
status_code
,
400
)
# Verify the error message is correct
actual
=
json
.
loads
(
response
.
content
)
expected
=
{
'non_field_errors'
:
[
'Verification deadline must be after the course mode upgrade deadlines.'
]
}
self
.
assertEqual
(
actual
,
expected
)
def
test_update_verification_deadline_without_expiring_modes
(
self
):
""" Verify verification deadline can be set if no course modes expire.
This accounts for the verified professional mode, which requires verification but should never expire.
"""
verification_deadline
=
datetime
(
year
=
1915
,
month
=
5
,
day
=
7
,
tzinfo
=
pytz
.
utc
)
response
,
__
=
self
.
_get_update_response_and_expected_data
(
None
,
verification_deadline
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
VerificationDeadline
.
deadline_for_course
(
self
.
course
.
id
),
verification_deadline
)
def
test_update_overwrite
(
self
):
""" Verify that data submitted via PUT overwrites/deletes modes that are
not included in the body of the request. """
permission
=
Permission
.
objects
.
get
(
name
=
'Can change course mode'
)
self
.
user
.
user_permissions
.
add
(
permission
)
course_id
=
unicode
(
self
.
course
.
id
)
expected
=
{
u'id'
:
course_id
,
u'modes'
:
[
self
.
_serialize_course_mode
(
CourseMode
(
mode_slug
=
u'credit'
,
min_price
=
500
,
currency
=
u'USD'
,
sku
=
u'ABC123'
)),
]
}
expected_course_mode
=
CourseMode
(
mode_slug
=
u'credit'
,
min_price
=
500
,
currency
=
u'USD'
,
sku
=
u'ABC123'
)
expected
=
self
.
_serialize_course
(
self
.
course
,
[
expected_course_mode
])
path
=
reverse
(
'commerce_api:v1:courses:retrieve_update'
,
args
=
[
course_id
])
response
=
self
.
client
.
put
(
path
,
json
.
dumps
(
expected
),
content_type
=
JSON_CONTENT_TYPE
)
self
.
assertEqual
(
response
.
status_code
,
200
)
...
...
@@ -167,9 +218,6 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
def
test_update_professional_expiration
(
self
,
mode_slug
,
expiration_datetime
):
""" Verify that pushing a mode with a professional certificate and an expiration datetime
will be rejected (this is not allowed). """
permission
=
Permission
.
objects
.
get
(
name
=
'Can change course mode'
)
self
.
user
.
user_permissions
.
add
(
permission
)
mode
=
self
.
_serialize_course_mode
(
CourseMode
(
mode_slug
=
mode_slug
,
...
...
@@ -190,19 +238,14 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
def
assert_can_create_course
(
self
,
**
request_kwargs
):
""" Verify a course can be created by the view. """
course
=
CourseFactory
.
create
()
course_id
=
unicode
(
course
.
id
)
expected
=
{
u'id'
:
course_id
,
u'modes'
:
[
self
.
_serialize_course_mode
(
CourseMode
(
mode_slug
=
u'verified'
,
min_price
=
150
,
currency
=
u'USD'
,
sku
=
u'ABC123'
)),
self
.
_serialize_course_mode
(
CourseMode
(
mode_slug
=
u'honor'
,
min_price
=
0
,
currency
=
u'USD'
,
sku
=
u'DEADBEEF'
)),
]
}
path
=
reverse
(
'commerce_api:v1:courses:retrieve_update'
,
args
=
[
course_id
])
expected_modes
=
[
CourseMode
(
mode_slug
=
u'verified'
,
min_price
=
150
,
currency
=
u'USD'
,
sku
=
u'ABC123'
),
CourseMode
(
mode_slug
=
u'honor'
,
min_price
=
0
,
currency
=
u'USD'
,
sku
=
u'DEADBEEF'
)]
expected
=
self
.
_serialize_course
(
course
,
expected_modes
)
path
=
reverse
(
'commerce_api:v1:courses:retrieve_update'
,
args
=
[
unicode
(
course
.
id
)])
response
=
self
.
client
.
put
(
path
,
json
.
dumps
(
expected
),
content_type
=
JSON_CONTENT_TYPE
,
**
request_kwargs
)
self
.
assertEqual
(
response
.
status_code
,
201
)
actual
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
actual
,
expected
)
...
...
lms/djangoapps/commerce/api/v1/views.py
View file @
09405a75
...
...
@@ -41,3 +41,8 @@ class CourseRetrieveUpdateView(RetrieveUpdateAPIView):
return
course
raise
Http404
def
pre_save
(
self
,
obj
):
# There is nothing to pre-save. The default behavior changes the Course.id attribute from
# a CourseKey to a string, which is not desired.
pass
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