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
28ffaa5a
Commit
28ffaa5a
authored
Aug 05, 2015
by
Renzo Lucioni
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #251 from edx/renzo/audit-migration
Support migration of legacy audit modes
parents
02be1db8
473e615d
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
107 additions
and
58 deletions
+107
-58
ecommerce/courses/models.py
+34
-15
ecommerce/courses/publishers.py
+1
-1
ecommerce/courses/tests/test_models.py
+2
-2
ecommerce/courses/tests/test_publishers.py
+1
-1
ecommerce/courses/tests/test_utils.py
+1
-0
ecommerce/courses/utils.py
+3
-1
ecommerce/credit/views.py
+1
-1
ecommerce/extensions/catalogue/management/commands/migrate_course.py
+8
-3
ecommerce/extensions/catalogue/tests/test_migrate_course.py
+26
-19
ecommerce/extensions/catalogue/utils.py
+6
-3
ecommerce/extensions/checkout/signals.py
+2
-1
ecommerce/extensions/fulfillment/modules.py
+3
-3
ecommerce/extensions/fulfillment/tests/test_modules.py
+1
-1
ecommerce/extensions/payment/processors/cybersource.py
+1
-1
ecommerce/extensions/refund/signals.py
+2
-1
ecommerce/extensions/refund/tests/factories.py
+12
-3
ecommerce/tests/mixins.py
+3
-2
No files found.
ecommerce/courses/models.py
View file @
28ffaa5a
...
@@ -72,13 +72,16 @@ class Course(models.Model):
...
@@ -72,13 +72,16 @@ class Course(models.Model):
if
mode
==
'no-id-professional'
:
if
mode
==
'no-id-professional'
:
return
'professional'
return
'professional'
elif
mode
==
'audit'
:
# Historically, users enrolled in an 'audit' mode have not received a certificate.
return
''
return
mode
return
mode
@property
@property
def
type
(
self
):
def
type
(
self
):
""" Returns the type of the course (based on the available seat types). """
""" Returns the type of the course (based on the available seat types). """
seat_types
=
[
(
seat
.
attr
.
certificate_type
or
''
)
.
lower
()
for
seat
in
self
.
seat_products
]
seat_types
=
[
getattr
(
seat
.
attr
,
'certificate_type'
,
''
)
.
lower
()
for
seat
in
self
.
seat_products
]
if
'credit'
in
seat_types
:
if
'credit'
in
seat_types
:
return
'credit'
return
'credit'
elif
'professional'
in
seat_types
or
'no-id-professional'
in
seat_types
:
elif
'professional'
in
seat_types
or
'no-id-professional'
in
seat_types
:
...
@@ -100,12 +103,13 @@ class Course(models.Model):
...
@@ -100,12 +103,13 @@ class Course(models.Model):
def
_get_course_seat_name
(
self
,
certificate_type
,
id_verification_required
):
def
_get_course_seat_name
(
self
,
certificate_type
,
id_verification_required
):
""" Returns the name for a course seat. """
""" Returns the name for a course seat. """
name
=
'Seat in {course_name} with {certificate_type} certificate'
.
format
(
name
=
u'Seat in {}'
.
format
(
self
.
name
)
course_name
=
self
.
name
,
certificate_type
=
certificate_type
)
if
id_verification_required
:
if
certificate_type
!=
''
:
name
+=
' (and ID verification)'
name
+=
u' with {} certificate'
.
format
(
certificate_type
)
if
id_verification_required
:
name
+=
u' (and ID verification)'
return
name
return
name
...
@@ -117,7 +121,6 @@ class Course(models.Model):
...
@@ -117,7 +121,6 @@ class Course(models.Model):
Returns:
Returns:
Product: The seat that has been created or updated.
Product: The seat that has been created or updated.
"""
"""
certificate_type
=
certificate_type
.
lower
()
certificate_type
=
certificate_type
.
lower
()
course_id
=
unicode
(
self
.
id
)
course_id
=
unicode
(
self
.
id
)
...
@@ -136,12 +139,18 @@ class Course(models.Model):
...
@@ -136,12 +139,18 @@ class Course(models.Model):
try
:
try
:
seat
=
Product
.
objects
.
get
(
slug__in
=
slugs
)
seat
=
Product
.
objects
.
get
(
slug__in
=
slugs
)
logger
.
info
(
'Retrieved [
%
s] course seat child product for [
%
s] from database.'
,
certificate_type
,
logger
.
info
(
course_id
)
'Retrieved course seat child product with certificate type [
%
s] for [
%
s] from database.'
,
certificate_type
,
course_id
)
except
Product
.
DoesNotExist
:
except
Product
.
DoesNotExist
:
seat
=
Product
(
slug
=
slug
)
seat
=
Product
(
slug
=
slug
)
logger
.
info
(
'[
%
s] course seat product for [
%
s] does not exist. Instantiated a new instance.'
,
logger
.
info
(
certificate_type
,
course_id
)
'Course seat product with certificate type [
%
s] for [
%
s] does not exist. Instantiated a new instance.'
,
certificate_type
,
course_id
)
seat
.
course
=
self
seat
.
course
=
self
seat
.
parent
=
self
.
parent_seat_product
seat
.
parent
=
self
.
parent_seat_product
...
@@ -149,7 +158,11 @@ class Course(models.Model):
...
@@ -149,7 +158,11 @@ class Course(models.Model):
seat
.
structure
=
Product
.
CHILD
seat
.
structure
=
Product
.
CHILD
seat
.
title
=
self
.
_get_course_seat_name
(
certificate_type
,
id_verification_required
)
seat
.
title
=
self
.
_get_course_seat_name
(
certificate_type
,
id_verification_required
)
seat
.
expires
=
expires
seat
.
expires
=
expires
# If a ProductAttribute is saved with a value of None or the empty string, the ProductAttribute is deleted.
# As a consequence, Seats derived from a migrated "audit" mode do not have a certificate_type attribute.
seat
.
attr
.
certificate_type
=
certificate_type
seat
.
attr
.
certificate_type
=
certificate_type
seat
.
attr
.
course_key
=
course_id
seat
.
attr
.
course_key
=
course_id
seat
.
attr
.
id_verification_required
=
id_verification_required
seat
.
attr
.
id_verification_required
=
id_verification_required
...
@@ -165,14 +178,20 @@ class Course(models.Model):
...
@@ -165,14 +178,20 @@ class Course(models.Model):
partner
=
Partner
.
objects
.
get
(
code
=
'edx'
)
partner
=
Partner
.
objects
.
get
(
code
=
'edx'
)
try
:
try
:
stock_record
=
StockRecord
.
objects
.
get
(
product
=
seat
,
partner
=
partner
)
stock_record
=
StockRecord
.
objects
.
get
(
product
=
seat
,
partner
=
partner
)
logger
.
info
(
'Retrieved [
%
s] course seat child product stock record for [
%
s] from database.'
,
logger
.
info
(
certificate_type
,
course_id
)
'Retrieved course seat product stock record with certificate type [
%
s] for [
%
s] from database.'
,
certificate_type
,
course_id
)
except
StockRecord
.
DoesNotExist
:
except
StockRecord
.
DoesNotExist
:
partner_sku
=
generate_sku
(
seat
)
partner_sku
=
generate_sku
(
seat
)
stock_record
=
StockRecord
(
product
=
seat
,
partner
=
partner
,
partner_sku
=
partner_sku
)
stock_record
=
StockRecord
(
product
=
seat
,
partner
=
partner
,
partner_sku
=
partner_sku
)
logger
.
info
(
logger
.
info
(
'[
%
s] course seat product stock record for [
%
s] does not exist. Instantiated a new instance.'
,
'Course seat product stock record with certificate type [
%
s] for [
%
s] does not exist. '
certificate_type
,
course_id
)
'Instantiated a new instance.'
,
certificate_type
,
course_id
)
stock_record
.
price_excl_tax
=
price
stock_record
.
price_excl_tax
=
price
...
...
ecommerce/courses/publishers.py
View file @
28ffaa5a
...
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
...
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
class
LMSPublisher
(
object
):
class
LMSPublisher
(
object
):
def
get_seat_expiration
(
self
,
seat
):
def
get_seat_expiration
(
self
,
seat
):
if
not
seat
.
expires
or
'professional'
in
seat
.
attr
.
certificate_type
:
if
not
seat
.
expires
or
'professional'
in
getattr
(
seat
.
attr
,
'certificate_type'
,
''
)
:
return
None
return
None
return
seat
.
expires
.
isoformat
()
return
seat
.
expires
.
isoformat
()
...
...
ecommerce/courses/tests/test_models.py
View file @
28ffaa5a
...
@@ -59,7 +59,7 @@ class CourseTests(CourseCatalogTestMixin, TestCase):
...
@@ -59,7 +59,7 @@ class CourseTests(CourseCatalogTestMixin, TestCase):
(
'professional'
,
'professional'
),
(
'professional'
,
'professional'
),
(
'honor'
,
'honor'
),
(
'honor'
,
'honor'
),
(
'no-id-professional'
,
'professional'
),
(
'no-id-professional'
,
'professional'
),
(
'audit'
,
'
audit
'
),
(
'audit'
,
''
),
(
'unknown'
,
'unknown'
),
(
'unknown'
,
'unknown'
),
)
)
@ddt.unpack
@ddt.unpack
...
@@ -92,7 +92,7 @@ class CourseTests(CourseCatalogTestMixin, TestCase):
...
@@ -92,7 +92,7 @@ class CourseTests(CourseCatalogTestMixin, TestCase):
# pylint: disable=protected-access
# pylint: disable=protected-access
self
.
assertEqual
(
seat
.
title
,
course
.
_get_course_seat_name
(
certificate_type
,
id_verification_required
))
self
.
assertEqual
(
seat
.
title
,
course
.
_get_course_seat_name
(
certificate_type
,
id_verification_required
))
self
.
assertEqual
(
seat
.
get_product_class
(),
self
.
seat_product_class
)
self
.
assertEqual
(
seat
.
get_product_class
(),
self
.
seat_product_class
)
self
.
assertEqual
(
seat
.
attr
.
certificate_type
,
certificate_type
)
self
.
assertEqual
(
getattr
(
seat
.
attr
,
'certificate_type'
,
''
)
,
certificate_type
)
self
.
assertEqual
(
seat
.
attr
.
course_key
,
course
.
id
)
self
.
assertEqual
(
seat
.
attr
.
course_key
,
course
.
id
)
self
.
assertEqual
(
seat
.
attr
.
id_verification_required
,
id_verification_required
)
self
.
assertEqual
(
seat
.
attr
.
id_verification_required
,
id_verification_required
)
self
.
assertEqual
(
seat
.
stockrecords
.
first
()
.
price_excl_tax
,
price
)
self
.
assertEqual
(
seat
.
stockrecords
.
first
()
.
price_excl_tax
,
price
)
...
...
ecommerce/courses/tests/test_publishers.py
View file @
28ffaa5a
...
@@ -98,7 +98,7 @@ class LMSPublisherTests(CourseCatalogTestMixin, TestCase):
...
@@ -98,7 +98,7 @@ class LMSPublisherTests(CourseCatalogTestMixin, TestCase):
def
test_serialize_seat_for_commerce_api
(
self
):
def
test_serialize_seat_for_commerce_api
(
self
):
""" The method should convert a seat to a JSON-serializable dict consumable by the Commerce API. """
""" The method should convert a seat to a JSON-serializable dict consumable by the Commerce API. """
# Grab the verified seat
# Grab the verified seat
seat
=
sorted
(
self
.
course
.
seat_products
,
key
=
lambda
p
:
p
.
attr
.
certificate_type
)[
1
]
seat
=
sorted
(
self
.
course
.
seat_products
,
key
=
lambda
p
:
getattr
(
p
.
attr
,
'certificate_type'
,
''
)
)[
1
]
stock_record
=
seat
.
stockrecords
.
first
()
stock_record
=
seat
.
stockrecords
.
first
()
actual
=
self
.
publisher
.
serialize_seat_for_commerce_api
(
seat
)
actual
=
self
.
publisher
.
serialize_seat_for_commerce_api
(
seat
)
...
...
ecommerce/courses/tests/test_utils.py
View file @
28ffaa5a
...
@@ -11,6 +11,7 @@ from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
...
@@ -11,6 +11,7 @@ from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
class
UtilsTests
(
CourseCatalogTestMixin
,
TestCase
):
class
UtilsTests
(
CourseCatalogTestMixin
,
TestCase
):
@ddt.unpack
@ddt.unpack
@ddt.data
(
@ddt.data
(
(
''
,
False
,
'audit'
),
(
'honor'
,
True
,
'honor'
),
(
'honor'
,
True
,
'honor'
),
(
'honor'
,
False
,
'honor'
),
(
'honor'
,
False
,
'honor'
),
(
'verified'
,
True
,
'verified'
),
(
'verified'
,
True
,
'verified'
),
...
...
ecommerce/courses/utils.py
View file @
28ffaa5a
def
mode_for_seat
(
seat
):
def
mode_for_seat
(
seat
):
""" Returns the Enrollment mode for a given seat product. """
""" Returns the Enrollment mode for a given seat product. """
certificate_type
=
seat
.
attr
.
certificate_type
certificate_type
=
getattr
(
seat
.
attr
,
'certificate_type'
,
''
)
if
certificate_type
==
'professional'
and
not
seat
.
attr
.
id_verification_required
:
if
certificate_type
==
'professional'
and
not
seat
.
attr
.
id_verification_required
:
return
'no-id-professional'
return
'no-id-professional'
elif
certificate_type
==
''
:
return
'audit'
return
certificate_type
return
certificate_type
ecommerce/credit/views.py
View file @
28ffaa5a
...
@@ -39,7 +39,7 @@ class Checkout(TemplateView):
...
@@ -39,7 +39,7 @@ class Checkout(TemplateView):
processors_dict
[
processor
]
=
'Checkout with {}'
.
format
(
processor
)
processors_dict
[
processor
]
=
'Checkout with {}'
.
format
(
processor
)
credit_seats
=
[
credit_seats
=
[
seat
for
seat
in
course
.
seat_products
if
seat
.
attr
.
certificate_type
==
self
.
CREDIT_MODE
seat
for
seat
in
course
.
seat_products
if
getattr
(
seat
.
attr
,
'certificate_type'
,
''
)
==
self
.
CREDIT_MODE
]
]
provider_ids
=
None
provider_ids
=
None
if
credit_seats
:
if
credit_seats
:
...
...
ecommerce/extensions/catalogue/management/commands/migrate_course.py
View file @
28ffaa5a
...
@@ -138,9 +138,14 @@ class Command(BaseCommand):
...
@@ -138,9 +138,14 @@ class Command(BaseCommand):
for
seat
in
course
.
seat_products
:
for
seat
in
course
.
seat_products
:
stock_record
=
seat
.
stockrecords
.
first
()
stock_record
=
seat
.
stockrecords
.
first
()
data
=
(
seat
.
attr
.
certificate_type
,
seat
.
attr
.
id_verification_required
,
data
=
(
'{0} {1}'
.
format
(
stock_record
.
price_currency
,
stock_record
.
price_excl_tax
),
getattr
(
seat
.
attr
,
'certificate_type'
,
''
),
stock_record
.
partner_sku
,
seat
.
slug
,
seat
.
expires
)
seat
.
attr
.
id_verification_required
,
'{0} {1}'
.
format
(
stock_record
.
price_currency
,
stock_record
.
price_excl_tax
),
stock_record
.
partner_sku
,
seat
.
slug
,
seat
.
expires
)
msg
+=
'
\t
{}
\n
'
.
format
(
data
)
msg
+=
'
\t
{}
\n
'
.
format
(
data
)
logger
.
info
(
msg
)
logger
.
info
(
msg
)
...
...
ecommerce/extensions/catalogue/tests/test_migrate_course.py
View file @
28ffaa5a
...
@@ -19,6 +19,7 @@ from waffle import Switch
...
@@ -19,6 +19,7 @@ from waffle import Switch
from
ecommerce.core.constants
import
ISO_8601_FORMAT
from
ecommerce.core.constants
import
ISO_8601_FORMAT
from
ecommerce.courses.models
import
Course
from
ecommerce.courses.models
import
Course
from
ecommerce.courses.publishers
import
LMSPublisher
from
ecommerce.courses.publishers
import
LMSPublisher
from
ecommerce.courses.utils
import
mode_for_seat
from
ecommerce.extensions.catalogue.management.commands.migrate_course
import
MigratedCourse
from
ecommerce.extensions.catalogue.management.commands.migrate_course
import
MigratedCourse
from
ecommerce.extensions.catalogue.tests.mixins
import
CourseCatalogTestMixin
from
ecommerce.extensions.catalogue.tests.mixins
import
CourseCatalogTestMixin
from
ecommerce.extensions.catalogue.utils
import
generate_sku
from
ecommerce.extensions.catalogue.utils
import
generate_sku
...
@@ -44,7 +45,9 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin):
...
@@ -44,7 +45,9 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin):
'honor'
:
0
,
'honor'
:
0
,
'verified'
:
10
,
'verified'
:
10
,
'no-id-professional'
:
100
,
'no-id-professional'
:
100
,
'professional'
:
1000
'professional'
:
1000
,
'audit'
:
0
,
'credit'
:
0
,
}
}
def
_mock_lms_api
(
self
):
def
_mock_lms_api
(
self
):
...
@@ -60,8 +63,8 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin):
...
@@ -60,8 +63,8 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin):
# Mock Enrollment API
# Mock Enrollment API
body
=
{
body
=
{
'course_id'
:
self
.
course_id
,
'course_id'
:
self
.
course_id
,
'course_modes'
:
[{
'slug'
:
seat_typ
e
,
'min_price'
:
price
,
'expiration_datetime'
:
EXPIRES_STRING
}
for
'course_modes'
:
[{
'slug'
:
mod
e
,
'min_price'
:
price
,
'expiration_datetime'
:
EXPIRES_STRING
}
for
seat_typ
e
,
price
in
self
.
prices
.
iteritems
()]
mod
e
,
price
in
self
.
prices
.
iteritems
()]
}
}
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
enrollment_api_url
,
body
=
json
.
dumps
(
body
),
content_type
=
JSON
)
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
enrollment_api_url
,
body
=
json
.
dumps
(
body
),
content_type
=
JSON
)
...
@@ -76,12 +79,15 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin):
...
@@ -76,12 +79,15 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin):
""" Verify the given seat is configured correctly. """
""" Verify the given seat is configured correctly. """
certificate_type
=
Course
.
certificate_type_for_mode
(
mode
)
certificate_type
=
Course
.
certificate_type_for_mode
(
mode
)
expected_title
=
'Seat in A Tést Côurse with {} certificate'
.
format
(
certificate_type
)
expected_title
=
'Seat in A Tést Côurse'
if
seat
.
attr
.
id_verification_required
:
if
certificate_type
!=
''
:
expected_title
+=
u' (and ID verification)'
expected_title
+=
' with {} certificate'
.
format
(
certificate_type
)
if
seat
.
attr
.
id_verification_required
:
expected_title
+=
u' (and ID verification)'
self
.
assertEqual
(
seat
.
title
,
expected_title
)
self
.
assertEqual
(
seat
.
title
,
expected_title
)
self
.
assertEqual
(
seat
.
attr
.
certificate_type
,
certificate_type
)
self
.
assertEqual
(
getattr
(
seat
.
attr
,
'certificate_type'
,
''
)
,
certificate_type
)
self
.
assertEqual
(
seat
.
expires
,
EXPIRES
)
self
.
assertEqual
(
seat
.
expires
,
EXPIRES
)
self
.
assertEqual
(
seat
.
attr
.
course_key
,
self
.
course_id
)
self
.
assertEqual
(
seat
.
attr
.
course_key
,
self
.
course_id
)
self
.
assertEqual
(
seat
.
attr
.
id_verification_required
,
Course
.
is_mode_verified
(
mode
))
self
.
assertEqual
(
seat
.
attr
.
id_verification_required
,
Course
.
is_mode_verified
(
mode
))
...
@@ -90,18 +96,20 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin):
...
@@ -90,18 +96,20 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin):
""" Verify the course was migrated and saved to the database. """
""" Verify the course was migrated and saved to the database. """
course
=
Course
.
objects
.
get
(
id
=
self
.
course_id
)
course
=
Course
.
objects
.
get
(
id
=
self
.
course_id
)
seats
=
course
.
seat_products
seats
=
course
.
seat_products
self
.
assertEqual
(
len
(
seats
),
4
)
# Verify that all modes are migrated.
self
.
assertEqual
(
len
(
seats
),
len
(
self
.
prices
))
parent
=
course
.
products
.
get
(
structure
=
Product
.
PARENT
)
parent
=
course
.
products
.
get
(
structure
=
Product
.
PARENT
)
self
.
assertEqual
(
list
(
parent
.
categories
.
all
()),
[
self
.
category
])
self
.
assertEqual
(
list
(
parent
.
categories
.
all
()),
[
self
.
category
])
for
seat
in
seats
:
for
seat
in
seats
:
seat_type
=
seat
.
attr
.
certificate_type
mode
=
mode_for_seat
(
seat
)
if
seat_type
==
'professional'
and
not
seat
.
attr
.
id_verification_required
:
logger
.
info
(
'Validating objects for [
%
s] mode...'
,
mode
)
seat_type
=
'no-id-professional'
logger
.
info
(
'Validating objects for
%
s certificate type...'
,
seat_type
)
stock_record
=
self
.
partner
.
stockrecords
.
get
(
product
=
seat
)
stock_record
=
self
.
partner
.
stockrecords
.
get
(
product
=
seat
)
self
.
assert_seat_valid
(
seat
,
seat_typ
e
)
self
.
assert_seat_valid
(
seat
,
mod
e
)
self
.
assert_stock_record_valid
(
stock_record
,
seat
,
self
.
prices
[
seat_typ
e
])
self
.
assert_stock_record_valid
(
stock_record
,
seat
,
self
.
prices
[
mod
e
])
def
assert_lms_api_headers
(
self
,
request
):
def
assert_lms_api_headers
(
self
,
request
):
self
.
assertEqual
(
request
.
headers
[
'Accept'
],
JSON
)
self
.
assertEqual
(
request
.
headers
[
'Accept'
],
JSON
)
...
@@ -143,11 +151,10 @@ class MigratedCourseTests(CourseMigrationTestMixin, TestCase):
...
@@ -143,11 +151,10 @@ class MigratedCourseTests(CourseMigrationTestMixin, TestCase):
self
.
assertEqual
(
course
.
verification_deadline
,
EXPIRES
)
self
.
assertEqual
(
course
.
verification_deadline
,
EXPIRES
)
for
seat
in
course
.
seat_products
:
for
seat
in
course
.
seat_products
:
certificate_type
=
seat
.
attr
.
certificate_type
mode
=
mode_for_seat
(
seat
)
if
certificate_type
==
'professional'
and
not
seat
.
attr
.
id_verification_required
:
logger
.
info
(
'Validating objects for [
%
s] mode...'
,
mode
)
certificate_type
=
'no-id-professional'
logger
.
info
(
'Validating objects for
%
s certificate type...'
,
certificate_type
)
self
.
assert_stock_record_valid
(
seat
.
stockrecords
.
first
(),
seat
,
Decimal
(
self
.
prices
[
mode
]))
self
.
assert_stock_record_valid
(
seat
.
stockrecords
.
first
(),
seat
,
Decimal
(
self
.
prices
[
certificate_type
]))
@httpretty.activate
@httpretty.activate
def
test_course_name_missing
(
self
):
def
test_course_name_missing
(
self
):
...
...
ecommerce/extensions/catalogue/utils.py
View file @
28ffaa5a
...
@@ -9,9 +9,12 @@ def generate_sku(product):
...
@@ -9,9 +9,12 @@ def generate_sku(product):
"""
"""
# Note: This currently supports seats. In the future, this should
# Note: This currently supports seats. In the future, this should
# be updated to accommodate other product classes.
# be updated to accommodate other product classes.
_hash
=
u' '
.
join
((
product
.
attr
.
certificate_type
.
lower
(),
_hash
=
u' '
.
join
((
product
.
attr
.
course_key
.
lower
(),
getattr
(
product
.
attr
,
'certificate_type'
,
''
)
.
lower
(),
unicode
(
product
.
attr
.
id_verification_required
)))
product
.
attr
.
course_key
.
lower
(),
unicode
(
product
.
attr
.
id_verification_required
)
))
_hash
=
md5
(
_hash
)
_hash
=
md5
(
_hash
)
_hash
=
_hash
.
hexdigest
()[
-
7
:]
_hash
=
_hash
.
hexdigest
()[
-
7
:]
return
_hash
.
upper
()
return
_hash
.
upper
()
ecommerce/extensions/checkout/signals.py
View file @
28ffaa5a
...
@@ -3,6 +3,7 @@ import waffle
...
@@ -3,6 +3,7 @@ import waffle
from
django.dispatch
import
receiver
from
django.dispatch
import
receiver
from
oscar.core.loading
import
get_class
from
oscar.core.loading
import
get_class
from
ecommerce.courses.utils
import
mode_for_seat
from
ecommerce.extensions.analytics.utils
import
is_segment_configured
,
parse_tracking_context
,
log_exceptions
from
ecommerce.extensions.analytics.utils
import
is_segment_configured
,
parse_tracking_context
,
log_exceptions
from
ecommerce.notifications.notifications
import
send_notification
from
ecommerce.notifications.notifications
import
send_notification
...
@@ -33,7 +34,7 @@ def track_completed_order(sender, order=None, **kwargs): # pylint: disable=unus
...
@@ -33,7 +34,7 @@ def track_completed_order(sender, order=None, **kwargs): # pylint: disable=unus
# SKU. Marketing is aware that this approach will not scale once we start selling
# SKU. Marketing is aware that this approach will not scale once we start selling
# products other than courses, and will need to change in the future.
# products other than courses, and will need to change in the future.
'id'
:
line
.
partner_sku
,
'id'
:
line
.
partner_sku
,
'sku'
:
line
.
product
.
attr
.
certificate_type
,
'sku'
:
mode_for_seat
(
line
.
product
)
,
'name'
:
line
.
product
.
title
,
'name'
:
line
.
product
.
title
,
'price'
:
str
(
line
.
line_price_excl_tax
),
'price'
:
str
(
line
.
line_price_excl_tax
),
'quantity'
:
line
.
quantity
,
'quantity'
:
line
.
quantity
,
...
...
ecommerce/extensions/fulfillment/modules.py
View file @
28ffaa5a
...
@@ -228,12 +228,12 @@ class EnrollmentFulfillmentModule(BaseFulfillmentModule):
...
@@ -228,12 +228,12 @@ class EnrollmentFulfillmentModule(BaseFulfillmentModule):
try
:
try
:
logger
.
info
(
'Attempting to revoke fulfillment of Line [
%
d]...'
,
line
.
id
)
logger
.
info
(
'Attempting to revoke fulfillment of Line [
%
d]...'
,
line
.
id
)
certificate_type
=
line
.
product
.
attr
.
certificate_type
mode
=
mode_for_seat
(
line
.
product
)
course_key
=
line
.
product
.
attr
.
course_key
course_key
=
line
.
product
.
attr
.
course_key
data
=
{
data
=
{
'user'
:
line
.
order
.
user
.
username
,
'user'
:
line
.
order
.
user
.
username
,
'is_active'
:
False
,
'is_active'
:
False
,
'mode'
:
certificate_typ
e
,
'mode'
:
mod
e
,
'course_details'
:
{
'course_details'
:
{
'course_id'
:
course_key
,
'course_id'
:
course_key
,
},
},
...
@@ -248,7 +248,7 @@ class EnrollmentFulfillmentModule(BaseFulfillmentModule):
...
@@ -248,7 +248,7 @@ class EnrollmentFulfillmentModule(BaseFulfillmentModule):
order_number
=
line
.
order
.
number
,
order_number
=
line
.
order
.
number
,
product_class
=
line
.
product
.
get_product_class
()
.
name
,
product_class
=
line
.
product
.
get_product_class
()
.
name
,
course_id
=
course_key
,
course_id
=
course_key
,
certificate_type
=
certificate_type
,
certificate_type
=
getattr
(
line
.
product
.
attr
,
'certificate_type'
,
''
)
,
user_id
=
line
.
order
.
user
.
id
user_id
=
line
.
order
.
user
.
id
)
)
...
...
ecommerce/extensions/fulfillment/tests/test_modules.py
View file @
28ffaa5a
...
@@ -143,7 +143,7 @@ class EnrollmentFulfillmentModuleTests(EnrollmentFulfillmentTestMixin, TestCase)
...
@@ -143,7 +143,7 @@ class EnrollmentFulfillmentModuleTests(EnrollmentFulfillmentTestMixin, TestCase)
'INFO'
,
'INFO'
,
'line_revoked: certificate_type="{}", course_id="{}", order_line_id="{}", order_number="{}", '
'line_revoked: certificate_type="{}", course_id="{}", order_line_id="{}", order_number="{}", '
'product_class="{}", user_id="{}"'
.
format
(
'product_class="{}", user_id="{}"'
.
format
(
line
.
product
.
attr
.
certificate_type
,
getattr
(
line
.
product
.
attr
,
'certificate_type'
,
''
)
,
line
.
product
.
attr
.
course_key
,
line
.
product
.
attr
.
course_key
,
line
.
id
,
line
.
id
,
line
.
order
.
number
,
line
.
order
.
number
,
...
...
ecommerce/extensions/payment/processors/cybersource.py
View file @
28ffaa5a
...
@@ -98,7 +98,7 @@ class Cybersource(BasePaymentProcessor):
...
@@ -98,7 +98,7 @@ class Cybersource(BasePaymentProcessor):
single_seat
=
self
.
get_single_seat
(
basket
)
single_seat
=
self
.
get_single_seat
(
basket
)
if
single_seat
:
if
single_seat
:
parameters
[
u'merchant_defined_data1'
]
=
single_seat
.
attr
.
course_key
parameters
[
u'merchant_defined_data1'
]
=
single_seat
.
attr
.
course_key
parameters
[
u'merchant_defined_data2'
]
=
single_seat
.
attr
.
certificate_type
parameters
[
u'merchant_defined_data2'
]
=
getattr
(
single_seat
.
attr
,
'certificate_type'
,
''
)
# Sign all fields
# Sign all fields
signed_field_names
=
parameters
.
keys
()
signed_field_names
=
parameters
.
keys
()
...
...
ecommerce/extensions/refund/signals.py
View file @
28ffaa5a
import
analytics
import
analytics
from
django.dispatch
import
receiver
,
Signal
from
django.dispatch
import
receiver
,
Signal
from
ecommerce.courses.utils
import
mode_for_seat
from
ecommerce.extensions.analytics.utils
import
is_segment_configured
,
parse_tracking_context
,
log_exceptions
from
ecommerce.extensions.analytics.utils
import
is_segment_configured
,
parse_tracking_context
,
log_exceptions
...
@@ -34,7 +35,7 @@ def track_completed_refund(sender, refund=None, **kwargs): # pylint: disable=un
...
@@ -34,7 +35,7 @@ def track_completed_refund(sender, refund=None, **kwargs): # pylint: disable=un
# SKU. Marketing is aware that this approach will not scale once we start selling
# SKU. Marketing is aware that this approach will not scale once we start selling
# products other than courses, and will need to change in the future.
# products other than courses, and will need to change in the future.
'id'
:
line
.
order_line
.
partner_sku
,
'id'
:
line
.
order_line
.
partner_sku
,
'sku'
:
line
.
order_line
.
product
.
attr
.
certificate_type
,
'sku'
:
mode_for_seat
(
line
.
order_line
.
product
)
,
'name'
:
line
.
order_line
.
product
.
title
,
'name'
:
line
.
order_line
.
product
.
title
,
'price'
:
str
(
line
.
line_credit_excl_tax
),
'price'
:
str
(
line
.
line_credit_excl_tax
),
'quantity'
:
-
1
*
line
.
quantity
,
'quantity'
:
-
1
*
line
.
quantity
,
...
...
ecommerce/extensions/refund/tests/factories.py
View file @
28ffaa5a
...
@@ -7,8 +7,10 @@ from oscar.core.loading import get_model
...
@@ -7,8 +7,10 @@ from oscar.core.loading import get_model
from
oscar.test
import
factories
from
oscar.test
import
factories
from
oscar.test.newfactories
import
UserFactory
from
oscar.test.newfactories
import
UserFactory
from
ecommerce.courses.models
import
Course
from
ecommerce.extensions.refund.status
import
REFUND
,
REFUND_LINE
from
ecommerce.extensions.refund.status
import
REFUND
,
REFUND_LINE
Category
=
get_model
(
"catalogue"
,
"Category"
)
Category
=
get_model
(
"catalogue"
,
"Category"
)
Partner
=
get_model
(
'partner'
,
'Partner'
)
Partner
=
get_model
(
'partner'
,
'Partner'
)
Product
=
get_model
(
"catalogue"
,
"Product"
)
Product
=
get_model
(
"catalogue"
,
"Product"
)
...
@@ -87,14 +89,21 @@ class CourseFactory(object):
...
@@ -87,14 +89,21 @@ class CourseFactory(object):
def
add_mode
(
self
,
name
,
price
,
id_verification_required
=
False
):
def
add_mode
(
self
,
name
,
price
,
id_verification_required
=
False
):
parent_product
=
self
.
_get_parent_seat_product
()
parent_product
=
self
.
_get_parent_seat_product
()
title
=
u'{mode_name} Seat in {course_name}'
.
format
(
mode_name
=
name
,
course_name
=
self
.
course_name
)
certificate_type
=
Course
.
certificate_type_for_mode
(
name
)
slug
=
slugify
(
u'{course_name}-seat-{mode_name}'
.
format
(
course_name
=
self
.
course_name
,
mode_name
=
name
))
title
=
u'{certificate_type} Seat in {course_name}'
.
format
(
certificate_type
=
certificate_type
,
course_name
=
self
.
course_name
)
slug
=
slugify
(
u'{course_name}-seat-{certificate_type}'
.
format
(
course_name
=
self
.
course_name
,
certificate_type
=
certificate_type
))
child_product
,
created
=
Product
.
objects
.
get_or_create
(
parent
=
parent_product
,
title
=
title
,
slug
=
slug
,
child_product
,
created
=
Product
.
objects
.
get_or_create
(
parent
=
parent_product
,
title
=
title
,
slug
=
slug
,
structure
=
'child'
)
structure
=
'child'
)
if
created
:
if
created
:
child_product
.
attr
.
course_key
=
self
.
course_id
child_product
.
attr
.
course_key
=
self
.
course_id
child_product
.
attr
.
certificate_type
=
nam
e
child_product
.
attr
.
certificate_type
=
certificate_typ
e
child_product
.
attr
.
id_verification_required
=
id_verification_required
child_product
.
attr
.
id_verification_required
=
id_verification_required
child_product
.
save
()
child_product
.
save
()
...
...
ecommerce/tests/mixins.py
View file @
28ffaa5a
...
@@ -12,6 +12,7 @@ from mock import patch
...
@@ -12,6 +12,7 @@ from mock import patch
from
oscar.test
import
factories
from
oscar.test
import
factories
from
oscar.core.loading
import
get_model
,
get_class
from
oscar.core.loading
import
get_model
,
get_class
from
ecommerce.courses.utils
import
mode_for_seat
from
ecommerce.extensions.api.constants
import
APIConstants
as
AC
from
ecommerce.extensions.api.constants
import
APIConstants
as
AC
from
ecommerce.extensions.fulfillment.mixins
import
FulfillmentMixin
from
ecommerce.extensions.fulfillment.mixins
import
FulfillmentMixin
...
@@ -201,7 +202,7 @@ class BusinessIntelligenceMixin(object):
...
@@ -201,7 +202,7 @@ class BusinessIntelligenceMixin(object):
self
.
assertEqual
(
line
.
product
.
title
,
tracked_product
[
'name'
])
self
.
assertEqual
(
line
.
product
.
title
,
tracked_product
[
'name'
])
self
.
assertEqual
(
str
(
line
.
line_price_excl_tax
),
tracked_product
[
'price'
])
self
.
assertEqual
(
str
(
line
.
line_price_excl_tax
),
tracked_product
[
'price'
])
self
.
assertEqual
(
line
.
quantity
,
tracked_product
[
'quantity'
])
self
.
assertEqual
(
line
.
quantity
,
tracked_product
[
'quantity'
])
self
.
assertEqual
(
line
.
product
.
attr
.
certificate_type
,
tracked_product
[
'sku'
])
self
.
assertEqual
(
mode_for_seat
(
line
.
product
)
,
tracked_product
[
'sku'
])
self
.
assertEqual
(
line
.
product
.
get_product_class
()
.
name
,
tracked_product
[
'category'
])
self
.
assertEqual
(
line
.
product
.
get_product_class
()
.
name
,
tracked_product
[
'category'
])
elif
model_name
==
'Refund'
:
elif
model_name
==
'Refund'
:
self
.
assertEqual
(
event_payload
[
'total'
],
'-{}'
.
format
(
total
))
self
.
assertEqual
(
event_payload
[
'total'
],
'-{}'
.
format
(
total
))
...
@@ -212,7 +213,7 @@ class BusinessIntelligenceMixin(object):
...
@@ -212,7 +213,7 @@ class BusinessIntelligenceMixin(object):
self
.
assertEqual
(
line
.
order_line
.
product
.
title
,
tracked_product
[
'name'
])
self
.
assertEqual
(
line
.
order_line
.
product
.
title
,
tracked_product
[
'name'
])
self
.
assertEqual
(
str
(
line
.
line_credit_excl_tax
),
tracked_product
[
'price'
])
self
.
assertEqual
(
str
(
line
.
line_credit_excl_tax
),
tracked_product
[
'price'
])
self
.
assertEqual
(
-
1
*
line
.
quantity
,
tracked_product
[
'quantity'
])
self
.
assertEqual
(
-
1
*
line
.
quantity
,
tracked_product
[
'quantity'
])
self
.
assertEqual
(
line
.
order_line
.
product
.
attr
.
certificate_type
,
tracked_product
[
'sku'
])
self
.
assertEqual
(
mode_for_seat
(
line
.
order_line
.
product
)
,
tracked_product
[
'sku'
])
self
.
assertEqual
(
line
.
order_line
.
product
.
get_product_class
()
.
name
,
tracked_product
[
'category'
])
self
.
assertEqual
(
line
.
order_line
.
product
.
get_product_class
()
.
name
,
tracked_product
[
'category'
])
else
:
else
:
# Payload validation is currently limited to order and refund events
# Payload validation is currently limited to order and refund events
...
...
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