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
d5064413
Commit
d5064413
authored
Apr 21, 2017
by
uzairr
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix of refund after course mode expiry
parent
8fbf01cd
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
52 additions
and
26 deletions
+52
-26
common/djangoapps/course_modes/models.py
+5
-2
common/djangoapps/student/models.py
+6
-2
common/djangoapps/student/tests/test_refunds.py
+20
-14
lms/djangoapps/shoppingcart/tests/test_models.py
+12
-5
lms/djangoapps/shoppingcart/tests/test_reports.py
+4
-2
lms/djangoapps/support/tests/test_refund.py
+5
-1
No files found.
common/djangoapps/course_modes/models.py
View file @
d5064413
...
@@ -340,7 +340,7 @@ class CourseMode(models.Model):
...
@@ -340,7 +340,7 @@ class CourseMode(models.Model):
return
{
mode
.
slug
:
mode
for
mode
in
modes
}
return
{
mode
.
slug
:
mode
for
mode
in
modes
}
@classmethod
@classmethod
def
mode_for_course
(
cls
,
course_id
,
mode_slug
,
modes
=
None
):
def
mode_for_course
(
cls
,
course_id
,
mode_slug
,
modes
=
None
,
include_expired
=
False
):
"""Returns the mode for the course corresponding to mode_slug.
"""Returns the mode for the course corresponding to mode_slug.
Returns only non-expired modes.
Returns only non-expired modes.
...
@@ -356,12 +356,15 @@ class CourseMode(models.Model):
...
@@ -356,12 +356,15 @@ class CourseMode(models.Model):
of course modes. This can be used to avoid an additional
of course modes. This can be used to avoid an additional
database query if you have already loaded the modes list.
database query if you have already loaded the modes list.
include_expired (bool): If True, expired course modes will be included
in the returned values. If False, these modes will be omitted.
Returns:
Returns:
Mode
Mode
"""
"""
if
modes
is
None
:
if
modes
is
None
:
modes
=
cls
.
modes_for_course
(
course_id
)
modes
=
cls
.
modes_for_course
(
course_id
,
include_expired
=
include_expired
)
matched
=
[
m
for
m
in
modes
if
m
.
slug
==
mode_slug
]
matched
=
[
m
for
m
in
modes
if
m
.
slug
==
mode_slug
]
if
matched
:
if
matched
:
...
...
common/djangoapps/student/models.py
View file @
d5064413
...
@@ -1557,6 +1557,7 @@ class CourseEnrollment(models.Model):
...
@@ -1557,6 +1557,7 @@ class CourseEnrollment(models.Model):
# which calls this method to determine whether to refund the order.
# which calls this method to determine whether to refund the order.
# This can't be set directly because refunds currently happen as a side-effect of unenrolling.
# This can't be set directly because refunds currently happen as a side-effect of unenrolling.
# (side-effects are bad)
# (side-effects are bad)
if
getattr
(
self
,
'can_refund'
,
None
)
is
not
None
:
if
getattr
(
self
,
'can_refund'
,
None
)
is
not
None
:
return
True
return
True
...
@@ -1570,10 +1571,13 @@ class CourseEnrollment(models.Model):
...
@@ -1570,10 +1571,13 @@ class CourseEnrollment(models.Model):
# If it is after the refundable cutoff date they should not be refunded.
# If it is after the refundable cutoff date they should not be refunded.
refund_cutoff_date
=
self
.
refund_cutoff_date
()
refund_cutoff_date
=
self
.
refund_cutoff_date
()
if
refund_cutoff_date
and
datetime
.
now
(
UTC
)
>
refund_cutoff_date
:
# `refund_cuttoff_date` will be `None` if there is no order. If there is no order return `False`.
if
refund_cutoff_date
is
None
:
return
False
if
datetime
.
now
(
UTC
)
>
refund_cutoff_date
:
return
False
return
False
course_mode
=
CourseMode
.
mode_for_course
(
self
.
course_id
,
'verified'
)
course_mode
=
CourseMode
.
mode_for_course
(
self
.
course_id
,
'verified'
,
include_expired
=
True
)
if
course_mode
is
None
:
if
course_mode
is
None
:
return
False
return
False
else
:
else
:
...
...
common/djangoapps/student/tests/test_refunds.py
View file @
d5064413
...
@@ -55,23 +55,24 @@ class RefundableTest(SharedModuleStoreTestCase):
...
@@ -55,23 +55,24 @@ class RefundableTest(SharedModuleStoreTestCase):
mode_display_name
=
'Verified'
,
mode_display_name
=
'Verified'
,
expiration_datetime
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
days
=
1
)
expiration_datetime
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
days
=
1
)
)
)
self
.
enrollment
=
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course
.
id
,
mode
=
'verified'
)
self
.
enrollment
=
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course
.
id
,
mode
=
'verified'
)
self
.
client
=
Client
()
self
.
client
=
Client
()
cache
.
clear
()
cache
.
clear
()
def
test_refundable
(
self
):
@patch
(
'student.models.CourseEnrollment.refund_cutoff_date'
)
def
test_refundable
(
self
,
cutoff_date
):
""" Assert base case is refundable"""
""" Assert base case is refundable"""
cutoff_date
.
return_value
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
days
=
1
)
self
.
assertTrue
(
self
.
enrollment
.
refundable
())
self
.
assertTrue
(
self
.
enrollment
.
refundable
())
def
test_refundable_expired_verification
(
self
):
@patch
(
'student.models.CourseEnrollment.refund_cutoff_date'
)
""" Assert that enrollment is not refundable if course mode has expired."""
def
test_refundable_expired_verification
(
self
,
cutoff_date
):
""" Assert that enrollment is refundable if course mode has expired."""
cutoff_date
.
return_value
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
days
=
1
)
self
.
verified_mode
.
expiration_datetime
=
datetime
.
now
(
pytz
.
UTC
)
-
timedelta
(
days
=
1
)
self
.
verified_mode
.
expiration_datetime
=
datetime
.
now
(
pytz
.
UTC
)
-
timedelta
(
days
=
1
)
self
.
verified_mode
.
save
()
self
.
verified_mode
.
save
()
self
.
assertFalse
(
self
.
enrollment
.
refundable
())
# Assert that can_refund overrides this and allows refund
self
.
enrollment
.
can_refund
=
True
self
.
assertTrue
(
self
.
enrollment
.
refundable
())
self
.
assertTrue
(
self
.
enrollment
.
refundable
())
def
test_refundable_of_purchased_course
(
self
):
def
test_refundable_of_purchased_course
(
self
):
...
@@ -94,8 +95,12 @@ class RefundableTest(SharedModuleStoreTestCase):
...
@@ -94,8 +95,12 @@ class RefundableTest(SharedModuleStoreTestCase):
resp
=
self
.
client
.
post
(
reverse
(
'student.views.dashboard'
,
args
=
[]))
resp
=
self
.
client
.
post
(
reverse
(
'student.views.dashboard'
,
args
=
[]))
self
.
assertIn
(
'You will not be refunded the amount you paid.'
,
resp
.
content
)
self
.
assertIn
(
'You will not be refunded the amount you paid.'
,
resp
.
content
)
def
test_refundable_when_certificate_exists
(
self
):
@patch
(
'student.models.CourseEnrollment.refund_cutoff_date'
)
def
test_refundable_when_certificate_exists
(
self
,
cutoff_date
):
""" Assert that enrollment is not refundable once a certificat has been generated."""
""" Assert that enrollment is not refundable once a certificat has been generated."""
cutoff_date
.
return_value
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
days
=
1
)
self
.
assertTrue
(
self
.
enrollment
.
refundable
())
self
.
assertTrue
(
self
.
enrollment
.
refundable
())
GeneratedCertificateFactory
.
create
(
GeneratedCertificateFactory
.
create
(
...
@@ -121,16 +126,17 @@ class RefundableTest(SharedModuleStoreTestCase):
...
@@ -121,16 +126,17 @@ class RefundableTest(SharedModuleStoreTestCase):
)
)
)
)
def
test_refundable_with_cutoff_date
(
self
):
@patch
(
'student.models.CourseEnrollment.refund_cutoff_date'
)
def
test_refundable_with_cutoff_date
(
self
,
cutoff_date
):
""" Assert enrollment is refundable before cutoff and not refundable after."""
""" Assert enrollment is refundable before cutoff and not refundable after."""
cutoff_date
.
return_value
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
days
=
1
)
self
.
assertTrue
(
self
.
enrollment
.
refundable
())
self
.
assertTrue
(
self
.
enrollment
.
refundable
())
with
patch
(
'student.models.CourseEnrollment.refund_cutoff_date'
)
as
cutoff_date
:
cutoff_date
.
return_value
=
datetime
.
now
(
pytz
.
UTC
)
-
timedelta
(
minutes
=
5
)
cutoff_date
.
return_value
=
datetime
.
now
(
pytz
.
UTC
)
-
timedelta
(
minutes
=
5
)
self
.
assertFalse
(
self
.
enrollment
.
refundable
())
self
.
assertFalse
(
self
.
enrollment
.
refundable
())
cutoff_date
.
return_value
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
5
)
cutoff_date
.
return_value
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
5
)
self
.
assertTrue
(
self
.
enrollment
.
refundable
())
self
.
assertTrue
(
self
.
enrollment
.
refundable
())
@ddt.data
(
@ddt.data
(
(
timedelta
(
days
=
1
),
timedelta
(
days
=
2
),
timedelta
(
days
=
2
),
14
),
(
timedelta
(
days
=
1
),
timedelta
(
days
=
2
),
timedelta
(
days
=
2
),
14
),
...
...
lms/djangoapps/shoppingcart/tests/test_models.py
View file @
d5064413
...
@@ -911,9 +911,10 @@ class CertificateItemTest(ModuleStoreTestCase):
...
@@ -911,9 +911,10 @@ class CertificateItemTest(ModuleStoreTestCase):
'STORE_BILLING_INFO'
:
True
,
'STORE_BILLING_INFO'
:
True
,
}
}
)
)
def
test_refund_cert_callback_no_expiration
(
self
):
@patch
(
'student.models.CourseEnrollment.refund_cutoff_date'
)
def
test_refund_cert_callback_no_expiration
(
self
,
cutoff_date
):
# When there is no expiration date on a verified mode, the user can always get a refund
# When there is no expiration date on a verified mode, the user can always get a refund
cutoff_date
.
return_value
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
1
)
# need to prevent analytics errors from appearing in stderr
# need to prevent analytics errors from appearing in stderr
with
patch
(
'sys.stderr'
,
sys
.
stdout
.
write
):
with
patch
(
'sys.stderr'
,
sys
.
stdout
.
write
):
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course_key
,
'verified'
)
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course_key
,
'verified'
)
...
@@ -952,7 +953,8 @@ class CertificateItemTest(ModuleStoreTestCase):
...
@@ -952,7 +953,8 @@ class CertificateItemTest(ModuleStoreTestCase):
'STORE_BILLING_INFO'
:
True
,
'STORE_BILLING_INFO'
:
True
,
}
}
)
)
def
test_refund_cert_callback_before_expiration
(
self
):
@patch
(
'student.models.CourseEnrollment.refund_cutoff_date'
)
def
test_refund_cert_callback_before_expiration
(
self
,
cutoff_date
):
# If the expiration date has not yet passed on a verified mode, the user can be refunded
# If the expiration date has not yet passed on a verified mode, the user can be refunded
many_days
=
datetime
.
timedelta
(
days
=
60
)
many_days
=
datetime
.
timedelta
(
days
=
60
)
...
@@ -965,6 +967,7 @@ class CertificateItemTest(ModuleStoreTestCase):
...
@@ -965,6 +967,7 @@ class CertificateItemTest(ModuleStoreTestCase):
expiration_datetime
=
(
datetime
.
datetime
.
now
(
pytz
.
utc
)
+
many_days
))
expiration_datetime
=
(
datetime
.
datetime
.
now
(
pytz
.
utc
)
+
many_days
))
course_mode
.
save
()
course_mode
.
save
()
cutoff_date
.
return_value
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
1
)
# need to prevent analytics errors from appearing in stderr
# need to prevent analytics errors from appearing in stderr
with
patch
(
'sys.stderr'
,
sys
.
stdout
.
write
):
with
patch
(
'sys.stderr'
,
sys
.
stdout
.
write
):
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course_key
,
'verified'
)
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course_key
,
'verified'
)
...
@@ -979,7 +982,8 @@ class CertificateItemTest(ModuleStoreTestCase):
...
@@ -979,7 +982,8 @@ class CertificateItemTest(ModuleStoreTestCase):
self
.
assertEquals
(
target_certs
[
0
]
.
order
.
status
,
'refunded'
)
self
.
assertEquals
(
target_certs
[
0
]
.
order
.
status
,
'refunded'
)
self
.
_assert_refund_tracked
()
self
.
_assert_refund_tracked
()
def
test_refund_cert_callback_before_expiration_email
(
self
):
@patch
(
'student.models.CourseEnrollment.refund_cutoff_date'
)
def
test_refund_cert_callback_before_expiration_email
(
self
,
cutoff_date
):
""" Test that refund emails are being sent correctly. """
""" Test that refund emails are being sent correctly. """
course
=
CourseFactory
.
create
()
course
=
CourseFactory
.
create
()
course_key
=
course
.
id
course_key
=
course
.
id
...
@@ -998,6 +1002,7 @@ class CertificateItemTest(ModuleStoreTestCase):
...
@@ -998,6 +1002,7 @@ class CertificateItemTest(ModuleStoreTestCase):
cart
.
purchase
()
cart
.
purchase
()
mail
.
outbox
=
[]
mail
.
outbox
=
[]
cutoff_date
.
return_value
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
1
)
with
patch
(
'shoppingcart.models.log.error'
)
as
mock_error_logger
:
with
patch
(
'shoppingcart.models.log.error'
)
as
mock_error_logger
:
CourseEnrollment
.
unenroll
(
self
.
user
,
course_key
)
CourseEnrollment
.
unenroll
(
self
.
user
,
course_key
)
self
.
assertFalse
(
mock_error_logger
.
called
)
self
.
assertFalse
(
mock_error_logger
.
called
)
...
@@ -1006,8 +1011,9 @@ class CertificateItemTest(ModuleStoreTestCase):
...
@@ -1006,8 +1011,9 @@ class CertificateItemTest(ModuleStoreTestCase):
self
.
assertEquals
(
settings
.
PAYMENT_SUPPORT_EMAIL
,
mail
.
outbox
[
0
]
.
from_email
)
self
.
assertEquals
(
settings
.
PAYMENT_SUPPORT_EMAIL
,
mail
.
outbox
[
0
]
.
from_email
)
self
.
assertIn
(
'has requested a refund on Order'
,
mail
.
outbox
[
0
]
.
body
)
self
.
assertIn
(
'has requested a refund on Order'
,
mail
.
outbox
[
0
]
.
body
)
@patch
(
'student.models.CourseEnrollment.refund_cutoff_date'
)
@patch
(
'shoppingcart.models.log.error'
)
@patch
(
'shoppingcart.models.log.error'
)
def
test_refund_cert_callback_before_expiration_email_error
(
self
,
error_logger
):
def
test_refund_cert_callback_before_expiration_email_error
(
self
,
error_logger
,
cutoff_date
):
# If there's an error sending an email to billing, we need to log this error
# If there's an error sending an email to billing, we need to log this error
many_days
=
datetime
.
timedelta
(
days
=
60
)
many_days
=
datetime
.
timedelta
(
days
=
60
)
...
@@ -1026,6 +1032,7 @@ class CertificateItemTest(ModuleStoreTestCase):
...
@@ -1026,6 +1032,7 @@ class CertificateItemTest(ModuleStoreTestCase):
CertificateItem
.
add_to_order
(
cart
,
course_key
,
self
.
cost
,
'verified'
)
CertificateItem
.
add_to_order
(
cart
,
course_key
,
self
.
cost
,
'verified'
)
cart
.
purchase
()
cart
.
purchase
()
cutoff_date
.
return_value
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
1
)
with
patch
(
'shoppingcart.models.send_mail'
,
side_effect
=
smtplib
.
SMTPException
):
with
patch
(
'shoppingcart.models.send_mail'
,
side_effect
=
smtplib
.
SMTPException
):
CourseEnrollment
.
unenroll
(
self
.
user
,
course_key
)
CourseEnrollment
.
unenroll
(
self
.
user
,
course_key
)
self
.
assertTrue
(
error_logger
.
call_args
[
0
][
0
]
.
startswith
(
'Failed sending email'
))
self
.
assertTrue
(
error_logger
.
call_args
[
0
][
0
]
.
startswith
(
'Failed sending email'
))
...
...
lms/djangoapps/shoppingcart/tests/test_reports.py
View file @
d5064413
...
@@ -7,6 +7,7 @@ import datetime
...
@@ -7,6 +7,7 @@ import datetime
import
pytz
import
pytz
import
StringIO
import
StringIO
from
textwrap
import
dedent
from
textwrap
import
dedent
from
mock
import
patch
from
django.conf
import
settings
from
django.conf
import
settings
...
@@ -26,9 +27,10 @@ class ReportTypeTests(ModuleStoreTestCase):
...
@@ -26,9 +27,10 @@ class ReportTypeTests(ModuleStoreTestCase):
"""
"""
FIVE_MINS
=
datetime
.
timedelta
(
minutes
=
5
)
FIVE_MINS
=
datetime
.
timedelta
(
minutes
=
5
)
def
setUp
(
self
):
@patch
(
'student.models.CourseEnrollment.refund_cutoff_date'
)
def
setUp
(
self
,
cutoff_date
):
super
(
ReportTypeTests
,
self
)
.
setUp
()
super
(
ReportTypeTests
,
self
)
.
setUp
()
cutoff_date
.
return_value
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
1
)
# Need to make a *lot* of users for this one
# Need to make a *lot* of users for this one
self
.
first_verified_user
=
UserFactory
.
create
(
profile__name
=
"John Doe"
)
self
.
first_verified_user
=
UserFactory
.
create
(
profile__name
=
"John Doe"
)
self
.
second_verified_user
=
UserFactory
.
create
(
profile__name
=
"Jane Deer"
)
self
.
second_verified_user
=
UserFactory
.
create
(
profile__name
=
"Jane Deer"
)
...
...
lms/djangoapps/support/tests/test_refund.py
View file @
d5064413
...
@@ -8,6 +8,8 @@ to the E-Commerce service is complete.
...
@@ -8,6 +8,8 @@ to the E-Commerce service is complete.
"""
"""
import
datetime
import
datetime
import
pytz
from
mock
import
patch
from
django.test.client
import
Client
from
django.test.client
import
Client
...
@@ -91,10 +93,12 @@ class RefundTests(ModuleStoreTestCase):
...
@@ -91,10 +93,12 @@ class RefundTests(ModuleStoreTestCase):
response
=
self
.
client
.
post
(
'/support/refund/'
,
{
'course_id'
:
str
(
self
.
course_id
),
'user'
:
'unknown@foo.com'
})
response
=
self
.
client
.
post
(
'/support/refund/'
,
{
'course_id'
:
str
(
self
.
course_id
),
'user'
:
'unknown@foo.com'
})
self
.
assertContains
(
response
,
'User not found'
)
self
.
assertContains
(
response
,
'User not found'
)
def
test_not_refundable
(
self
):
@patch
(
'student.models.CourseEnrollment.refund_cutoff_date'
)
def
test_not_refundable
(
self
,
cutoff_date
):
self
.
_enroll
()
self
.
_enroll
()
self
.
course_mode
.
expiration_datetime
=
datetime
.
datetime
(
2033
,
4
,
6
)
self
.
course_mode
.
expiration_datetime
=
datetime
.
datetime
(
2033
,
4
,
6
)
self
.
course_mode
.
save
()
self
.
course_mode
.
save
()
cutoff_date
.
return_value
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
1
)
response
=
self
.
client
.
post
(
'/support/refund/'
,
self
.
form_pars
)
response
=
self
.
client
.
post
(
'/support/refund/'
,
self
.
form_pars
)
self
.
assertContains
(
response
,
'not past the refund window'
)
self
.
assertContains
(
response
,
'not past the refund window'
)
...
...
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