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
4e454cca
Commit
4e454cca
authored
Mar 13, 2015
by
Awais Qureshi
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #7267 from edx/awais786/ECOM-911-enable-noid-paid-course
Awais786/ecom 911 enable noid paid course
parents
cff9a5aa
4bab316b
Show whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
515 additions
and
121 deletions
+515
-121
common/djangoapps/course_modes/admin.py
+2
-1
common/djangoapps/course_modes/models.py
+176
-19
common/djangoapps/course_modes/tests/test_models.py
+114
-0
common/djangoapps/course_modes/tests/test_views.py
+25
-4
common/djangoapps/course_modes/views.py
+4
-4
common/djangoapps/enrollment/tests/test_api.py
+4
-2
common/djangoapps/student/models.py
+11
-3
common/djangoapps/student/tests/test_enrollment.py
+4
-0
common/djangoapps/student/tests/tests.py
+6
-1
common/djangoapps/student/views.py
+2
-2
lms/djangoapps/shoppingcart/models.py
+12
-6
lms/djangoapps/shoppingcart/tests/test_models.py
+23
-0
lms/djangoapps/student_account/test/test_views.py
+1
-1
lms/djangoapps/verify_student/tests/test_views.py
+70
-6
lms/djangoapps/verify_student/views.py
+48
-28
lms/templates/dashboard/_dashboard_course_listing.html
+13
-44
No files found.
common/djangoapps/course_modes/admin.py
View file @
4e454cca
...
@@ -21,7 +21,8 @@ class CourseModeForm(forms.ModelForm):
...
@@ -21,7 +21,8 @@ class CourseModeForm(forms.ModelForm):
COURSE_MODE_SLUG_CHOICES
=
(
COURSE_MODE_SLUG_CHOICES
=
(
[(
CourseMode
.
DEFAULT_MODE_SLUG
,
CourseMode
.
DEFAULT_MODE_SLUG
)]
+
[(
CourseMode
.
DEFAULT_MODE_SLUG
,
CourseMode
.
DEFAULT_MODE_SLUG
)]
+
[(
mode_slug
,
mode_slug
)
for
mode_slug
in
CourseMode
.
VERIFIED_MODES
]
[(
mode_slug
,
mode_slug
)
for
mode_slug
in
CourseMode
.
VERIFIED_MODES
]
+
[(
CourseMode
.
NO_ID_PROFESSIONAL_MODE
,
CourseMode
.
NO_ID_PROFESSIONAL_MODE
)]
)
)
mode_slug
=
forms
.
ChoiceField
(
choices
=
COURSE_MODE_SLUG_CHOICES
)
mode_slug
=
forms
.
ChoiceField
(
choices
=
COURSE_MODE_SLUG_CHOICES
)
...
...
common/djangoapps/course_modes/models.py
View file @
4e454cca
...
@@ -65,11 +65,17 @@ class CourseMode(models.Model):
...
@@ -65,11 +65,17 @@ class CourseMode(models.Model):
help_text
=
"This is the SKU(stock keeping unit) of this mode in external services."
help_text
=
"This is the SKU(stock keeping unit) of this mode in external services."
)
)
DEFAULT_MODE
=
Mode
(
'honor'
,
_
(
'Honor Code Certificate'
),
0
,
''
,
'usd'
,
None
,
None
,
None
)
HONOR
=
'honor'
DEFAULT_MODE_SLUG
=
'honor'
PROFESSIONAL
=
'professional'
VERIFIED
=
"verified"
AUDIT
=
"audit"
NO_ID_PROFESSIONAL_MODE
=
"no-id-professional"
DEFAULT_MODE
=
Mode
(
HONOR
,
_
(
'Honor Code Certificate'
),
0
,
''
,
'usd'
,
None
,
None
,
None
)
DEFAULT_MODE_SLUG
=
HONOR
# Modes that allow a student to pursue a verified certificate
# Modes that allow a student to pursue a verified certificate
VERIFIED_MODES
=
[
"verified"
,
"professional"
]
VERIFIED_MODES
=
[
VERIFIED
,
PROFESSIONAL
]
class
Meta
:
class
Meta
:
""" meta attributes of this model """
""" meta attributes of this model """
...
@@ -249,6 +255,23 @@ class CourseMode(models.Model):
...
@@ -249,6 +255,23 @@ class CourseMode(models.Model):
return
professional_mode
if
professional_mode
else
verified_mode
return
professional_mode
if
professional_mode
else
verified_mode
@classmethod
@classmethod
def
min_course_price_for_verified_for_currency
(
cls
,
course_id
,
currency
):
# pylint: disable=invalid-name
"""
Returns the minimum price of the course int he appropriate currency over all the
course's *verified*, non-expired modes.
Assuming all verified courses have a minimum price of >0, this value should always
be >0.
If no verified mode is found, 0 is returned.
"""
modes
=
cls
.
modes_for_course
(
course_id
)
for
mode
in
modes
:
if
(
mode
.
currency
==
currency
)
and
(
mode
.
slug
==
'verified'
):
return
mode
.
min_price
return
0
@classmethod
def
has_verified_mode
(
cls
,
course_mode_dict
):
def
has_verified_mode
(
cls
,
course_mode_dict
):
"""Check whether the modes for a course allow a student to pursue a verfied certificate.
"""Check whether the modes for a course allow a student to pursue a verfied certificate.
...
@@ -265,21 +288,65 @@ class CourseMode(models.Model):
...
@@ -265,21 +288,65 @@ class CourseMode(models.Model):
return
False
return
False
@classmethod
@classmethod
def
min_course_price_for_verified_for_currency
(
cls
,
course_id
,
currency
):
def
has_professional_mode
(
cls
,
modes_dict
):
"""
"""
Returns the minimum price of the course int he appropriate currency over all the
check the course mode is profession or no-id-professional
course's *verified*, non-expired modes.
A
ssuming all verified courses have a minimum price of >0, this value should always
A
rgs:
be >0
.
modes_dict (dict): course modes
.
If no verified mode is found, 0 is returned.
Returns:
bool
"""
"""
modes
=
cls
.
modes_for_course
(
course_id
)
return
cls
.
PROFESSIONAL
in
modes_dict
or
cls
.
NO_ID_PROFESSIONAL_MODE
in
modes_dict
for
mode
in
modes
:
if
(
mode
.
currency
==
currency
)
and
(
mode
.
slug
==
'verified'
):
@classmethod
return
mode
.
min_price
def
is_professional_mode
(
cls
,
course_mode_tuple
):
return
0
"""
checking that tuple is professional mode.
Args:
course_mode_tuple (tuple) : course mode tuple
Returns:
bool
"""
return
course_mode_tuple
.
slug
in
[
cls
.
PROFESSIONAL
,
cls
.
NO_ID_PROFESSIONAL_MODE
]
if
course_mode_tuple
else
False
@classmethod
def
is_professional_slug
(
cls
,
slug
):
"""checking slug is professional
Args:
slug (str) : course mode string
Return:
bool
"""
return
slug
in
[
cls
.
PROFESSIONAL
,
cls
.
NO_ID_PROFESSIONAL_MODE
]
@classmethod
def
is_verified_mode
(
cls
,
course_mode_tuple
):
"""Check whether the given modes is_verified or not.
Args:
course_mode_tuple(Mode): Mode tuple
Returns:
bool: True iff the course modes is verified else False.
"""
return
course_mode_tuple
.
slug
in
cls
.
VERIFIED_MODES
@classmethod
def
is_verified_slug
(
cls
,
mode_slug
):
"""Check whether the given mode_slug is_verified or not.
Args:
mode_slug(str): Mode Slug
Returns:
bool: True iff the course mode slug is verified else False.
"""
return
mode_slug
in
cls
.
VERIFIED_MODES
@classmethod
@classmethod
def
has_payment_options
(
cls
,
course_id
):
def
has_payment_options
(
cls
,
course_id
):
...
@@ -325,8 +392,8 @@ class CourseMode(models.Model):
...
@@ -325,8 +392,8 @@ class CourseMode(models.Model):
if
modes_dict
is
None
:
if
modes_dict
is
None
:
modes_dict
=
cls
.
modes_for_course_dict
(
course_id
)
modes_dict
=
cls
.
modes_for_course_dict
(
course_id
)
# Professional mode courses are always behind a paywall
# Professional
and no-id-professional
mode courses are always behind a paywall
if
"professional"
in
modes_dict
:
if
cls
.
has_professional_mode
(
modes_dict
)
:
return
False
return
False
# White-label uses course mode honor with a price
# White-label uses course mode honor with a price
...
@@ -335,7 +402,7 @@ class CourseMode(models.Model):
...
@@ -335,7 +402,7 @@ class CourseMode(models.Model):
return
False
return
False
# Check that the default mode is available.
# Check that the default mode is available.
return
(
"honor"
in
modes_dict
)
return
(
cls
.
HONOR
in
modes_dict
)
@classmethod
@classmethod
def
is_white_label
(
cls
,
course_id
,
modes_dict
=
None
):
def
is_white_label
(
cls
,
course_id
,
modes_dict
=
None
):
...
@@ -360,13 +427,13 @@ class CourseMode(models.Model):
...
@@ -360,13 +427,13 @@ class CourseMode(models.Model):
# White-label uses course mode honor with a price
# White-label uses course mode honor with a price
# to indicate that the course is behind a paywall.
# to indicate that the course is behind a paywall.
if
"honor"
in
modes_dict
and
len
(
modes_dict
)
==
1
:
if
cls
.
HONOR
in
modes_dict
and
len
(
modes_dict
)
==
1
:
if
modes_dict
[
"honor"
]
.
min_price
>
0
or
modes_dict
[
"honor"
]
.
suggested_prices
!=
''
:
if
modes_dict
[
"honor"
]
.
min_price
>
0
or
modes_dict
[
"honor"
]
.
suggested_prices
!=
''
:
return
True
return
True
return
False
return
False
@classmethod
@classmethod
def
min_course_price_for_currency
(
cls
,
course_id
,
currency
):
def
min_course_price_for_currency
(
cls
,
course_id
,
currency
):
# pylint: disable=invalid-name
"""
"""
Returns the minimum price of the course in the appropriate currency over all the course's
Returns the minimum price of the course in the appropriate currency over all the course's
non-expired modes.
non-expired modes.
...
@@ -375,6 +442,96 @@ class CourseMode(models.Model):
...
@@ -375,6 +442,96 @@ class CourseMode(models.Model):
modes
=
cls
.
modes_for_course
(
course_id
)
modes
=
cls
.
modes_for_course
(
course_id
)
return
min
(
mode
.
min_price
for
mode
in
modes
if
mode
.
currency
==
currency
)
return
min
(
mode
.
min_price
for
mode
in
modes
if
mode
.
currency
==
currency
)
@classmethod
def
enrollment_mode_display
(
cls
,
mode
,
verification_status
):
""" Select appropriate display strings and CSS classes.
Uses mode and verification status to select appropriate display strings and CSS classes
for certificate display.
Args:
mode (str): enrollment mode.
verification_status (str) : verification status of student
Returns:
dictionary:
"""
# import inside the function to avoid the circular import
from
student.helpers
import
(
VERIFY_STATUS_NEED_TO_VERIFY
,
VERIFY_STATUS_SUBMITTED
,
VERIFY_STATUS_APPROVED
)
show_image
=
False
image_alt
=
''
if
mode
==
cls
.
VERIFIED
:
if
verification_status
in
[
VERIFY_STATUS_NEED_TO_VERIFY
,
VERIFY_STATUS_SUBMITTED
]:
enrollment_title
=
_
(
"Your verification is pending"
)
enrollment_value
=
_
(
"Verified: Pending Verification"
)
show_image
=
True
image_alt
=
_
(
"ID verification pending"
)
elif
verification_status
==
VERIFY_STATUS_APPROVED
:
enrollment_title
=
_
(
"You're enrolled as a verified student"
)
enrollment_value
=
_
(
"Verified"
)
show_image
=
True
image_alt
=
_
(
"ID Verified Ribbon/Badge"
)
else
:
enrollment_title
=
_
(
"You're enrolled as an honor code student"
)
enrollment_value
=
_
(
"Honor Code"
)
elif
mode
==
cls
.
HONOR
:
enrollment_title
=
_
(
"You're enrolled as an honor code student"
)
enrollment_value
=
_
(
"Honor Code"
)
elif
mode
==
cls
.
AUDIT
:
enrollment_title
=
_
(
"You're auditing this course"
)
enrollment_value
=
_
(
"Auditing"
)
elif
mode
in
[
cls
.
PROFESSIONAL
,
cls
.
NO_ID_PROFESSIONAL_MODE
]:
enrollment_title
=
_
(
"You're enrolled as a professional education student"
)
enrollment_value
=
_
(
"Professional Ed"
)
else
:
enrollment_title
=
''
enrollment_value
=
''
return
{
'enrollment_title'
:
unicode
(
enrollment_title
),
'enrollment_value'
:
unicode
(
enrollment_value
),
'show_image'
:
show_image
,
'image_alt'
:
unicode
(
image_alt
),
'display_mode'
:
cls
.
_enrollment_mode_display
(
mode
,
verification_status
)
}
@staticmethod
def
_enrollment_mode_display
(
enrollment_mode
,
verification_status
):
"""Checking enrollment mode and status and returns the display mode
Args:
enrollment_mode (str): enrollment mode.
verification_status (str) : verification status of student
Returns:
display_mode (str) : display mode for certs
"""
# import inside the function to avoid the circular import
from
student.helpers
import
(
VERIFY_STATUS_NEED_TO_VERIFY
,
VERIFY_STATUS_SUBMITTED
,
VERIFY_STATUS_APPROVED
)
if
enrollment_mode
==
CourseMode
.
VERIFIED
:
if
verification_status
in
[
VERIFY_STATUS_NEED_TO_VERIFY
,
VERIFY_STATUS_SUBMITTED
,
VERIFY_STATUS_APPROVED
]:
display_mode
=
"verified"
else
:
display_mode
=
"honor"
elif
enrollment_mode
in
[
CourseMode
.
PROFESSIONAL
,
CourseMode
.
NO_ID_PROFESSIONAL_MODE
]:
display_mode
=
"professional"
else
:
display_mode
=
enrollment_mode
return
display_mode
def
to_tuple
(
self
):
def
to_tuple
(
self
):
"""
"""
Takes a mode model and turns it into a model named tuple.
Takes a mode model and turns it into a model named tuple.
...
...
common/djangoapps/course_modes/tests/test_models.py
View file @
4e454cca
...
@@ -150,11 +150,17 @@ class CourseModeModelTest(TestCase):
...
@@ -150,11 +150,17 @@ class CourseModeModelTest(TestCase):
honor
.
save
()
honor
.
save
()
self
.
assertTrue
(
CourseMode
.
has_payment_options
(
self
.
course_key
))
self
.
assertTrue
(
CourseMode
.
has_payment_options
(
self
.
course_key
))
def
test_course_has_payment_options_with_no_id_professional
(
self
):
# Has payment options.
self
.
create_mode
(
'no-id-professional'
,
'no-id-professional'
,
min_price
=
5
)
self
.
assertTrue
(
CourseMode
.
has_payment_options
(
self
.
course_key
))
@ddt.data
(
@ddt.data
(
([],
True
),
([],
True
),
([(
"honor"
,
0
),
(
"audit"
,
0
),
(
"verified"
,
100
)],
True
),
([(
"honor"
,
0
),
(
"audit"
,
0
),
(
"verified"
,
100
)],
True
),
([(
"honor"
,
100
)],
False
),
([(
"honor"
,
100
)],
False
),
([(
"professional"
,
100
)],
False
),
([(
"professional"
,
100
)],
False
),
([(
"no-id-professional"
,
100
)],
False
),
)
)
@ddt.unpack
@ddt.unpack
def
test_can_auto_enroll
(
self
,
modes_and_prices
,
can_auto_enroll
):
def
test_can_auto_enroll
(
self
,
modes_and_prices
,
can_auto_enroll
):
...
@@ -206,3 +212,111 @@ class CourseModeModelTest(TestCase):
...
@@ -206,3 +212,111 @@ class CourseModeModelTest(TestCase):
# Check that we get a default mode for when no course mode is available
# Check that we get a default mode for when no course mode is available
self
.
assertEqual
(
len
(
all_modes
[
other_course_key
]),
1
)
self
.
assertEqual
(
len
(
all_modes
[
other_course_key
]),
1
)
self
.
assertEqual
(
all_modes
[
other_course_key
][
0
],
CourseMode
.
DEFAULT_MODE
)
self
.
assertEqual
(
all_modes
[
other_course_key
][
0
],
CourseMode
.
DEFAULT_MODE
)
@ddt.data
(
''
,
'no-id-professional'
,
'professional'
,
'verified'
)
def
test_course_has_professional_mode
(
self
,
mode
):
# check the professional mode.
self
.
create_mode
(
mode
,
'course mode'
,
10
)
modes_dict
=
CourseMode
.
modes_for_course_dict
(
self
.
course_key
)
if
mode
in
[
'professional'
,
'no-id-professional'
]:
self
.
assertTrue
(
CourseMode
.
has_professional_mode
(
modes_dict
))
else
:
self
.
assertFalse
(
CourseMode
.
has_professional_mode
(
modes_dict
))
@ddt.data
(
'no-id-professional'
,
'professional'
,
'verified'
)
def
test_course_is_professional_mode
(
self
,
mode
):
# check that tuple has professional mode
course_mode
,
__
=
self
.
create_mode
(
mode
,
'course mode'
,
10
)
if
mode
in
[
'professional'
,
'no-id-professional'
]:
self
.
assertTrue
(
CourseMode
.
is_professional_mode
(
course_mode
.
to_tuple
()))
else
:
self
.
assertFalse
(
CourseMode
.
is_professional_mode
(
course_mode
.
to_tuple
()))
def
test_course_is_professional_mode_with_invalid_tuple
(
self
):
# check that tuple has professional mode with None
self
.
assertFalse
(
CourseMode
.
is_professional_mode
(
None
))
@ddt.data
(
(
'no-id-professional'
,
False
),
(
'professional'
,
True
),
(
'verified'
,
True
),
(
'honor'
,
False
),
(
'audit'
,
False
)
)
@ddt.unpack
def
test_is_verified_slug
(
self
,
mode_slug
,
is_verified
):
# check that mode slug is verified or not
if
is_verified
:
self
.
assertTrue
(
CourseMode
.
is_verified_slug
(
mode_slug
))
else
:
self
.
assertFalse
(
CourseMode
.
is_verified_slug
(
mode_slug
))
@ddt.data
(
(
"verified"
,
"verify_need_to_verify"
),
(
"verified"
,
"verify_submitted"
),
(
"verified"
,
"verify_approved"
),
(
"verified"
,
'dummy'
),
(
"verified"
,
None
),
(
'honor'
,
None
),
(
'honor'
,
'dummy'
),
(
'audit'
,
None
),
(
'professional'
,
None
),
(
'no-id-professional'
,
None
),
(
'no-id-professional'
,
'dummy'
)
)
@ddt.unpack
def
test_enrollment_mode_display
(
self
,
mode
,
verification_status
):
if
mode
==
"verified"
:
self
.
assertEqual
(
CourseMode
.
enrollment_mode_display
(
mode
,
verification_status
),
self
.
_enrollment_display_modes_dicts
(
verification_status
)
)
self
.
assertEqual
(
CourseMode
.
enrollment_mode_display
(
mode
,
verification_status
),
self
.
_enrollment_display_modes_dicts
(
verification_status
)
)
self
.
assertEqual
(
CourseMode
.
enrollment_mode_display
(
mode
,
verification_status
),
self
.
_enrollment_display_modes_dicts
(
verification_status
)
)
elif
mode
==
"honor"
:
self
.
assertEqual
(
CourseMode
.
enrollment_mode_display
(
mode
,
verification_status
),
self
.
_enrollment_display_modes_dicts
(
mode
)
)
elif
mode
==
"audit"
:
self
.
assertEqual
(
CourseMode
.
enrollment_mode_display
(
mode
,
verification_status
),
self
.
_enrollment_display_modes_dicts
(
mode
)
)
elif
mode
==
"professional"
:
self
.
assertEqual
(
CourseMode
.
enrollment_mode_display
(
mode
,
verification_status
),
self
.
_enrollment_display_modes_dicts
(
mode
)
)
def
_enrollment_display_modes_dicts
(
self
,
dict_type
):
"""
Helper function to generate the enrollment display mode dict.
"""
dict_keys
=
[
'enrollment_title'
,
'enrollment_value'
,
'show_image'
,
'image_alt'
,
'display_mode'
]
display_values
=
{
"verify_need_to_verify"
:
[
"Your verification is pending"
,
"Verified: Pending Verification"
,
True
,
'ID verification pending'
,
'verified'
],
"verify_approved"
:
[
"You're enrolled as a verified student"
,
"Verified"
,
True
,
'ID Verified Ribbon/Badge'
,
'verified'
],
"verify_none"
:
[
"You're enrolled as an honor code student"
,
"Honor Code"
,
False
,
''
,
'honor'
],
"honor"
:
[
"You're enrolled as an honor code student"
,
"Honor Code"
,
False
,
''
,
'honor'
],
"audit"
:
[
"You're auditing this course"
,
"Auditing"
,
False
,
''
,
'audit'
],
"professional"
:
[
"You're enrolled as a professional education student"
,
"Professional Ed"
,
False
,
''
,
'professional'
]
}
if
dict_type
in
[
'verify_need_to_verify'
,
'verify_submitted'
]:
return
dict
(
zip
(
dict_keys
,
display_values
.
get
(
'verify_need_to_verify'
)))
elif
dict_type
is
None
or
dict_type
==
'dummy'
:
return
dict
(
zip
(
dict_keys
,
display_values
.
get
(
'verify_none'
)))
else
:
return
dict
(
zip
(
dict_keys
,
display_values
.
get
(
dict_type
)))
common/djangoapps/course_modes/tests/test_views.py
View file @
4e454cca
...
@@ -68,6 +68,25 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
...
@@ -68,6 +68,25 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
else
:
else
:
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
def
test_no_id_redirect
(
self
):
# Create the course modes
CourseModeFactory
(
mode_slug
=
CourseMode
.
NO_ID_PROFESSIONAL_MODE
,
course_id
=
self
.
course
.
id
,
min_price
=
100
)
# Enroll the user in the test course
CourseEnrollmentFactory
(
is_active
=
False
,
mode
=
CourseMode
.
NO_ID_PROFESSIONAL_MODE
,
course_id
=
self
.
course
.
id
,
user
=
self
.
user
)
# Configure whether we're upgrading or not
url
=
reverse
(
'course_modes_choose'
,
args
=
[
unicode
(
self
.
course
.
id
)])
response
=
self
.
client
.
get
(
url
)
# Check whether we were correctly redirected
start_flow_url
=
reverse
(
'verify_student_start_flow'
,
args
=
[
unicode
(
self
.
course
.
id
)])
self
.
assertRedirects
(
response
,
start_flow_url
)
def
test_no_enrollment
(
self
):
def
test_no_enrollment
(
self
):
# Create the course modes
# Create the course modes
for
mode
in
(
'audit'
,
'honor'
,
'verified'
):
for
mode
in
(
'audit'
,
'honor'
,
'verified'
):
...
@@ -115,9 +134,10 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
...
@@ -115,9 +134,10 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
# TODO: Fix it so that response.templates works w/ mako templates, and then assert
# TODO: Fix it so that response.templates works w/ mako templates, and then assert
# that the right template rendered
# that the right template rendered
def
test_professional_enrollment
(
self
):
@ddt.data
(
'professional'
,
'no-id-professional'
)
def
test_professional_enrollment
(
self
,
mode
):
# The only course mode is professional ed
# The only course mode is professional ed
CourseModeFactory
(
mode_slug
=
'professional'
,
course_id
=
self
.
course
.
id
)
CourseModeFactory
(
mode_slug
=
mode
,
course_id
=
self
.
course
.
id
,
min_price
=
1
)
# Go to the "choose your track" page
# Go to the "choose your track" page
choose_track_url
=
reverse
(
'course_modes_choose'
,
args
=
[
unicode
(
self
.
course
.
id
)])
choose_track_url
=
reverse
(
'course_modes_choose'
,
args
=
[
unicode
(
self
.
course
.
id
)])
...
@@ -132,7 +152,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
...
@@ -132,7 +152,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
CourseEnrollmentFactory
(
CourseEnrollmentFactory
(
user
=
self
.
user
,
user
=
self
.
user
,
is_active
=
True
,
is_active
=
True
,
mode
=
"professional"
,
mode
=
mode
,
course_id
=
unicode
(
self
.
course
.
id
),
course_id
=
unicode
(
self
.
course
.
id
),
)
)
...
@@ -156,7 +176,8 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
...
@@ -156,7 +176,8 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
def
test_choose_mode_redirect
(
self
,
course_mode
,
expected_redirect
):
def
test_choose_mode_redirect
(
self
,
course_mode
,
expected_redirect
):
# Create the course modes
# Create the course modes
for
mode
in
(
'audit'
,
'honor'
,
'verified'
):
for
mode
in
(
'audit'
,
'honor'
,
'verified'
):
CourseModeFactory
(
mode_slug
=
mode
,
course_id
=
self
.
course
.
id
)
min_price
=
0
if
course_mode
in
[
"honor"
,
"audit"
]
else
1
CourseModeFactory
(
mode_slug
=
mode
,
course_id
=
self
.
course
.
id
,
min_price
=
min_price
)
# Choose the mode (POST request)
# Choose the mode (POST request)
choose_track_url
=
reverse
(
'course_modes_choose'
,
args
=
[
unicode
(
self
.
course
.
id
)])
choose_track_url
=
reverse
(
'course_modes_choose'
,
args
=
[
unicode
(
self
.
course
.
id
)])
...
...
common/djangoapps/course_modes/views.py
View file @
4e454cca
...
@@ -72,9 +72,9 @@ class ChooseModeView(View):
...
@@ -72,9 +72,9 @@ class ChooseModeView(View):
# We assume that, if 'professional' is one of the modes, it is the *only* mode.
# We assume that, if 'professional' is one of the modes, it is the *only* mode.
# If we offer more modes alongside 'professional' in the future, this will need to route
# If we offer more modes alongside 'professional' in the future, this will need to route
# to the usual "choose your track" page.
# to the usual "choose your track" page
same is true for no-id-professional mode
.
has_enrolled_professional
=
(
enrollment_mode
==
"professional"
and
is_active
)
has_enrolled_professional
=
(
CourseMode
.
is_professional_slug
(
enrollment_mode
)
and
is_active
)
if
"professional"
in
modes
and
not
has_enrolled_professional
:
if
CourseMode
.
has_professional_mode
(
modes
)
and
not
has_enrolled_professional
:
return
redirect
(
return
redirect
(
reverse
(
reverse
(
'verify_student_start_flow'
,
'verify_student_start_flow'
,
...
@@ -90,7 +90,7 @@ class ChooseModeView(View):
...
@@ -90,7 +90,7 @@ class ChooseModeView(View):
return
redirect
(
reverse
(
'dashboard'
))
return
redirect
(
reverse
(
'dashboard'
))
# If a user has already paid, redirect them to the dashboard.
# If a user has already paid, redirect them to the dashboard.
if
is_active
and
enrollment_mode
in
CourseMode
.
VERIFIED_MODES
:
if
is_active
and
(
enrollment_mode
in
CourseMode
.
VERIFIED_MODES
+
[
CourseMode
.
NO_ID_PROFESSIONAL_MODE
])
:
return
redirect
(
reverse
(
'dashboard'
))
return
redirect
(
reverse
(
'dashboard'
))
donation_for_course
=
request
.
session
.
get
(
"donation_for_course"
,
{})
donation_for_course
=
request
.
session
.
get
(
"donation_for_course"
,
{})
...
...
common/djangoapps/enrollment/tests/test_api.py
View file @
4e454cca
...
@@ -38,7 +38,8 @@ class EnrollmentTest(TestCase):
...
@@ -38,7 +38,8 @@ class EnrollmentTest(TestCase):
([
'honor'
,
'verified'
,
'audit'
],
'honor'
),
([
'honor'
,
'verified'
,
'audit'
],
'honor'
),
# Check for professional ed happy path.
# Check for professional ed happy path.
([
'professional'
],
'professional'
)
([
'professional'
],
'professional'
),
([
'no-id-professional'
],
'no-id-professional'
)
)
)
@ddt.unpack
@ddt.unpack
def
test_enroll
(
self
,
course_modes
,
mode
):
def
test_enroll
(
self
,
course_modes
,
mode
):
...
@@ -72,7 +73,8 @@ class EnrollmentTest(TestCase):
...
@@ -72,7 +73,8 @@ class EnrollmentTest(TestCase):
([
'honor'
,
'verified'
,
'audit'
],
'honor'
),
([
'honor'
,
'verified'
,
'audit'
],
'honor'
),
# Check for professional ed happy path.
# Check for professional ed happy path.
([
'professional'
],
'professional'
)
([
'professional'
],
'professional'
),
([
'no-id-professional'
],
'no-id-professional'
)
)
)
@ddt.unpack
@ddt.unpack
def
test_unenroll
(
self
,
course_modes
,
mode
):
def
test_unenroll
(
self
,
course_modes
,
mode
):
...
...
common/djangoapps/student/models.py
View file @
4e454cca
...
@@ -1100,9 +1100,8 @@ class CourseEnrollment(models.Model):
...
@@ -1100,9 +1100,8 @@ class CourseEnrollment(models.Model):
"""
"""
Returns True, if course is paid
Returns True, if course is paid
"""
"""
paid_course
=
CourseMode
.
objects
.
filter
(
Q
(
course_id
=
self
.
course_id
)
&
Q
(
mode_slug
=
'honor'
)
&
paid_course
=
CourseMode
.
is_white_label
(
self
.
course_id
)
(
Q
(
expiration_datetime__isnull
=
True
)
|
Q
(
expiration_datetime__gte
=
datetime
.
now
(
pytz
.
UTC
))))
.
exclude
(
min_price
=
0
)
if
paid_course
or
CourseMode
.
is_professional_slug
(
self
.
mode
):
if
paid_course
or
self
.
mode
==
'professional'
:
return
True
return
True
return
False
return
False
...
@@ -1154,6 +1153,12 @@ class CourseEnrollment(models.Model):
...
@@ -1154,6 +1153,12 @@ class CourseEnrollment(models.Model):
def
course
(
self
):
def
course
(
self
):
return
modulestore
()
.
get_course
(
self
.
course_id
)
return
modulestore
()
.
get_course
(
self
.
course_id
)
def
is_verified_enrollment
(
self
):
"""
Check the course enrollment mode is verified or not
"""
return
CourseMode
.
is_verified_slug
(
self
.
mode
)
class
CourseEnrollmentAllowed
(
models
.
Model
):
class
CourseEnrollmentAllowed
(
models
.
Model
):
"""
"""
...
@@ -1403,6 +1408,9 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
...
@@ -1403,6 +1408,9 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
"honor"
:
ugettext_lazy
(
u"{platform_name} Honor Code Certificate for {course_name}"
),
"honor"
:
ugettext_lazy
(
u"{platform_name} Honor Code Certificate for {course_name}"
),
"verified"
:
ugettext_lazy
(
u"{platform_name} Verified Certificate for {course_name}"
),
"verified"
:
ugettext_lazy
(
u"{platform_name} Verified Certificate for {course_name}"
),
"professional"
:
ugettext_lazy
(
u"{platform_name} Professional Certificate for {course_name}"
),
"professional"
:
ugettext_lazy
(
u"{platform_name} Professional Certificate for {course_name}"
),
"no-id-professional"
:
ugettext_lazy
(
u"{platform_name} Professional Certificate for {course_name}"
),
}
}
company_identifier
=
models
.
TextField
(
company_identifier
=
models
.
TextField
(
...
...
common/djangoapps/student/tests/test_enrollment.py
View file @
4e454cca
...
@@ -55,6 +55,7 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
...
@@ -55,6 +55,7 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
# We should NOT be auto-enrolled, because that would be giving
# We should NOT be auto-enrolled, because that would be giving
# away an expensive course for free :)
# away an expensive course for free :)
([
'professional'
],
'course_modes_choose'
,
None
),
([
'professional'
],
'course_modes_choose'
,
None
),
([
'no-id-professional'
],
'course_modes_choose'
,
None
),
)
)
@ddt.unpack
@ddt.unpack
def
test_enroll
(
self
,
course_modes
,
next_url
,
enrollment_mode
):
def
test_enroll
(
self
,
course_modes
,
next_url
,
enrollment_mode
):
...
@@ -113,6 +114,9 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
...
@@ -113,6 +114,9 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
([
'professional'
],
'true'
),
([
'professional'
],
'true'
),
([
'professional'
],
'false'
),
([
'professional'
],
'false'
),
([
'professional'
],
None
),
([
'professional'
],
None
),
([
'no-id-professional'
],
'true'
),
([
'no-id-professional'
],
'false'
),
([
'no-id-professional'
],
None
),
)
)
@ddt.unpack
@ddt.unpack
def
test_enroll_with_email_opt_in
(
self
,
course_modes
,
email_opt_in
,
mock_update_email_opt_in
):
def
test_enroll_with_email_opt_in
(
self
,
course_modes
,
email_opt_in
,
mock_update_email_opt_in
):
...
...
common/djangoapps/student/tests/tests.py
View file @
4e454cca
...
@@ -219,7 +219,10 @@ class DashboardTest(ModuleStoreTestCase):
...
@@ -219,7 +219,10 @@ class DashboardTest(ModuleStoreTestCase):
attempt
.
approve
()
attempt
.
approve
()
response
=
self
.
client
.
get
(
reverse
(
'dashboard'
))
response
=
self
.
client
.
get
(
reverse
(
'dashboard'
))
self
.
assertContains
(
response
,
"class=
\"
course {0}
\"
"
.
format
(
mode
))
if
mode
in
[
'professional'
,
'no-id-professional'
]:
self
.
assertContains
(
response
,
'class="course professional"'
)
else
:
self
.
assertContains
(
response
,
'class="course {0}"'
.
format
(
mode
))
self
.
assertContains
(
response
,
value
)
self
.
assertContains
(
response
,
value
)
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
'ENABLE_VERIFIED_CERTIFICATES'
:
True
})
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
'ENABLE_VERIFIED_CERTIFICATES'
:
True
})
...
@@ -231,6 +234,8 @@ class DashboardTest(ModuleStoreTestCase):
...
@@ -231,6 +234,8 @@ class DashboardTest(ModuleStoreTestCase):
self
.
_check_verification_status_on
(
'verified'
,
'You
\'
re enrolled as a verified student'
)
self
.
_check_verification_status_on
(
'verified'
,
'You
\'
re enrolled as a verified student'
)
self
.
_check_verification_status_on
(
'honor'
,
'You
\'
re enrolled as an honor code student'
)
self
.
_check_verification_status_on
(
'honor'
,
'You
\'
re enrolled as an honor code student'
)
self
.
_check_verification_status_on
(
'audit'
,
'You
\'
re auditing this course'
)
self
.
_check_verification_status_on
(
'audit'
,
'You
\'
re auditing this course'
)
self
.
_check_verification_status_on
(
'professional'
,
'You
\'
re enrolled as a professional education student'
)
self
.
_check_verification_status_on
(
'no-id-professional'
,
'You
\'
re enrolled as a professional education student'
)
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
def
_check_verification_status_off
(
self
,
mode
,
value
):
def
_check_verification_status_off
(
self
,
mode
,
value
):
...
...
common/djangoapps/student/views.py
View file @
4e454cca
...
@@ -909,9 +909,9 @@ def change_enrollment(request, check_access=True):
...
@@ -909,9 +909,9 @@ def change_enrollment(request, check_access=True):
# If we have more than one course mode or professional ed is enabled,
# If we have more than one course mode or professional ed is enabled,
# then send the user to the choose your track page.
# then send the user to the choose your track page.
# (In the case of professional ed, this will redirect to a page that
# (In the case of
no-id-professional/
professional ed, this will redirect to a page that
# funnels users directly into the verification / payment flow)
# funnels users directly into the verification / payment flow)
if
CourseMode
.
has_verified_mode
(
available_modes
):
if
CourseMode
.
has_verified_mode
(
available_modes
)
or
CourseMode
.
has_professional_mode
(
available_modes
)
:
return
HttpResponse
(
return
HttpResponse
(
reverse
(
"course_modes_choose"
,
kwargs
=
{
'course_id'
:
unicode
(
course_id
)})
reverse
(
"course_modes_choose"
,
kwargs
=
{
'course_id'
:
unicode
(
course_id
)})
)
)
...
...
lms/djangoapps/shoppingcart/models.py
View file @
4e454cca
...
@@ -1746,13 +1746,10 @@ class CertificateItem(OrderItem):
...
@@ -1746,13 +1746,10 @@ class CertificateItem(OrderItem):
self
.
course_enrollment
.
activate
()
self
.
course_enrollment
.
activate
()
def
additional_instruction_text
(
self
):
def
additional_instruction_text
(
self
):
refund_reminder
=
_
(
verification_reminder
=
""
"You have up to two weeks into the course to unenroll from the Verified Certificate option "
is_enrollment_mode_verified
=
self
.
course_enrollment
.
is_verified_enrollment
()
# pylint: disable=E1101
"and receive a full refund. To receive your refund, contact {billing_email}. "
"Please include your order number in your email. "
"Please do NOT include your credit card information."
)
.
format
(
billing_email
=
settings
.
PAYMENT_SUPPORT_EMAIL
)
if
is_enrollment_mode_verified
:
domain
=
microsite
.
get_value
(
'SITE_NAME'
,
settings
.
SITE_NAME
)
domain
=
microsite
.
get_value
(
'SITE_NAME'
,
settings
.
SITE_NAME
)
path
=
reverse
(
'verify_student_verify_later'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course_id
)})
path
=
reverse
(
'verify_student_verify_later'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course_id
)})
verification_url
=
"http://{domain}{path}"
.
format
(
domain
=
domain
,
path
=
path
)
verification_url
=
"http://{domain}{path}"
.
format
(
domain
=
domain
,
path
=
path
)
...
@@ -1761,6 +1758,15 @@ class CertificateItem(OrderItem):
...
@@ -1761,6 +1758,15 @@ class CertificateItem(OrderItem):
"If you haven't verified your identity yet, please start the verification process ({verification_url})."
"If you haven't verified your identity yet, please start the verification process ({verification_url})."
)
.
format
(
verification_url
=
verification_url
)
)
.
format
(
verification_url
=
verification_url
)
refund_reminder
=
_
(
"You have up to two weeks into the course to unenroll and receive a full refund."
"To receive your refund, contact {billing_email}. "
"Please include your order number in your email. "
"Please do NOT include your credit card information."
)
.
format
(
billing_email
=
settings
.
PAYMENT_SUPPORT_EMAIL
)
# Need this to be unicode in case the reminder strings
# Need this to be unicode in case the reminder strings
# have been translated and contain non-ASCII unicode
# have been translated and contain non-ASCII unicode
return
u"{verification_reminder} {refund_reminder}"
.
format
(
return
u"{verification_reminder} {refund_reminder}"
.
format
(
...
...
lms/djangoapps/shoppingcart/tests/test_models.py
View file @
4e454cca
...
@@ -801,6 +801,29 @@ class CertificateItemTest(ModuleStoreTestCase):
...
@@ -801,6 +801,29 @@ class CertificateItemTest(ModuleStoreTestCase):
ret_val
=
CourseEnrollment
.
unenroll
(
self
.
user
,
self
.
course_key
)
ret_val
=
CourseEnrollment
.
unenroll
(
self
.
user
,
self
.
course_key
)
self
.
assertFalse
(
ret_val
)
self
.
assertFalse
(
ret_val
)
def
test_no_id_prof_confirm_email
(
self
):
# Pay for a no-id-professional course
course_mode
=
CourseMode
(
course_id
=
self
.
course_key
,
mode_slug
=
"no-id-professional"
,
mode_display_name
=
"No Id Professional Cert"
,
min_price
=
self
.
cost
)
course_mode
.
save
()
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course_key
)
cart
=
Order
.
get_cart_for_user
(
user
=
self
.
user
)
CertificateItem
.
add_to_order
(
cart
,
self
.
course_key
,
self
.
cost
,
'no-id-professional'
)
# verify that we are still enrolled
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course_key
))
self
.
mock_tracker
.
reset_mock
()
cart
.
purchase
()
enrollment
=
CourseEnrollment
.
objects
.
get
(
user
=
self
.
user
,
course_id
=
self
.
course_key
)
self
.
assertEquals
(
enrollment
.
mode
,
u'no-id-professional'
)
# Check that the tax-deduction information appears in the confirmation email
self
.
assertEqual
(
len
(
mail
.
outbox
),
1
)
email
=
mail
.
outbox
[
0
]
self
.
assertEquals
(
'Order Payment Confirmation'
,
email
.
subject
)
self
.
assertNotIn
(
"If you haven't verified your identity yet, please start the verification process"
,
email
.
body
)
class
DonationTest
(
ModuleStoreTestCase
):
class
DonationTest
(
ModuleStoreTestCase
):
"""Tests for the donation order item type. """
"""Tests for the donation order item type. """
...
...
lms/djangoapps/student_account/test/test_views.py
View file @
4e454cca
...
@@ -298,7 +298,7 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase)
...
@@ -298,7 +298,7 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase)
]
]
self
.
_assert_third_party_auth_data
(
response
,
current_provider
,
expected_providers
)
self
.
_assert_third_party_auth_data
(
response
,
current_provider
,
expected_providers
)
@ddt.data
([],
[
"honor"
],
[
"honor"
,
"verified"
,
"audit"
],
[
"professional"
])
@ddt.data
([],
[
"honor"
],
[
"honor"
,
"verified"
,
"audit"
],
[
"professional"
]
,
[
"no-id-professional"
]
)
def
test_third_party_auth_course_id_verified
(
self
,
modes
):
def
test_third_party_auth_course_id_verified
(
self
,
modes
):
# Create a course with the specified course modes
# Create a course with the specified course modes
course
=
CourseFactory
.
create
()
course
=
CourseFactory
.
create
()
...
...
lms/djangoapps/verify_student/tests/test_views.py
View file @
4e454cca
...
@@ -98,6 +98,21 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
...
@@ -98,6 +98,21 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
])
])
self
.
_assert_upgrade_session_flag
(
False
)
self
.
_assert_upgrade_session_flag
(
False
)
@ddt.data
(
"no-id-professional"
)
def
test_start_flow_with_no_id_professional
(
self
,
course_mode
):
course
=
self
.
_create_course
(
course_mode
)
# by default enrollment is honor
self
.
_enroll
(
course
.
id
,
"honor"
)
response
=
self
.
_get_page
(
'verify_student_start_flow'
,
course
.
id
)
self
.
_assert_displayed_mode
(
response
,
course_mode
)
self
.
_assert_steps_displayed
(
response
,
PayAndVerifyView
.
PAYMENT_STEPS
,
PayAndVerifyView
.
MAKE_PAYMENT_STEP
)
self
.
_assert_messaging
(
response
,
PayAndVerifyView
.
FIRST_TIME_VERIFY_MSG
)
self
.
_assert_requirements_displayed
(
response
,
[])
@ddt.data
(
"expired"
,
"denied"
)
@ddt.data
(
"expired"
,
"denied"
)
def
test_start_flow_expired_or_denied_verification
(
self
,
verification_status
):
def
test_start_flow_expired_or_denied_verification
(
self
,
verification_status
):
course
=
self
.
_create_course
(
"verified"
)
course
=
self
.
_create_course
(
"verified"
)
...
@@ -121,7 +136,8 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
...
@@ -121,7 +136,8 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
(
"verified"
,
"submitted"
),
(
"verified"
,
"submitted"
),
(
"verified"
,
"approved"
),
(
"verified"
,
"approved"
),
(
"verified"
,
"error"
),
(
"verified"
,
"error"
),
(
"professional"
,
"submitted"
)
(
"professional"
,
"submitted"
),
(
"no-id-professional"
,
None
),
)
)
@ddt.unpack
@ddt.unpack
def
test_start_flow_already_verified
(
self
,
course_mode
,
verification_status
):
def
test_start_flow_already_verified
(
self
,
course_mode
,
verification_status
):
...
@@ -516,6 +532,14 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
...
@@ -516,6 +532,14 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
expected_status_code
=
404
expected_status_code
=
404
)
)
@ddt.data
([],
[
"no-id-professional"
,
"professional"
],
[
"honor"
,
"audit"
])
def
test_no_id_professional_entry_point
(
self
,
modes_available
):
course
=
self
.
_create_course
(
*
modes_available
)
if
"no-id-professional"
in
modes_available
or
"professional"
in
modes_available
:
self
.
_get_page
(
"verify_student_start_flow"
,
course
.
id
,
expected_status_code
=
200
)
else
:
self
.
_get_page
(
"verify_student_start_flow"
,
course
.
id
,
expected_status_code
=
404
)
@ddt.data
(
@ddt.data
(
"verify_student_start_flow"
,
"verify_student_start_flow"
,
"verify_student_verify_now"
,
"verify_student_verify_now"
,
...
@@ -647,7 +671,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
...
@@ -647,7 +671,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
modulestore
()
.
update_item
(
course
,
ModuleStoreEnum
.
UserID
.
test
)
modulestore
()
.
update_item
(
course
,
ModuleStoreEnum
.
UserID
.
test
)
for
course_mode
in
course_modes
:
for
course_mode
in
course_modes
:
min_price
=
(
self
.
MIN_PRICE
if
course_mode
!=
"honor"
else
0
)
min_price
=
(
0
if
course_mode
in
[
"honor"
,
"audit"
]
else
self
.
MIN_PRICE
)
CourseModeFactory
(
CourseModeFactory
(
course_id
=
course
.
id
,
course_id
=
course
.
id
,
mode_slug
=
course_mode
,
mode_slug
=
course_mode
,
...
@@ -826,8 +850,8 @@ class TestCreateOrder(ModuleStoreTestCase):
...
@@ -826,8 +850,8 @@ class TestCreateOrder(ModuleStoreTestCase):
self
.
user
=
UserFactory
.
create
(
username
=
"test"
,
password
=
"test"
)
self
.
user
=
UserFactory
.
create
(
username
=
"test"
,
password
=
"test"
)
self
.
course
=
CourseFactory
.
create
()
self
.
course
=
CourseFactory
.
create
()
for
mode
in
(
'audit'
,
'honor'
,
'verified'
):
for
mode
,
min_price
in
((
'audit'
,
0
),
(
'honor'
,
0
),
(
'verified'
,
100
)
):
CourseModeFactory
(
mode_slug
=
mode
,
course_id
=
self
.
course
.
id
)
CourseModeFactory
(
mode_slug
=
mode
,
course_id
=
self
.
course
.
id
,
min_price
=
min_price
)
self
.
client
.
login
(
username
=
"test"
,
password
=
"test"
)
self
.
client
.
login
(
username
=
"test"
,
password
=
"test"
)
def
test_create_order_already_verified
(
self
):
def
test_create_order_already_verified
(
self
):
...
@@ -838,6 +862,7 @@ class TestCreateOrder(ModuleStoreTestCase):
...
@@ -838,6 +862,7 @@ class TestCreateOrder(ModuleStoreTestCase):
url
=
reverse
(
'verify_student_create_order'
)
url
=
reverse
(
'verify_student_create_order'
)
params
=
{
params
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'course_id'
:
unicode
(
self
.
course
.
id
),
'contribution'
:
100
}
}
response
=
self
.
client
.
post
(
url
,
params
)
response
=
self
.
client
.
post
(
url
,
params
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
...
@@ -857,7 +882,7 @@ class TestCreateOrder(ModuleStoreTestCase):
...
@@ -857,7 +882,7 @@ class TestCreateOrder(ModuleStoreTestCase):
# Create a prof ed course
# Create a prof ed course
course
=
CourseFactory
.
create
()
course
=
CourseFactory
.
create
()
CourseModeFactory
(
mode_slug
=
"professional"
,
course_id
=
course
.
id
)
CourseModeFactory
(
mode_slug
=
"professional"
,
course_id
=
course
.
id
,
min_price
=
10
)
# Create an order for a prof ed course
# Create an order for a prof ed course
url
=
reverse
(
'verify_student_create_order'
)
url
=
reverse
(
'verify_student_create_order'
)
...
@@ -872,6 +897,45 @@ class TestCreateOrder(ModuleStoreTestCase):
...
@@ -872,6 +897,45 @@ class TestCreateOrder(ModuleStoreTestCase):
self
.
assertEqual
(
data
[
'merchant_defined_data1'
],
unicode
(
course
.
id
))
self
.
assertEqual
(
data
[
'merchant_defined_data1'
],
unicode
(
course
.
id
))
self
.
assertEqual
(
data
[
'merchant_defined_data2'
],
"professional"
)
self
.
assertEqual
(
data
[
'merchant_defined_data2'
],
"professional"
)
def
test_create_order_for_no_id_professional
(
self
):
# Create a no-id-professional ed course
course
=
CourseFactory
.
create
()
CourseModeFactory
(
mode_slug
=
"no-id-professional"
,
course_id
=
course
.
id
,
min_price
=
10
)
# Create an order for a prof ed course
url
=
reverse
(
'verify_student_create_order'
)
params
=
{
'course_id'
:
unicode
(
course
.
id
)
}
response
=
self
.
client
.
post
(
url
,
params
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Verify that the course ID and transaction type are included in "merchant-defined data"
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
[
'merchant_defined_data1'
],
unicode
(
course
.
id
))
self
.
assertEqual
(
data
[
'merchant_defined_data2'
],
"no-id-professional"
)
def
test_create_order_for_multiple_paid_modes
(
self
):
# Create a no-id-professional ed course
course
=
CourseFactory
.
create
()
CourseModeFactory
(
mode_slug
=
"no-id-professional"
,
course_id
=
course
.
id
,
min_price
=
10
)
CourseModeFactory
(
mode_slug
=
"professional"
,
course_id
=
course
.
id
,
min_price
=
10
)
# Create an order for a prof ed course
url
=
reverse
(
'verify_student_create_order'
)
params
=
{
'course_id'
:
unicode
(
course
.
id
)
}
response
=
self
.
client
.
post
(
url
,
params
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Verify that the course ID and transaction type are included in "merchant-defined data"
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
[
'merchant_defined_data1'
],
unicode
(
course
.
id
))
self
.
assertEqual
(
data
[
'merchant_defined_data2'
],
"no-id-professional"
)
def
test_create_order_set_donation_amount
(
self
):
def
test_create_order_set_donation_amount
(
self
):
# Verify the student so we don't need to submit photos
# Verify the student so we don't need to submit photos
self
.
_verify_student
()
self
.
_verify_student
()
...
@@ -959,7 +1023,7 @@ class TestCreateOrderView(ModuleStoreTestCase):
...
@@ -959,7 +1023,7 @@ class TestCreateOrderView(ModuleStoreTestCase):
photo_id_image
=
self
.
IMAGE_DATA
,
photo_id_image
=
self
.
IMAGE_DATA
,
expect_status_code
=
400
expect_status_code
=
400
)
)
self
.
assertIn
(
'This course doesn
\'
t support
verifie
d certificates'
,
response
.
content
)
self
.
assertIn
(
'This course doesn
\'
t support
pai
d certificates'
,
response
.
content
)
@patch.dict
(
settings
.
FEATURES
,
{
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
:
True
})
@patch.dict
(
settings
.
FEATURES
,
{
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
:
True
})
def
test_create_order_fail_with_get
(
self
):
def
test_create_order_fail_with_get
(
self
):
...
...
lms/djangoapps/verify_student/views.py
View file @
4e454cca
...
@@ -272,15 +272,16 @@ class PayAndVerifyView(View):
...
@@ -272,15 +272,16 @@ class PayAndVerifyView(View):
if
redirect_url
:
if
redirect_url
:
return
redirect
(
redirect_url
)
return
redirect
(
redirect_url
)
# Check that the course has an unexpired verified mode
expired_verified_course_mode
,
unexpired_paid_course_mode
=
self
.
_get_expired_verified_and_paid_mode
(
course_key
)
course_mode
,
expired_course_mode
=
self
.
_get_verified_modes_for_course
(
course_key
)
if
course_mode
is
not
None
:
# Check that the course has an unexpired paid mode
if
unexpired_paid_course_mode
is
not
None
:
if
CourseMode
.
is_verified_mode
(
unexpired_paid_course_mode
):
log
.
info
(
log
.
info
(
u"Entering verified workflow for user '
%
s', course '
%
s', with current step '
%
s'."
,
u"Entering verified workflow for user '
%
s', course '
%
s', with current step '
%
s'."
,
request
.
user
.
id
,
course_id
,
current_step
request
.
user
.
id
,
course_id
,
current_step
)
)
elif
expired_course_mode
is
not
None
:
elif
expired_
verified_
course_mode
is
not
None
:
# Check if there is an *expired* verified course mode;
# Check if there is an *expired* verified course mode;
# if so, we should show a message explaining that the verification
# if so, we should show a message explaining that the verification
# deadline has passed.
# deadline has passed.
...
@@ -288,16 +289,16 @@ class PayAndVerifyView(View):
...
@@ -288,16 +289,16 @@ class PayAndVerifyView(View):
context
=
{
context
=
{
'course'
:
course
,
'course'
:
course
,
'deadline'
:
(
'deadline'
:
(
get_default_time_display
(
expired_course_mode
.
expiration_datetime
)
get_default_time_display
(
expired_
verified_
course_mode
.
expiration_datetime
)
if
expired_course_mode
.
expiration_datetime
else
""
if
expired_
verified_
course_mode
.
expiration_datetime
else
""
)
)
}
}
return
render_to_response
(
"verify_student/missed_verification_deadline.html"
,
context
)
return
render_to_response
(
"verify_student/missed_verification_deadline.html"
,
context
)
else
:
else
:
# Otherwise, there has never been a verified mode,
# Otherwise, there has never been a verified
/paid
mode,
# so return a page not found response.
# so return a page not found response.
log
.
warn
(
log
.
warn
(
u"No
verified course mode found for course '
%
s' for verification
flow request"
,
u"No
paid/verified course mode found for course '
%
s' for verification/payment
flow request"
,
course_id
course_id
)
)
raise
Http404
raise
Http404
...
@@ -307,7 +308,9 @@ class PayAndVerifyView(View):
...
@@ -307,7 +308,9 @@ class PayAndVerifyView(View):
# with a paid course mode (such as "verified").
# with a paid course mode (such as "verified").
# For this reason, every paid user is enrolled, but not
# For this reason, every paid user is enrolled, but not
# every enrolled user is paid.
# every enrolled user is paid.
already_verified
=
self
.
_check_already_verified
(
request
.
user
)
# If the course mode is not verified(i.e only paid) then already_verified is always True
already_verified
=
self
.
_check_already_verified
(
request
.
user
)
\
if
CourseMode
.
is_verified_mode
(
unexpired_paid_course_mode
)
else
True
already_paid
,
is_enrolled
=
self
.
_check_enrollment
(
request
.
user
,
course_key
)
already_paid
,
is_enrolled
=
self
.
_check_enrollment
(
request
.
user
,
course_key
)
# Redirect the user to a more appropriate page if the
# Redirect the user to a more appropriate page if the
...
@@ -326,7 +329,8 @@ class PayAndVerifyView(View):
...
@@ -326,7 +329,8 @@ class PayAndVerifyView(View):
display_steps
=
self
.
_display_steps
(
display_steps
=
self
.
_display_steps
(
always_show_payment
,
always_show_payment
,
already_verified
,
already_verified
,
already_paid
already_paid
,
unexpired_paid_course_mode
)
)
requirements
=
self
.
_requirements
(
display_steps
,
request
.
user
.
is_active
)
requirements
=
self
.
_requirements
(
display_steps
,
request
.
user
.
is_active
)
...
@@ -371,7 +375,7 @@ class PayAndVerifyView(View):
...
@@ -371,7 +375,7 @@ class PayAndVerifyView(View):
'contribution_amount'
:
contribution_amount
,
'contribution_amount'
:
contribution_amount
,
'course'
:
course
,
'course'
:
course
,
'course_key'
:
unicode
(
course_key
),
'course_key'
:
unicode
(
course_key
),
'course_mode'
:
course_mode
,
'course_mode'
:
unexpired_paid_
course_mode
,
'courseware_url'
:
courseware_url
,
'courseware_url'
:
courseware_url
,
'current_step'
:
current_step
,
'current_step'
:
current_step
,
'disable_courseware_js'
:
True
,
'disable_courseware_js'
:
True
,
...
@@ -383,8 +387,8 @@ class PayAndVerifyView(View):
...
@@ -383,8 +387,8 @@ class PayAndVerifyView(View):
'requirements'
:
requirements
,
'requirements'
:
requirements
,
'user_full_name'
:
full_name
,
'user_full_name'
:
full_name
,
'verification_deadline'
:
(
'verification_deadline'
:
(
get_default_time_display
(
course_mode
.
expiration_datetime
)
get_default_time_display
(
unexpired_paid_
course_mode
.
expiration_datetime
)
if
course_mode
.
expiration_datetime
else
""
if
unexpired_paid_
course_mode
.
expiration_datetime
else
""
),
),
}
}
return
render_to_response
(
"verify_student/pay_and_verify.html"
,
context
)
return
render_to_response
(
"verify_student/pay_and_verify.html"
,
context
)
...
@@ -459,22 +463,33 @@ class PayAndVerifyView(View):
...
@@ -459,22 +463,33 @@ class PayAndVerifyView(View):
if
url
is
not
None
:
if
url
is
not
None
:
return
redirect
(
url
)
return
redirect
(
url
)
def
_get_
verified_modes_for_course
(
self
,
course_key
):
def
_get_
expired_verified_and_paid_mode
(
self
,
course_key
):
# pylint: disable=invalid-name
"""Retrieve
unexpired and expired verified modes
for a course.
"""Retrieve
expired verified mode and unexpired paid mode(with min_price>0)
for a course.
Arguments:
Arguments:
course_key (CourseKey): The location of the course.
course_key (CourseKey): The location of the course.
Returns:
Returns:
Tuple of `(
verified_mode, expired_verifie
d_mode)`. If provided,
Tuple of `(
expired_verified_mode, unexpired_pai
d_mode)`. If provided,
`
verified_mode` is an *un
expired* verified mode for the course.
`
expired_verified_mode` is an *
expired* verified mode for the course.
If provided, `
expired_verified_mode` is an *expired* verified
If provided, `
unexpired_paid_mode` is an *unexpired* paid(with min_price>0)
mode for the course. Either of these may be None.
mode for the course. Either of these may be None.
"""
"""
# Retrieve all the modes at once to reduce the number of database queries
# Retrieve all the modes at once to reduce the number of database queries
all_modes
,
unexpired_modes
=
CourseMode
.
all_and_unexpired_modes_for_courses
([
course_key
])
all_modes
,
unexpired_modes
=
CourseMode
.
all_and_unexpired_modes_for_courses
([
course_key
])
# Unexpired paid modes
unexpired_paid_modes
=
[
mode
for
mode
in
unexpired_modes
[
course_key
]
if
mode
.
min_price
]
if
len
(
unexpired_paid_modes
)
>
1
:
# There is more than one paid mode defined,
# so choose the first one.
log
.
warn
(
u"More than one paid modes are defined for course '
%
s' choosing the first one
%
s"
,
course_key
,
unexpired_paid_modes
[
0
]
)
unexpired_paid_mode
=
unexpired_paid_modes
[
0
]
if
unexpired_paid_modes
else
None
# Find an unexpired verified mode
# Find an unexpired verified mode
verified_mode
=
CourseMode
.
verified_mode_for_course
(
course_key
,
modes
=
unexpired_modes
[
course_key
])
verified_mode
=
CourseMode
.
verified_mode_for_course
(
course_key
,
modes
=
unexpired_modes
[
course_key
])
expired_verified_mode
=
None
expired_verified_mode
=
None
...
@@ -482,9 +497,9 @@ class PayAndVerifyView(View):
...
@@ -482,9 +497,9 @@ class PayAndVerifyView(View):
if
verified_mode
is
None
:
if
verified_mode
is
None
:
expired_verified_mode
=
CourseMode
.
verified_mode_for_course
(
course_key
,
modes
=
all_modes
[
course_key
])
expired_verified_mode
=
CourseMode
.
verified_mode_for_course
(
course_key
,
modes
=
all_modes
[
course_key
])
return
(
verified_mode
,
expired_verifie
d_mode
)
return
(
expired_verified_mode
,
unexpired_pai
d_mode
)
def
_display_steps
(
self
,
always_show_payment
,
already_verified
,
already_paid
):
def
_display_steps
(
self
,
always_show_payment
,
already_verified
,
already_paid
,
course_mode
):
"""Determine which steps to display to the user.
"""Determine which steps to display to the user.
Includes all steps by default, but removes steps
Includes all steps by default, but removes steps
...
@@ -508,7 +523,7 @@ class PayAndVerifyView(View):
...
@@ -508,7 +523,7 @@ class PayAndVerifyView(View):
display_steps
=
self
.
ALL_STEPS
display_steps
=
self
.
ALL_STEPS
remove_steps
=
set
()
remove_steps
=
set
()
if
already_verified
:
if
already_verified
or
not
CourseMode
.
is_verified_mode
(
course_mode
)
:
remove_steps
|=
set
(
self
.
VERIFICATION_STEPS
)
remove_steps
|=
set
(
self
.
VERIFICATION_STEPS
)
if
already_paid
and
not
always_show_payment
:
if
already_paid
and
not
always_show_payment
:
...
@@ -517,7 +532,6 @@ class PayAndVerifyView(View):
...
@@ -517,7 +532,6 @@ class PayAndVerifyView(View):
# The "make payment" step doubles as an intro step,
# The "make payment" step doubles as an intro step,
# so if we're showing the payment step, hide the intro step.
# so if we're showing the payment step, hide the intro step.
remove_steps
|=
set
([
self
.
INTRO_STEP
])
remove_steps
|=
set
([
self
.
INTRO_STEP
])
return
[
return
[
{
{
'name'
:
step
,
'name'
:
step
,
...
@@ -642,15 +656,21 @@ def create_order(request):
...
@@ -642,15 +656,21 @@ def create_order(request):
except
decimal
.
InvalidOperation
:
except
decimal
.
InvalidOperation
:
return
HttpResponseBadRequest
(
_
(
"Selected price is not valid number."
))
return
HttpResponseBadRequest
(
_
(
"Selected price is not valid number."
))
# prefer professional mode over verified_mode
current_mode
=
None
current_mode
=
CourseMode
.
verified_mode_for_course
(
course_id
)
paid_modes
=
CourseMode
.
paid_modes_for_course
(
course_id
)
# Check if there are more than 1 paid(mode with min_price>0 e.g verified/professional/no-id-professional) modes
# for course exist then choose the first one
if
paid_modes
:
if
len
(
paid_modes
)
>
1
:
log
.
warn
(
u"Multiple paid course modes found for course '
%
s' for create order request"
,
course_id
)
current_mode
=
paid_modes
[
0
]
#
make sure this course has a verifie
d mode
#
Make sure this course has a pai
d mode
if
not
current_mode
:
if
not
current_mode
:
log
.
warn
(
u"
Verification requested for course {course_id} without a verified mode."
.
format
(
course_id
=
course_id
)
)
log
.
warn
(
u"
Create order requested for course '
%
s' without a paid mode."
,
course_id
)
return
HttpResponseBadRequest
(
_
(
"This course doesn't support
verifie
d certificates"
))
return
HttpResponseBadRequest
(
_
(
"This course doesn't support
pai
d certificates"
))
if
current_mode
.
slug
==
'professional'
:
if
CourseMode
.
is_professional_mode
(
current_mode
)
:
amount
=
current_mode
.
min_price
amount
=
current_mode
.
min_price
if
amount
<
current_mode
.
min_price
:
if
amount
<
current_mode
.
min_price
:
...
...
lms/templates/dashboard/_dashboard_course_listing.html
View file @
4e454cca
...
@@ -6,6 +6,7 @@ from django.utils.translation import ungettext
...
@@ -6,6 +6,7 @@ from django.utils.translation import ungettext
from
django
.
core
.
urlresolvers
import
reverse
from
django
.
core
.
urlresolvers
import
reverse
from
markupsafe
import
escape
from
markupsafe
import
escape
from
courseware
.
courses
import
course_image_url
,
get_course_about_section
from
courseware
.
courses
import
course_image_url
,
get_course_about_section
from
course_modes
.
models
import
CourseMode
from
student
.
helpers
import
(
from
student
.
helpers
import
(
VERIFY_STATUS_NEED_TO_VERIFY
,
VERIFY_STATUS_NEED_TO_VERIFY
,
VERIFY_STATUS_SUBMITTED
,
VERIFY_STATUS_SUBMITTED
,
...
@@ -30,17 +31,14 @@ from student.helpers import (
...
@@ -30,17 +31,14 @@ from student.helpers import (
<li
class=
"course-item"
>
<li
class=
"course-item"
>
% if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'):
% if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'):
% if enrollment.mode == "verified":
<
%
course_verified_certs =
CourseMode.enrollment_mode_display(enrollment.mode,
verification_status
.
get
('
status
'))
%
>
% if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED]:
<
%
<
%
mode_class =
" verified"
%
>
mode_class =
course_verified_certs.get('display_mode',
'')
% else:
if
mode_class
!=
''
:
<
%
mode_class =
" honor"
%
>
mode_class =
' '
+
mode_class
;
% endif
%
>
% else:
<
%
mode_class =
" "
+
enrollment
.
mode
%
>
% endif
% else:
% else:
<
%
mode_class =
""
%
>
<
%
mode_class =
''
%
>
% endif
% endif
<article
class=
"course${mode_class}"
>
<article
class=
"course${mode_class}"
>
...
@@ -64,44 +62,15 @@ from student.helpers import (
...
@@ -64,44 +62,15 @@ from student.helpers import (
<img
src=
"${course_image_url(course)}"
alt=
"${_('{course_number} {course_name} Cover Image').format(course_number=course.number, course_name=course.display_name_with_default) | h}"
/>
<img
src=
"${course_image_url(course)}"
alt=
"${_('{course_number} {course_name} Cover Image').format(course_number=course.number, course_name=course.display_name_with_default) | h}"
/>
</div>
</div>
% endif
% endif
% if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'):
% if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'):
% if enrollment.mode == "verified":
<span
class=
"sts-enrollment"
title=
"${course_verified_certs.get('enrollment_title')}"
>
% if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED]:
<span
class=
"sts-enrollment"
title=
"${_("
Your
verification
is
pending
")}"
>
<span
class=
"label"
>
${_("Enrolled as: ")}
</span>
<span
class=
"label"
>
${_("Enrolled as: ")}
</span>
## Translators: This text describes that the student has enrolled for a Verified Certificate, but verification of identity is pending.
% if course_verified_certs.get('show_image'):
<img
class=
"deco-graphic"
src=
"${static.url('images/verified-ribbon.png')}"
alt=
"${_("
ID
verification
pending
")}"
/>
<img
class=
"deco-graphic"
src=
"${static.url('images/verified-ribbon.png')}"
alt=
"${course_verified_certs.get('image_alt')}"
/>
## Translators: The student is enrolled for a Verified Certificate, but verification of identity is pending.
<div
class=
"sts-enrollment-value"
>
${_("Verified: Pending Verification")}
</div>
</span>
% elif verification_status.get('status') == VERIFY_STATUS_APPROVED:
<span
class=
"sts-enrollment"
title=
"${_("
You
'
re
enrolled
as
a
verified
student
")}"
>
<span
class=
"label"
>
${_("Enrolled as: ")}
</span>
<img
class=
"deco-graphic"
src=
"${static.url('images/verified-ribbon.png')}"
alt=
"${_("
ID
Verified
Ribbon
/
Badge
")}"
/>
<div
class=
"sts-enrollment-value"
>
${_("Verified")}
</div>
</span>
% else:
<span
class=
"sts-enrollment"
title=
"${_("
You
'
re
enrolled
as
an
honor
code
student
")}"
>
<span
class=
"label"
>
${_("Enrolled as: ")}
</span>
<div
class=
"sts-enrollment-value"
>
${_("Honor Code")}
</div>
</span>
% endif
% endif
% elif enrollment.mode == "honor":
<div
class=
"sts-enrollment-value"
>
${course_verified_certs.get('enrollment_value')}
</div>
<span
class=
"sts-enrollment"
title=
"${_("
You
'
re
enrolled
as
an
honor
code
student
")}"
>
<span
class=
"label"
>
${_("Enrolled as: ")}
</span>
<div
class=
"sts-enrollment-value"
>
${_("Honor Code")}
</div>
</span>
</span>
% elif enrollment.mode == "audit":
<span
class=
"sts-enrollment"
title=
"${_("
You
'
re
auditing
this
course
")}"
>
<span
class=
"label"
>
${_("Enrolled as: ")}
</span>
<div
class=
"sts-enrollment-value"
>
${_("Auditing")}
</div>
</span>
% elif enrollment.mode == "professional":
<span
class=
"sts-enrollment"
title=
"${_("
You
'
re
enrolled
as
a
professional
education
student
")}"
>
<span
class=
"label"
>
${_("Enrolled as: ")}
</span>
<div
class=
"sts-enrollment-value"
>
${_("Professional Ed")}
</div>
</span>
% endif
% endif
% endif
<section
class=
"info"
>
<section
class=
"info"
>
...
...
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