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
79eea58b
Commit
79eea58b
authored
Nov 16, 2015
by
Sarina Canelake
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #10483 from mitocw/gdm_feature_ccx_api_#122
Enforced maximum amount of students for CCX
parents
32d0fc7c
64acf484
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
337 additions
and
191 deletions
+337
-191
lms/djangoapps/ccx/models.py
+10
-0
lms/djangoapps/ccx/tests/test_models.py
+9
-0
lms/djangoapps/ccx/tests/test_views.py
+196
-112
lms/djangoapps/ccx/views.py
+111
-78
lms/envs/aws.py
+1
-0
lms/envs/common.py
+7
-0
openedx/core/djangoapps/content/course_overviews/models.py
+3
-1
No files found.
lms/djangoapps/ccx/models.py
View file @
79eea58b
...
@@ -53,6 +53,16 @@ class CustomCourseForEdX(models.Model):
...
@@ -53,6 +53,16 @@ class CustomCourseForEdX(models.Model):
from
.overrides
import
get_override_for_ccx
from
.overrides
import
get_override_for_ccx
return
get_override_for_ccx
(
self
,
self
.
course
,
'due'
)
return
get_override_for_ccx
(
self
,
self
.
course
,
'due'
)
@lazy
def
max_student_enrollments_allowed
(
self
):
"""
Get the value of the override of the 'max_student_enrollments_allowed'
datetime for this CCX
"""
# avoid circular import problems
from
.overrides
import
get_override_for_ccx
return
get_override_for_ccx
(
self
,
self
.
course
,
'max_student_enrollments_allowed'
)
def
has_started
(
self
):
def
has_started
(
self
):
"""Return True if the CCX start date is in the past"""
"""Return True if the CCX start date is in the past"""
return
datetime
.
now
(
UTC
())
>
self
.
start
return
datetime
.
now
(
UTC
())
>
self
.
start
...
...
lms/djangoapps/ccx/tests/test_models.py
View file @
79eea58b
...
@@ -200,3 +200,12 @@ class TestCCX(ModuleStoreTestCase):
...
@@ -200,3 +200,12 @@ class TestCCX(ModuleStoreTestCase):
self
.
assertEqual
(
expected
,
actual
)
self
.
assertEqual
(
expected
,
actual
)
actual
=
self
.
ccx
.
end_datetime_text
(
'DATE_TIME'
)
# pylint: disable=no-member
actual
=
self
.
ccx
.
end_datetime_text
(
'DATE_TIME'
)
# pylint: disable=no-member
self
.
assertEqual
(
expected
,
actual
)
self
.
assertEqual
(
expected
,
actual
)
def
test_ccx_max_student_enrollment_correct
(
self
):
"""
Verify the override value for max_student_enrollments_allowed
"""
expected
=
200
self
.
set_ccx_override
(
'max_student_enrollments_allowed'
,
expected
)
actual
=
self
.
ccx
.
max_student_enrollments_allowed
# pylint: disable=no-member
self
.
assertEqual
(
expected
,
actual
)
lms/djangoapps/ccx/tests/test_views.py
View file @
79eea58b
...
@@ -15,6 +15,9 @@ from courseware.courses import get_course_by_id
...
@@ -15,6 +15,9 @@ from courseware.courses import get_course_by_id
from
courseware.tests.factories
import
StudentModuleFactory
from
courseware.tests.factories
import
StudentModuleFactory
from
courseware.tests.helpers
import
LoginEnrollmentTestCase
from
courseware.tests.helpers
import
LoginEnrollmentTestCase
from
courseware.tabs
import
get_course_tab_list
from
courseware.tabs
import
get_course_tab_list
from
django.conf
import
settings
from
django.core.exceptions
import
ValidationError
from
django.core.validators
import
validate_email
from
django.core.urlresolvers
import
reverse
,
resolve
from
django.core.urlresolvers
import
reverse
,
resolve
from
django.utils.timezone
import
UTC
from
django.utils.timezone
import
UTC
from
django.test.utils
import
override_settings
from
django.test.utils
import
override_settings
...
@@ -114,6 +117,17 @@ def setup_students_and_grades(context):
...
@@ -114,6 +117,17 @@ def setup_students_and_grades(context):
)
)
def
is_email
(
identifier
):
"""
Checks if an `identifier` string is a valid email
"""
try
:
validate_email
(
identifier
)
except
ValidationError
:
return
False
return
True
@attr
(
'shard_1'
)
@attr
(
'shard_1'
)
@ddt.ddt
@ddt.ddt
class
TestCoachDashboard
(
SharedModuleStoreTestCase
,
LoginEnrollmentTestCase
):
class
TestCoachDashboard
(
SharedModuleStoreTestCase
,
LoginEnrollmentTestCase
):
...
@@ -179,11 +193,12 @@ class TestCoachDashboard(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
...
@@ -179,11 +193,12 @@ class TestCoachDashboard(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
role
=
CourseCcxCoachRole
(
self
.
course
.
id
)
role
=
CourseCcxCoachRole
(
self
.
course
.
id
)
role
.
add_users
(
self
.
coach
)
role
.
add_users
(
self
.
coach
)
def
make_ccx
(
self
):
def
make_ccx
(
self
,
max_students_allowed
=
settings
.
CCX_MAX_STUDENTS_ALLOWED
):
"""
"""
create ccx
create ccx
"""
"""
ccx
=
CcxFactory
(
course_id
=
self
.
course
.
id
,
coach
=
self
.
coach
)
ccx
=
CcxFactory
(
course_id
=
self
.
course
.
id
,
coach
=
self
.
coach
)
override_field_for_ccx
(
ccx
,
self
.
course
,
'max_student_enrollments_allowed'
,
max_students_allowed
)
return
ccx
return
ccx
def
get_outbox
(
self
):
def
get_outbox
(
self
):
...
@@ -270,6 +285,11 @@ class TestCoachDashboard(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
...
@@ -270,6 +285,11 @@ class TestCoachDashboard(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
self
.
coach
,
course_key
))
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
self
.
coach
,
course_key
))
self
.
assertTrue
(
re
.
search
(
'id="ccx-schedule"'
,
response
.
content
))
self
.
assertTrue
(
re
.
search
(
'id="ccx-schedule"'
,
response
.
content
))
# check if the max amount of student that can be enrolled has been overridden
ccx
=
CustomCourseForEdX
.
objects
.
get
()
course_enrollments
=
get_override_for_ccx
(
ccx
,
self
.
course
,
'max_student_enrollments_allowed'
)
self
.
assertEqual
(
course_enrollments
,
settings
.
CCX_MAX_STUDENTS_ALLOWED
)
@SharedModuleStoreTestCase.modifies_courseware
@SharedModuleStoreTestCase.modifies_courseware
@patch
(
'ccx.views.render_to_response'
,
intercept_renderer
)
@patch
(
'ccx.views.render_to_response'
,
intercept_renderer
)
@patch
(
'ccx.views.TODAY'
)
@patch
(
'ccx.views.TODAY'
)
...
@@ -430,8 +450,20 @@ class TestCoachDashboard(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
...
@@ -430,8 +450,20 @@ class TestCoachDashboard(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
)
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
def
test_enroll_member_student
(
self
):
@ddt.data
(
"""enroll a list of students who are members of the class
(
'ccx_invite'
,
True
,
1
,
'student-ids'
,
(
'enrollment-button'
,
'Enroll'
)),
(
'ccx_invite'
,
False
,
0
,
'student-ids'
,
(
'enrollment-button'
,
'Enroll'
)),
(
'ccx_manage_student'
,
True
,
1
,
'student-id'
,
(
'student-action'
,
'add'
)),
(
'ccx_manage_student'
,
False
,
0
,
'student-id'
,
(
'student-action'
,
'add'
)),
)
@ddt.unpack
def
test_enroll_member_student
(
self
,
view_name
,
send_email
,
outbox_count
,
student_form_input_name
,
button_tuple
):
"""
Tests the enrollment of a list of students who are members
of the class.
It tests 2 different views that use slightly different parameters,
but that perform the same task.
"""
"""
self
.
make_coach
()
self
.
make_coach
()
ccx
=
self
.
make_ccx
()
ccx
=
self
.
make_ccx
()
...
@@ -441,204 +473,256 @@ class TestCoachDashboard(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
...
@@ -441,204 +473,256 @@ class TestCoachDashboard(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
self
.
assertEqual
(
outbox
,
[])
self
.
assertEqual
(
outbox
,
[])
url
=
reverse
(
url
=
reverse
(
'ccx_invite'
,
view_name
,
kwargs
=
{
'course_id'
:
CCXLocator
.
from_course_locator
(
self
.
course
.
id
,
ccx
.
id
)}
kwargs
=
{
'course_id'
:
CCXLocator
.
from_course_locator
(
self
.
course
.
id
,
ccx
.
id
)}
)
)
data
=
{
data
=
{
'enrollment-button'
:
'Enroll'
,
button_tuple
[
0
]:
button_tuple
[
1
],
'student-ids'
:
u','
.
join
([
student
.
email
,
]),
# pylint: disable=no-member
student_form_input_name
:
u','
.
join
([
student
.
email
,
]),
# pylint: disable=no-member
'email-students'
:
'Notify-students-by-email'
,
}
}
if
send_email
:
data
[
'email-students'
]
=
'Notify-students-by-email'
response
=
self
.
client
.
post
(
url
,
data
=
data
,
follow
=
True
)
response
=
self
.
client
.
post
(
url
,
data
=
data
,
follow
=
True
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# we were redirected to our current location
# we were redirected to our current location
self
.
assertEqual
(
len
(
response
.
redirect_chain
),
1
)
self
.
assertEqual
(
len
(
response
.
redirect_chain
),
1
)
self
.
assertIn
(
302
,
response
.
redirect_chain
[
0
])
self
.
assertIn
(
302
,
response
.
redirect_chain
[
0
])
self
.
assertEqual
(
len
(
outbox
),
1
)
self
.
assertEqual
(
len
(
outbox
),
outbox_count
)
self
.
assertIn
(
student
.
email
,
outbox
[
0
]
.
recipients
())
# pylint: disable=no-member
if
send_email
:
self
.
assertIn
(
student
.
email
,
outbox
[
0
]
.
recipients
())
# pylint: disable=no-member
# a CcxMembership exists for this student
# a CcxMembership exists for this student
self
.
assertTrue
(
self
.
assertTrue
(
CourseEnrollment
.
objects
.
filter
(
course_id
=
self
.
course
.
id
,
user
=
student
)
.
exists
()
CourseEnrollment
.
objects
.
filter
(
course_id
=
self
.
course
.
id
,
user
=
student
)
.
exists
()
)
)
def
test_unenroll_member_student
(
self
):
def
test_ccx_invite_enroll_up_to_limit
(
self
):
"""unenroll a list of students who are members of the class
"""
"""
self
.
make_coach
()
Enrolls a list of students up to the enrollment limit.
ccx
=
self
.
make_ccx
()
course_key
=
CCXLocator
.
from_course_locator
(
self
.
course
.
id
,
ccx
.
id
)
enrollment
=
CourseEnrollmentFactory
(
course_id
=
course_key
)
student
=
enrollment
.
user
outbox
=
self
.
get_outbox
()
self
.
assertEqual
(
outbox
,
[])
This test is specific to one of the enrollment views: the reason is because
the view used in this test can perform bulk enrollments.
"""
self
.
make_coach
()
# create ccx and limit the maximum amount of students that can be enrolled to 2
ccx
=
self
.
make_ccx
(
max_students_allowed
=
2
)
ccx_course_key
=
CCXLocator
.
from_course_locator
(
self
.
course
.
id
,
ccx
.
id
)
# create some users
students
=
[
UserFactory
.
create
(
is_staff
=
False
)
for
_
in
range
(
3
)
]
url
=
reverse
(
url
=
reverse
(
'ccx_invite'
,
'ccx_invite'
,
kwargs
=
{
'course_id'
:
course_key
}
kwargs
=
{
'course_id'
:
c
cx_c
ourse_key
}
)
)
data
=
{
data
=
{
'enrollment-button'
:
'Unenroll'
,
'enrollment-button'
:
'Enroll'
,
'student-ids'
:
u','
.
join
([
student
.
email
,
]),
# pylint: disable=no-member
'student-ids'
:
u','
.
join
([
student
.
email
for
student
in
students
]),
# pylint: disable=no-member
'email-students'
:
'Notify-students-by-email'
,
}
}
response
=
self
.
client
.
post
(
url
,
data
=
data
,
follow
=
True
)
response
=
self
.
client
.
post
(
url
,
data
=
data
,
follow
=
True
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# we were redirected to our current location
# a CcxMembership exists for the first two students but not the third
self
.
assertEqual
(
len
(
response
.
redirect_chain
),
1
)
self
.
assertTrue
(
self
.
assertIn
(
302
,
response
.
redirect_chain
[
0
])
CourseEnrollment
.
objects
.
filter
(
course_id
=
ccx_course_key
,
user
=
students
[
0
])
.
exists
()
self
.
assertEqual
(
len
(
outbox
),
1
)
)
self
.
assertIn
(
student
.
email
,
outbox
[
0
]
.
recipients
())
# pylint: disable=no-member
self
.
assertTrue
(
CourseEnrollment
.
objects
.
filter
(
course_id
=
ccx_course_key
,
user
=
students
[
1
])
.
exists
()
)
self
.
assertFalse
(
CourseEnrollment
.
objects
.
filter
(
course_id
=
ccx_course_key
,
user
=
students
[
2
])
.
exists
()
)
def
test_enroll_non_user_student
(
self
):
def
test_manage_student_enrollment_limit
(
self
):
"""enroll a list of students who are not users yet
"""
"""
test_email
=
"nobody@nowhere.com"
Enroll students up to the enrollment limit.
self
.
make_coach
()
ccx
=
self
.
make_ccx
()
course_key
=
CCXLocator
.
from_course_locator
(
self
.
course
.
id
,
ccx
.
id
)
outbox
=
self
.
get_outbox
()
self
.
assertEqual
(
outbox
,
[])
This test is specific to one of the enrollment views: the reason is because
the view used in this test cannot perform bulk enrollments.
"""
students_limit
=
1
self
.
make_coach
()
ccx
=
self
.
make_ccx
(
max_students_allowed
=
students_limit
)
ccx_course_key
=
CCXLocator
.
from_course_locator
(
self
.
course
.
id
,
ccx
.
id
)
students
=
[
UserFactory
.
create
(
is_staff
=
False
)
for
_
in
range
(
2
)
]
url
=
reverse
(
url
=
reverse
(
'ccx_
invite
'
,
'ccx_
manage_student
'
,
kwargs
=
{
'course_id'
:
course_key
}
kwargs
=
{
'course_id'
:
CCXLocator
.
from_course_locator
(
self
.
course
.
id
,
ccx
.
id
)
}
)
)
# enroll the first student
data
=
{
data
=
{
'enrollment-button'
:
'Enroll'
,
'student-action'
:
'add'
,
'student-ids'
:
u','
.
join
([
test_email
,
]),
'student-id'
:
u','
.
join
([
students
[
0
]
.
email
,
]),
# pylint: disable=no-member
'email-students'
:
'Notify-students-by-email'
,
}
}
response
=
self
.
client
.
post
(
url
,
data
=
data
,
follow
=
True
)
response
=
self
.
client
.
post
(
url
,
data
=
data
,
follow
=
True
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# we were redirected to our current location
# a CcxMembership exists for this student
self
.
assertEqual
(
len
(
response
.
redirect_chain
),
1
)
self
.
assertIn
(
302
,
response
.
redirect_chain
[
0
])
self
.
assertEqual
(
len
(
outbox
),
1
)
self
.
assertIn
(
test_email
,
outbox
[
0
]
.
recipients
())
self
.
assertTrue
(
self
.
assertTrue
(
CourseEnrollmentAllowed
.
objects
.
filter
(
CourseEnrollment
.
objects
.
filter
(
course_id
=
ccx_course_key
,
user
=
students
[
0
])
.
exists
()
course_id
=
course_key
,
email
=
test_email
)
)
.
exists
()
# try to enroll the second student without success
# enroll the first student
data
=
{
'student-action'
:
'add'
,
'student-id'
:
u','
.
join
([
students
[
1
]
.
email
,
]),
# pylint: disable=no-member
}
response
=
self
.
client
.
post
(
url
,
data
=
data
,
follow
=
True
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# a CcxMembership does not exist for this student
self
.
assertFalse
(
CourseEnrollment
.
objects
.
filter
(
course_id
=
ccx_course_key
,
user
=
students
[
1
])
.
exists
()
)
)
error_message
=
'The course is full: the limit is {students_limit}'
.
format
(
students_limit
=
students_limit
)
self
.
assertContains
(
response
,
error_message
,
status_code
=
200
)
def
test_unenroll_non_user_student
(
self
):
@ddt.data
(
"""unenroll a list of students who are not users yet
(
'ccx_invite'
,
True
,
1
,
'student-ids'
,
(
'enrollment-button'
,
'Unenroll'
)),
(
'ccx_invite'
,
False
,
0
,
'student-ids'
,
(
'enrollment-button'
,
'Unenroll'
)),
(
'ccx_manage_student'
,
True
,
1
,
'student-id'
,
(
'student-action'
,
'revoke'
)),
(
'ccx_manage_student'
,
False
,
0
,
'student-id'
,
(
'student-action'
,
'revoke'
)),
)
@ddt.unpack
def
test_unenroll_member_student
(
self
,
view_name
,
send_email
,
outbox_count
,
student_form_input_name
,
button_tuple
):
"""
Tests the unenrollment of a list of students who are members of the class.
It tests 2 different views that use slightly different parameters,
but that perform the same task.
"""
"""
test_email
=
"nobody@nowhere.com"
self
.
make_coach
()
self
.
make_coach
()
course
=
CourseFactory
.
create
()
ccx
=
self
.
make_ccx
()
ccx
=
self
.
make_ccx
()
course_key
=
CCXLocator
.
from_course_locator
(
course
.
id
,
ccx
.
id
)
course_key
=
CCXLocator
.
from_course_locator
(
self
.
course
.
id
,
ccx
.
id
)
enrollment
=
CourseEnrollmentFactory
(
course_id
=
course_key
)
student
=
enrollment
.
user
outbox
=
self
.
get_outbox
()
outbox
=
self
.
get_outbox
()
CourseEnrollmentAllowed
(
course_id
=
course_key
,
email
=
test_email
)
self
.
assertEqual
(
outbox
,
[])
self
.
assertEqual
(
outbox
,
[])
url
=
reverse
(
url
=
reverse
(
'ccx_invite'
,
view_name
,
kwargs
=
{
'course_id'
:
course_key
}
kwargs
=
{
'course_id'
:
course_key
}
)
)
data
=
{
data
=
{
'enrollment-button'
:
'Unenroll'
,
button_tuple
[
0
]:
button_tuple
[
1
],
'student-ids'
:
u','
.
join
([
test_email
,
]),
student_form_input_name
:
u','
.
join
([
student
.
email
,
]),
# pylint: disable=no-member
'email-students'
:
'Notify-students-by-email'
,
}
}
if
send_email
:
data
[
'email-students'
]
=
'Notify-students-by-email'
response
=
self
.
client
.
post
(
url
,
data
=
data
,
follow
=
True
)
response
=
self
.
client
.
post
(
url
,
data
=
data
,
follow
=
True
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# we were redirected to our current location
# we were redirected to our current location
self
.
assertEqual
(
len
(
response
.
redirect_chain
),
1
)
self
.
assertEqual
(
len
(
response
.
redirect_chain
),
1
)
self
.
assertIn
(
302
,
response
.
redirect_chain
[
0
])
self
.
assertIn
(
302
,
response
.
redirect_chain
[
0
])
self
.
assertEqual
(
len
(
outbox
),
outbox_count
)
if
send_email
:
self
.
assertIn
(
student
.
email
,
outbox
[
0
]
.
recipients
())
# pylint: disable=no-member
# a CcxMembership does not exists for this student
self
.
assertFalse
(
self
.
assertFalse
(
CourseEnrollmentAllowed
.
objects
.
filter
(
CourseEnrollment
.
objects
.
filter
(
course_id
=
self
.
course
.
id
,
user
=
student
)
.
exists
()
course_id
=
course_key
,
email
=
test_email
)
.
exists
()
)
)
@ddt.data
(
"dummy_student_id"
,
"xyz@gmail.com"
)
@ddt.data
(
def
test_manage_add_single_invalid_student
(
self
,
student_id
):
(
'ccx_invite'
,
True
,
1
,
'student-ids'
,
(
'enrollment-button'
,
'Enroll'
),
'nobody@nowhere.com'
),
"""enroll a single non valid student
(
'ccx_invite'
,
False
,
0
,
'student-ids'
,
(
'enrollment-button'
,
'Enroll'
),
'nobody@nowhere.com'
),
(
'ccx_invite'
,
True
,
0
,
'student-ids'
,
(
'enrollment-button'
,
'Enroll'
),
'nobody'
),
(
'ccx_invite'
,
False
,
0
,
'student-ids'
,
(
'enrollment-button'
,
'Enroll'
),
'nobody'
),
(
'ccx_manage_student'
,
True
,
0
,
'student-id'
,
(
'student-action'
,
'add'
),
'dummy_student_id'
),
(
'ccx_manage_student'
,
False
,
0
,
'student-id'
,
(
'student-action'
,
'add'
),
'dummy_student_id'
),
(
'ccx_manage_student'
,
True
,
1
,
'student-id'
,
(
'student-action'
,
'add'
),
'xyz@gmail.com'
),
(
'ccx_manage_student'
,
False
,
0
,
'student-id'
,
(
'student-action'
,
'add'
),
'xyz@gmail.com'
),
)
@ddt.unpack
def
test_enroll_non_user_student
(
self
,
view_name
,
send_email
,
outbox_count
,
student_form_input_name
,
button_tuple
,
identifier
):
"""
"""
self
.
make_coach
()
Tests the enrollment of a list of students who are not users yet.
ccx
=
self
.
make_ccx
()
course_key
=
CCXLocator
.
from_course_locator
(
self
.
course
.
id
,
ccx
.
id
)
url
=
reverse
(
'ccx_manage_student'
,
kwargs
=
{
'course_id'
:
course_key
}
)
redirect_url
=
reverse
(
'ccx_coach_dashboard'
,
kwargs
=
{
'course_id'
:
course_key
}
)
data
=
{
'student-action'
:
'add'
,
'student-id'
:
u','
.
join
([
student_id
,
]),
# pylint: disable=no-member
}
response
=
self
.
client
.
post
(
url
,
data
=
data
,
follow
=
True
)
error_message
=
'Could not find a user with name or email "{student_id}" '
.
format
(
student_id
=
student_id
)
self
.
assertContains
(
response
,
error_message
,
status_code
=
200
)
# we were redirected to our current location
It tests 2 different views that use slightly different parameters,
self
.
assertRedirects
(
response
,
redirect_url
,
status_code
=
302
)
but that perform the same task.
def
test_manage_add_single_student
(
self
):
"""enroll a single student who is a member of the class already
"""
"""
self
.
make_coach
()
self
.
make_coach
()
ccx
=
self
.
make_ccx
()
ccx
=
self
.
make_ccx
()
course_key
=
CCXLocator
.
from_course_locator
(
self
.
course
.
id
,
ccx
.
id
)
course_key
=
CCXLocator
.
from_course_locator
(
self
.
course
.
id
,
ccx
.
id
)
enrollment
=
CourseEnrollmentFactory
(
course_id
=
course_key
)
student
=
enrollment
.
user
# no emails have been sent so far
outbox
=
self
.
get_outbox
()
outbox
=
self
.
get_outbox
()
self
.
assertEqual
(
outbox
,
[])
self
.
assertEqual
(
outbox
,
[])
url
=
reverse
(
url
=
reverse
(
'ccx_manage_student'
,
view_name
,
kwargs
=
{
'course_id'
:
course_key
}
kwargs
=
{
'course_id'
:
course_key
}
)
)
data
=
{
data
=
{
'student-action'
:
'add'
,
button_tuple
[
0
]:
button_tuple
[
1
]
,
'student-id'
:
u','
.
join
([
student
.
email
,
]),
# pylint: disable=no-member
student_form_input_name
:
u','
.
join
([
identifier
,
]),
}
}
if
send_email
:
data
[
'email-students'
]
=
'Notify-students-by-email'
response
=
self
.
client
.
post
(
url
,
data
=
data
,
follow
=
True
)
response
=
self
.
client
.
post
(
url
,
data
=
data
,
follow
=
True
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# we were redirected to our current location
# we were redirected to our current location
self
.
assertEqual
(
len
(
response
.
redirect_chain
),
1
)
self
.
assertEqual
(
len
(
response
.
redirect_chain
),
1
)
self
.
assertIn
(
302
,
response
.
redirect_chain
[
0
])
self
.
assertIn
(
302
,
response
.
redirect_chain
[
0
])
self
.
assertEqual
(
outbox
,
[])
self
.
assertEqual
(
len
(
outbox
),
outbox_count
)
# a CcxMembership exists for this student
self
.
assertTrue
(
# some error messages are returned for one of the views only
CourseEnrollment
.
objects
.
filter
(
course_id
=
course_key
,
user
=
student
)
.
exists
()
if
view_name
==
'ccx_manage_student'
and
not
is_email
(
identifier
):
)
error_message
=
'Could not find a user with name or email "{identifier}" '
.
format
(
identifier
=
identifier
)
self
.
assertContains
(
response
,
error_message
,
status_code
=
200
)
def
test_manage_remove_single_student
(
self
):
if
is_email
(
identifier
):
"""unenroll a single student who is a member of the class already
if
send_email
:
self
.
assertIn
(
identifier
,
outbox
[
0
]
.
recipients
())
self
.
assertTrue
(
CourseEnrollmentAllowed
.
objects
.
filter
(
course_id
=
course_key
,
email
=
identifier
)
.
exists
()
)
else
:
self
.
assertFalse
(
CourseEnrollmentAllowed
.
objects
.
filter
(
course_id
=
course_key
,
email
=
identifier
)
.
exists
()
)
@ddt.data
(
(
'ccx_invite'
,
True
,
0
,
'student-ids'
,
(
'enrollment-button'
,
'Unenroll'
),
'nobody@nowhere.com'
),
(
'ccx_invite'
,
False
,
0
,
'student-ids'
,
(
'enrollment-button'
,
'Unenroll'
),
'nobody@nowhere.com'
),
(
'ccx_invite'
,
True
,
0
,
'student-ids'
,
(
'enrollment-button'
,
'Unenroll'
),
'nobody'
),
(
'ccx_invite'
,
False
,
0
,
'student-ids'
,
(
'enrollment-button'
,
'Unenroll'
),
'nobody'
),
)
@ddt.unpack
def
test_unenroll_non_user_student
(
self
,
view_name
,
send_email
,
outbox_count
,
student_form_input_name
,
button_tuple
,
identifier
):
"""
Unenroll a list of students who are not users yet
"""
"""
self
.
make_coach
()
self
.
make_coach
()
course
=
CourseFactory
.
create
()
ccx
=
self
.
make_ccx
()
ccx
=
self
.
make_ccx
()
course_key
=
CCXLocator
.
from_course_locator
(
self
.
course
.
id
,
ccx
.
id
)
course_key
=
CCXLocator
.
from_course_locator
(
course
.
id
,
ccx
.
id
)
enrollment
=
CourseEnrollmentFactory
(
course_id
=
course_key
)
student
=
enrollment
.
user
# no emails have been sent so far
outbox
=
self
.
get_outbox
()
outbox
=
self
.
get_outbox
()
CourseEnrollmentAllowed
(
course_id
=
course_key
,
email
=
identifier
)
self
.
assertEqual
(
outbox
,
[])
self
.
assertEqual
(
outbox
,
[])
url
=
reverse
(
url
=
reverse
(
'ccx_manage_student'
,
view_name
,
kwargs
=
{
'course_id'
:
CCXLocator
.
from_course_locator
(
self
.
course
.
id
,
ccx
.
id
)
}
kwargs
=
{
'course_id'
:
course_key
}
)
)
data
=
{
data
=
{
'student-action'
:
'revoke'
,
button_tuple
[
0
]:
button_tuple
[
1
]
,
'student-id'
:
u','
.
join
([
student
.
email
,
]),
# pylint: disable=no-member
student_form_input_name
:
u','
.
join
([
identifier
,
]),
}
}
if
send_email
:
data
[
'email-students'
]
=
'Notify-students-by-email'
response
=
self
.
client
.
post
(
url
,
data
=
data
,
follow
=
True
)
response
=
self
.
client
.
post
(
url
,
data
=
data
,
follow
=
True
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# we were redirected to our current location
# we were redirected to our current location
self
.
assertEqual
(
len
(
response
.
redirect_chain
),
1
)
self
.
assertEqual
(
len
(
response
.
redirect_chain
),
1
)
self
.
assertIn
(
302
,
response
.
redirect_chain
[
0
])
self
.
assertIn
(
302
,
response
.
redirect_chain
[
0
])
self
.
assertEqual
(
outbox
,
[])
self
.
assertEqual
(
len
(
outbox
),
outbox_count
)
self
.
assertFalse
(
CourseEnrollmentAllowed
.
objects
.
filter
(
course_id
=
course_key
,
email
=
identifier
)
.
exists
()
)
GET_CHILDREN
=
XModuleMixin
.
get_children
GET_CHILDREN
=
XModuleMixin
.
get_children
...
...
lms/djangoapps/ccx/views.py
View file @
79eea58b
...
@@ -12,6 +12,7 @@ from contextlib import contextmanager
...
@@ -12,6 +12,7 @@ from contextlib import contextmanager
from
copy
import
deepcopy
from
copy
import
deepcopy
from
cStringIO
import
StringIO
from
cStringIO
import
StringIO
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.http
import
(
from
django.http
import
(
HttpResponse
,
HttpResponse
,
...
@@ -35,6 +36,7 @@ from courseware.model_data import FieldDataCache
...
@@ -35,6 +36,7 @@ from courseware.model_data import FieldDataCache
from
courseware.module_render
import
get_module_for_descriptor
from
courseware.module_render
import
get_module_for_descriptor
from
edxmako.shortcuts
import
render_to_response
from
edxmako.shortcuts
import
render_to_response
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
ccx_keys.locator
import
CCXLocator
from
ccx_keys.locator
import
CCXLocator
from
student.roles
import
CourseCcxCoachRole
from
student.roles
import
CourseCcxCoachRole
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
...
@@ -59,6 +61,13 @@ log = logging.getLogger(__name__)
...
@@ -59,6 +61,13 @@ log = logging.getLogger(__name__)
TODAY
=
datetime
.
datetime
.
today
# for patching in tests
TODAY
=
datetime
.
datetime
.
today
# for patching in tests
class
CCXUserValidationException
(
Exception
):
"""
Custom Exception for validation of users in CCX
"""
pass
def
coach_dashboard
(
view
):
def
coach_dashboard
(
view
):
"""
"""
View decorator which enforces that the user have the CCX coach role on the
View decorator which enforces that the user have the CCX coach role on the
...
@@ -171,6 +180,9 @@ def create_ccx(request, course, ccx=None):
...
@@ -171,6 +180,9 @@ def create_ccx(request, course, ccx=None):
override_field_for_ccx
(
ccx
,
course
,
'start'
,
start
)
override_field_for_ccx
(
ccx
,
course
,
'start'
,
start
)
override_field_for_ccx
(
ccx
,
course
,
'due'
,
None
)
override_field_for_ccx
(
ccx
,
course
,
'due'
,
None
)
# Enforce a static limit for the maximum amount of students that can be enrolled
override_field_for_ccx
(
ccx
,
course
,
'max_student_enrollments_allowed'
,
settings
.
CCX_MAX_STUDENTS_ALLOWED
)
# Hide anything that can show up in the schedule
# Hide anything that can show up in the schedule
hidden
=
'visible_to_staff_only'
hidden
=
'visible_to_staff_only'
for
chapter
in
course
.
get_children
():
for
chapter
in
course
.
get_children
():
...
@@ -407,6 +419,90 @@ def ccx_schedule(request, course, ccx=None): # pylint: disable=unused-argument
...
@@ -407,6 +419,90 @@ def ccx_schedule(request, course, ccx=None): # pylint: disable=unused-argument
return
HttpResponse
(
json_schedule
,
mimetype
=
'application/json'
)
return
HttpResponse
(
json_schedule
,
mimetype
=
'application/json'
)
def
get_valid_student_email
(
identifier
):
"""
Helper function to get an user email from an identifier and validate it.
In the UI a Coach can enroll users using both an email and an username.
This function takes care of:
- in case the identifier is an username, extracting the user object from
the DB and then the associated email
- validating the email
Arguments:
identifier (str): Username or email of the user to enroll
Returns:
str: A validated email for the user to enroll
Raises:
CCXUserValidationException: if the username is not found or the email
is not valid.
"""
user
=
email
=
None
try
:
user
=
get_student_from_identifier
(
identifier
)
except
User
.
DoesNotExist
:
email
=
identifier
else
:
email
=
user
.
email
try
:
validate_email
(
email
)
except
ValidationError
:
raise
CCXUserValidationException
(
'Could not find a user with name or email "{0}" '
.
format
(
identifier
))
return
email
def
_ccx_students_enrrolling_center
(
action
,
identifiers
,
email_students
,
course_key
,
email_params
):
"""
Function to enroll/add or unenroll/revoke students.
This function exists for backwards compatibility: in CCX there are
two different views to manage students that used to implement
a different logic. Now the logic has been reconciled at the point that
this function can be used by both.
The two different views can be merged after some UI refactoring.
Arguments:
action (str): type of action to perform (add, Enroll, revoke, Unenroll)
identifiers (list): list of students username/email
email_students (bool): Flag to send an email to students
course_key (CCXLocator): a CCX course key
email_params (dict): dictionary of settings for the email to be sent
Returns:
list: list of error
"""
errors
=
[]
if
action
==
'Enroll'
or
action
==
'add'
:
ccx_course_overview
=
CourseOverview
.
get_from_id
(
course_key
)
for
identifier
in
identifiers
:
if
CourseEnrollment
.
objects
.
is_course_full
(
ccx_course_overview
):
error
=
(
'The course is full: the limit is {0}'
.
format
(
ccx_course_overview
.
max_student_enrollments_allowed
))
log
.
info
(
"
%
s"
,
error
)
errors
.
append
(
error
)
break
try
:
email
=
get_valid_student_email
(
identifier
)
except
CCXUserValidationException
as
exp
:
log
.
info
(
"
%
s"
,
exp
)
errors
.
append
(
"{0}"
.
format
(
exp
))
continue
enroll_email
(
course_key
,
email
,
auto_enroll
=
True
,
email_students
=
email_students
,
email_params
=
email_params
)
elif
action
==
'Unenroll'
or
action
==
'revoke'
:
for
identifier
in
identifiers
:
try
:
email
=
get_valid_student_email
(
identifier
)
except
CCXUserValidationException
as
exp
:
log
.
info
(
"
%
s"
,
exp
)
errors
.
append
(
"{0}"
.
format
(
exp
))
continue
unenroll_email
(
course_key
,
email
,
email_students
=
email_students
,
email_params
=
email_params
)
return
errors
@ensure_csrf_cookie
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@coach_dashboard
@coach_dashboard
...
@@ -420,99 +516,36 @@ def ccx_invite(request, course, ccx=None):
...
@@ -420,99 +516,36 @@ def ccx_invite(request, course, ccx=None):
action
=
request
.
POST
.
get
(
'enrollment-button'
)
action
=
request
.
POST
.
get
(
'enrollment-button'
)
identifiers_raw
=
request
.
POST
.
get
(
'student-ids'
)
identifiers_raw
=
request
.
POST
.
get
(
'student-ids'
)
identifiers
=
_split_input_list
(
identifiers_raw
)
identifiers
=
_split_input_list
(
identifiers_raw
)
auto_enroll
=
True
email_students
=
'email-students'
in
request
.
POST
email_students
=
True
if
'email-students'
in
request
.
POST
else
False
course_key
=
CCXLocator
.
from_course_locator
(
course
.
id
,
ccx
.
id
)
for
identifier
in
identifiers
:
email_params
=
get_email_params
(
course
,
auto_enroll
=
True
,
course_key
=
course_key
,
display_name
=
ccx
.
display_name
)
user
=
None
email
=
None
try
:
user
=
get_student_from_identifier
(
identifier
)
except
User
.
DoesNotExist
:
email
=
identifier
else
:
email
=
user
.
email
try
:
validate_email
(
email
)
course_key
=
CCXLocator
.
from_course_locator
(
course
.
id
,
ccx
.
id
)
email_params
=
get_email_params
(
course
,
auto_enroll
,
course_key
=
course_key
,
display_name
=
ccx
.
display_name
)
if
action
==
'Enroll'
:
enroll_email
(
course_key
,
email
,
auto_enroll
=
auto_enroll
,
email_students
=
email_students
,
email_params
=
email_params
)
if
action
==
"Unenroll"
:
unenroll_email
(
course_key
,
email
,
email_students
=
email_students
,
email_params
=
email_params
)
except
ValidationError
:
log
.
info
(
'Invalid user name or email when trying to invite students:
%
s'
,
email
)
url
=
reverse
(
'ccx_coach_dashboard'
,
kwargs
=
{
'course_id'
:
CCXLocator
.
from_course_locator
(
course
.
id
,
ccx
.
id
)}
)
return
redirect
(
url
)
def
validate_student_email
(
email
):
_ccx_students_enrrolling_center
(
action
,
identifiers
,
email_students
,
course_key
,
email_params
)
"""
validate student's email id
"""
error_message
=
None
try
:
validate_email
(
email
)
except
ValidationError
:
log
.
info
(
'Invalid user name or email when trying to enroll student:
%
s'
,
email
)
if
email
:
error_message
=
_
(
'Could not find a user with name or email "{email}" '
)
.
format
(
email
=
email
)
else
:
error_message
=
_
(
'Please enter a valid username or email.'
)
return
error_message
url
=
reverse
(
'ccx_coach_dashboard'
,
kwargs
=
{
'course_id'
:
course_key
})
return
redirect
(
url
)
@ensure_csrf_cookie
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@coach_dashboard
@coach_dashboard
def
ccx_student_management
(
request
,
course
,
ccx
=
None
):
def
ccx_student_management
(
request
,
course
,
ccx
=
None
):
"""Manage the enrollment of individual students in a CCX
"""
Manage the enrollment of individual students in a CCX
"""
"""
if
not
ccx
:
if
not
ccx
:
raise
Http404
raise
Http404
action
=
request
.
POST
.
get
(
'student-action'
,
None
)
action
=
request
.
POST
.
get
(
'student-action'
,
None
)
student_id
=
request
.
POST
.
get
(
'student-id'
,
''
)
student_id
=
request
.
POST
.
get
(
'student-id'
,
''
)
user
=
email
=
None
email_students
=
'email-students'
in
request
.
POST
error_message
=
""
identifiers
=
[
student_id
]
course_key
=
CCXLocator
.
from_course_locator
(
course
.
id
,
ccx
.
id
)
course_key
=
CCXLocator
.
from_course_locator
(
course
.
id
,
ccx
.
id
)
try
:
email_params
=
get_email_params
(
course
,
auto_enroll
=
True
,
course_key
=
course_key
,
display_name
=
ccx
.
display_name
)
user
=
get_student_from_identifier
(
student_id
)
except
User
.
DoesNotExist
:
errors
=
_ccx_students_enrrolling_center
(
action
,
identifiers
,
email_students
,
course_key
,
email_params
)
email
=
student_id
error_message
=
validate_student_email
(
email
)
for
error_message
in
errors
:
if
email
and
not
error_message
:
error_message
=
_
(
'Could not find a user with name or email "{email}" '
)
.
format
(
email
=
email
)
else
:
email
=
user
.
email
error_message
=
validate_student_email
(
email
)
if
error_message
is
None
:
if
action
==
'add'
:
# by decree, no emails sent to students added this way
# by decree, any students added this way are auto_enrolled
enroll_email
(
course_key
,
email
,
auto_enroll
=
True
,
email_students
=
False
)
elif
action
==
'revoke'
:
unenroll_email
(
course_key
,
email
,
email_students
=
False
)
else
:
messages
.
error
(
request
,
error_message
)
messages
.
error
(
request
,
error_message
)
url
=
reverse
(
'ccx_coach_dashboard'
,
kwargs
=
{
'course_id'
:
course_key
})
url
=
reverse
(
'ccx_coach_dashboard'
,
kwargs
=
{
'course_id'
:
course_key
})
...
...
lms/envs/aws.py
View file @
79eea58b
...
@@ -669,6 +669,7 @@ if FEATURES.get('CUSTOM_COURSES_EDX'):
...
@@ -669,6 +669,7 @@ if FEATURES.get('CUSTOM_COURSES_EDX'):
FIELD_OVERRIDE_PROVIDERS
+=
(
FIELD_OVERRIDE_PROVIDERS
+=
(
'ccx.overrides.CustomCoursesForEdxOverrideProvider'
,
'ccx.overrides.CustomCoursesForEdxOverrideProvider'
,
)
)
CCX_MAX_STUDENTS_ALLOWED
=
ENV_TOKENS
.
get
(
'CCX_MAX_STUDENTS_ALLOWED'
,
CCX_MAX_STUDENTS_ALLOWED
)
##### Individual Due Date Extensions #####
##### Individual Due Date Extensions #####
if
FEATURES
.
get
(
'INDIVIDUAL_DUE_DATES'
):
if
FEATURES
.
get
(
'INDIVIDUAL_DUE_DATES'
):
...
...
lms/envs/common.py
View file @
79eea58b
...
@@ -2705,3 +2705,10 @@ PROCTORING_BACKEND_PROVIDER = {
...
@@ -2705,3 +2705,10 @@ PROCTORING_BACKEND_PROVIDER = {
'options'
:
{},
'options'
:
{},
}
}
PROCTORING_SETTINGS
=
{}
PROCTORING_SETTINGS
=
{}
#### Custom Courses for EDX (CCX) configuration
# This is an arbitrary hard limit.
# The reason we introcuced this number is because we do not want the CCX
# to compete with the MOOC.
CCX_MAX_STUDENTS_ALLOWED
=
200
openedx/core/djangoapps/content/course_overviews/models.py
View file @
79eea58b
...
@@ -109,12 +109,14 @@ class CourseOverview(TimeStampedModel):
...
@@ -109,12 +109,14 @@ class CourseOverview(TimeStampedModel):
display_name
=
course
.
display_name
display_name
=
course
.
display_name
start
=
course
.
start
start
=
course
.
start
end
=
course
.
end
end
=
course
.
end
max_student_enrollments_allowed
=
course
.
max_student_enrollments_allowed
if
isinstance
(
course
.
id
,
CCXLocator
):
if
isinstance
(
course
.
id
,
CCXLocator
):
from
ccx.utils
import
get_ccx_from_ccx_locator
# pylint: disable=import-error
from
ccx.utils
import
get_ccx_from_ccx_locator
# pylint: disable=import-error
ccx
=
get_ccx_from_ccx_locator
(
course
.
id
)
ccx
=
get_ccx_from_ccx_locator
(
course
.
id
)
display_name
=
ccx
.
display_name
display_name
=
ccx
.
display_name
start
=
ccx
.
start
start
=
ccx
.
start
end
=
ccx
.
due
end
=
ccx
.
due
max_student_enrollments_allowed
=
ccx
.
max_student_enrollments_allowed
return
cls
(
return
cls
(
version
=
cls
.
VERSION
,
version
=
cls
.
VERSION
,
...
@@ -150,7 +152,7 @@ class CourseOverview(TimeStampedModel):
...
@@ -150,7 +152,7 @@ class CourseOverview(TimeStampedModel):
enrollment_end
=
course
.
enrollment_end
,
enrollment_end
=
course
.
enrollment_end
,
enrollment_domain
=
course
.
enrollment_domain
,
enrollment_domain
=
course
.
enrollment_domain
,
invitation_only
=
course
.
invitation_only
,
invitation_only
=
course
.
invitation_only
,
max_student_enrollments_allowed
=
course
.
max_student_enrollments_allowed
,
max_student_enrollments_allowed
=
max_student_enrollments_allowed
,
)
)
@classmethod
@classmethod
...
...
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