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
511e6536
Commit
511e6536
authored
Jun 13, 2017
by
rabia23
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' into ri/EDUCATOR-394-disable-self-generation-certificates
parents
a8913f9a
54eb9e84
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
227 additions
and
40 deletions
+227
-40
cms/djangoapps/contentstore/management/commands/delete_course.py
+26
-2
cms/djangoapps/contentstore/management/commands/tests/test_delete_course.py
+26
-0
cms/djangoapps/contentstore/tests/test_contentstore.py
+26
-6
cms/djangoapps/contentstore/tests/test_course_listing.py
+2
-2
cms/djangoapps/contentstore/tests/test_users_default_role.py
+4
-4
cms/djangoapps/contentstore/utils.py
+25
-9
cms/djangoapps/contentstore/views/course.py
+42
-10
cms/envs/common.py
+1
-1
cms/static/js/views/modals/move_xblock_modal.js
+4
-0
cms/static/sass/elements/_modal-window.scss
+5
-2
cms/static/sass/views/_dashboard.scss
+12
-0
cms/templates/index.html
+25
-1
common/djangoapps/course_modes/models.py
+13
-2
lms/envs/aws.py
+3
-0
lms/envs/common.py
+12
-0
requirements/edx/base.txt
+1
-1
No files found.
cms/djangoapps/contentstore/management/commands/delete_course.py
View file @
511e6536
...
@@ -11,7 +11,7 @@ from django.core.management.base import BaseCommand, CommandError
...
@@ -11,7 +11,7 @@ from django.core.management.base import BaseCommand, CommandError
from
opaque_keys
import
InvalidKeyError
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
contentstore.utils
import
delete_course
_and_groups
from
contentstore.utils
import
delete_course
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
...
@@ -21,12 +21,36 @@ from .prompt import query_yes_no
...
@@ -21,12 +21,36 @@ from .prompt import query_yes_no
class
Command
(
BaseCommand
):
class
Command
(
BaseCommand
):
"""
"""
Delete a MongoDB backed course
Delete a MongoDB backed course
Example usage:
$ ./manage.py cms delete_course 'course-v1:edX+DemoX+Demo_Course' --settings=devstack
$ ./manage.py cms delete_course 'course-v1:edX+DemoX+Demo_Course' --keep-instructors --settings=devstack
Note:
keep-instructors option is added in effort to delete duplicate courses safely.
There happens to be courses with difference of casing in ids, for example
course-v1:DartmouthX+DART.ENGL.01.X+2016_T1 is a duplicate of course-v1:DartmouthX+DART.ENGL.01.x+2016_T1
(Note the differene in 'x' of course number). These two are independent courses in MongoDB.
Current MYSQL setup is case-insensitive which essentially means there are not
seperate entries (in all course related mysql tables, but here we are concerned about accesses)
for duplicate courses.
This option will make us able to delete course (duplicate one) from
mongo while perserving course's related access data in mysql.
"""
"""
help
=
'''Delete a MongoDB backed course'''
help
=
'''Delete a MongoDB backed course'''
def
add_arguments
(
self
,
parser
):
def
add_arguments
(
self
,
parser
):
"""
Add arguments to the command parser.
"""
parser
.
add_argument
(
'course_key'
,
help
=
"ID of the course to delete."
)
parser
.
add_argument
(
'course_key'
,
help
=
"ID of the course to delete."
)
parser
.
add_argument
(
'--keep-instructors'
,
action
=
'store_true'
,
default
=
False
,
help
=
'Do not remove permissions of users and groups for course'
,
)
def
handle
(
self
,
*
args
,
**
options
):
def
handle
(
self
,
*
args
,
**
options
):
try
:
try
:
course_key
=
CourseKey
.
from_string
(
options
[
'course_key'
])
course_key
=
CourseKey
.
from_string
(
options
[
'course_key'
])
...
@@ -39,5 +63,5 @@ class Command(BaseCommand):
...
@@ -39,5 +63,5 @@ class Command(BaseCommand):
print
'Going to delete the
%
s course from DB....'
%
options
[
'course_key'
]
print
'Going to delete the
%
s course from DB....'
%
options
[
'course_key'
]
if
query_yes_no
(
"Deleting course {0}. Confirm?"
.
format
(
course_key
),
default
=
"no"
):
if
query_yes_no
(
"Deleting course {0}. Confirm?"
.
format
(
course_key
),
default
=
"no"
):
if
query_yes_no
(
"Are you sure. This action cannot be undone!"
,
default
=
"no"
):
if
query_yes_no
(
"Are you sure. This action cannot be undone!"
,
default
=
"no"
):
delete_course
_and_groups
(
course_key
,
ModuleStoreEnum
.
UserID
.
mgmt_command
)
delete_course
(
course_key
,
ModuleStoreEnum
.
UserID
.
mgmt_command
,
options
[
'keep_instructors'
]
)
print
"Deleted course {}"
.
format
(
course_key
)
print
"Deleted course {}"
.
format
(
course_key
)
cms/djangoapps/contentstore/management/commands/tests/test_delete_course.py
View file @
511e6536
...
@@ -6,9 +6,11 @@ import mock
...
@@ -6,9 +6,11 @@ import mock
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
django.core.management
import
call_command
,
CommandError
from
django.core.management
import
call_command
,
CommandError
from
django.contrib.auth.models
import
User
from
contentstore.tests.utils
import
CourseTestCase
from
contentstore.tests.utils
import
CourseTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
student.roles
import
CourseInstructorRole
class
DeleteCourseTest
(
CourseTestCase
):
class
DeleteCourseTest
(
CourseTestCase
):
...
@@ -60,3 +62,27 @@ class DeleteCourseTest(CourseTestCase):
...
@@ -60,3 +62,27 @@ class DeleteCourseTest(CourseTestCase):
patched_yes_no
.
return_value
=
True
patched_yes_no
.
return_value
=
True
call_command
(
'delete_course'
,
'TestX/TS01/2015_Q1'
)
call_command
(
'delete_course'
,
'TestX/TS01/2015_Q1'
)
self
.
assertIsNone
(
modulestore
()
.
get_course
(
SlashSeparatedCourseKey
(
"TestX"
,
"TS01"
,
"2015_Q1"
)))
self
.
assertIsNone
(
modulestore
()
.
get_course
(
SlashSeparatedCourseKey
(
"TestX"
,
"TS01"
,
"2015_Q1"
)))
def
test_course_deletion_with_keep_instructors
(
self
):
"""
Tests that deleting course with keep-instructors option do not remove instructors from course.
"""
instructor_user
=
User
.
objects
.
create
(
username
=
'test_instructor'
,
email
=
'test_email@example.com'
)
self
.
assertIsNotNone
(
instructor_user
)
# Add and verify instructor role for the course
instructor_role
=
CourseInstructorRole
(
self
.
course
.
id
)
instructor_role
.
add_users
(
instructor_user
)
self
.
assertTrue
(
instructor_role
.
has_user
(
instructor_user
))
# Verify the course we are about to delete exists in the modulestore
self
.
assertIsNotNone
(
modulestore
()
.
get_course
(
self
.
course
.
id
))
with
mock
.
patch
(
self
.
YESNO_PATCH_LOCATION
,
return_value
=
True
):
call_command
(
'delete_course'
,
'TestX/TS01/2015_Q1'
,
'--keep-instructors'
)
self
.
assertIsNone
(
modulestore
()
.
get_course
(
self
.
course
.
id
))
self
.
assertTrue
(
instructor_role
.
has_user
(
instructor_user
))
cms/djangoapps/contentstore/tests/test_contentstore.py
View file @
511e6536
...
@@ -26,7 +26,7 @@ from path import Path as path
...
@@ -26,7 +26,7 @@ from path import Path as path
from
common.test.utils
import
XssTestMixin
from
common.test.utils
import
XssTestMixin
from
contentstore.tests.utils
import
AjaxEnabledTestClient
,
CourseTestCase
,
get_url
,
parse_json
from
contentstore.tests.utils
import
AjaxEnabledTestClient
,
CourseTestCase
,
get_url
,
parse_json
from
contentstore.utils
import
delete_course
_and_groups
,
reverse_course_url
,
reverse_url
from
contentstore.utils
import
delete_course
,
reverse_course_url
,
reverse_url
from
contentstore.views.component
import
ADVANCED_COMPONENT_TYPES
from
contentstore.views.component
import
ADVANCED_COMPONENT_TYPES
from
course_action_state.managers
import
CourseActionStateItemNotFoundError
from
course_action_state.managers
import
CourseActionStateItemNotFoundError
from
course_action_state.models
import
CourseRerunState
,
CourseRerunUIStateManager
from
course_action_state.models
import
CourseRerunState
,
CourseRerunUIStateManager
...
@@ -1223,7 +1223,7 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
...
@@ -1223,7 +1223,7 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
test_course_data
=
self
.
assert_created_course
(
number_suffix
=
uuid4
()
.
hex
)
test_course_data
=
self
.
assert_created_course
(
number_suffix
=
uuid4
()
.
hex
)
course_id
=
_get_course_id
(
self
.
store
,
test_course_data
)
course_id
=
_get_course_id
(
self
.
store
,
test_course_data
)
self
.
assertTrue
(
are_permissions_roles_seeded
(
course_id
))
self
.
assertTrue
(
are_permissions_roles_seeded
(
course_id
))
delete_course
_and_groups
(
course_id
,
self
.
user
.
id
)
delete_course
(
course_id
,
self
.
user
.
id
)
# should raise an exception for checking permissions on deleted course
# should raise an exception for checking permissions on deleted course
with
self
.
assertRaises
(
ItemNotFoundError
):
with
self
.
assertRaises
(
ItemNotFoundError
):
are_permissions_roles_seeded
(
course_id
)
are_permissions_roles_seeded
(
course_id
)
...
@@ -1235,7 +1235,7 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
...
@@ -1235,7 +1235,7 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
# unseed the forums for the first course
# unseed the forums for the first course
course_id
=
_get_course_id
(
self
.
store
,
test_course_data
)
course_id
=
_get_course_id
(
self
.
store
,
test_course_data
)
delete_course
_and_groups
(
course_id
,
self
.
user
.
id
)
delete_course
(
course_id
,
self
.
user
.
id
)
# should raise an exception for checking permissions on deleted course
# should raise an exception for checking permissions on deleted course
with
self
.
assertRaises
(
ItemNotFoundError
):
with
self
.
assertRaises
(
ItemNotFoundError
):
are_permissions_roles_seeded
(
course_id
)
are_permissions_roles_seeded
(
course_id
)
...
@@ -1255,7 +1255,7 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
...
@@ -1255,7 +1255,7 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
course_id
))
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
course_id
))
self
.
assertTrue
(
self
.
user
.
roles
.
filter
(
name
=
"Student"
,
course_id
=
course_id
))
self
.
assertTrue
(
self
.
user
.
roles
.
filter
(
name
=
"Student"
,
course_id
=
course_id
))
delete_course
_and_groups
(
course_id
,
self
.
user
.
id
)
delete_course
(
course_id
,
self
.
user
.
id
)
# check that user's enrollment for this course is not deleted
# check that user's enrollment for this course is not deleted
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
course_id
))
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
course_id
))
# check that user has form role "Student" for this course even after deleting it
# check that user has form role "Student" for this course even after deleting it
...
@@ -1277,7 +1277,7 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
...
@@ -1277,7 +1277,7 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
self
.
assertGreater
(
len
(
instructor_role
.
users_with_role
()),
0
)
self
.
assertGreater
(
len
(
instructor_role
.
users_with_role
()),
0
)
# Now delete course and check that user not in instructor groups of this course
# Now delete course and check that user not in instructor groups of this course
delete_course
_and_groups
(
course_id
,
self
.
user
.
id
)
delete_course
(
course_id
,
self
.
user
.
id
)
# Update our cached user since its roles have changed
# Update our cached user since its roles have changed
self
.
user
=
User
.
objects
.
get_by_natural_key
(
self
.
user
.
natural_key
()[
0
])
self
.
user
=
User
.
objects
.
get_by_natural_key
(
self
.
user
.
natural_key
()[
0
])
...
@@ -1285,6 +1285,26 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
...
@@ -1285,6 +1285,26 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
self
.
assertFalse
(
instructor_role
.
has_user
(
self
.
user
))
self
.
assertFalse
(
instructor_role
.
has_user
(
self
.
user
))
self
.
assertEqual
(
len
(
instructor_role
.
users_with_role
()),
0
)
self
.
assertEqual
(
len
(
instructor_role
.
users_with_role
()),
0
)
def
test_delete_course_with_keep_instructors
(
self
):
"""
Tests that when you delete a course with 'keep_instructors',
it does not remove any permissions of users/groups from the course
"""
test_course_data
=
self
.
assert_created_course
(
number_suffix
=
uuid4
()
.
hex
)
course_id
=
_get_course_id
(
self
.
store
,
test_course_data
)
# Add and verify instructor role for the course
instructor_role
=
CourseInstructorRole
(
course_id
)
instructor_role
.
add_users
(
self
.
user
)
self
.
assertTrue
(
instructor_role
.
has_user
(
self
.
user
))
delete_course
(
course_id
,
self
.
user
.
id
,
keep_instructors
=
True
)
# Update our cached user so if any change in roles can be captured
self
.
user
=
User
.
objects
.
get_by_natural_key
(
self
.
user
.
natural_key
()[
0
])
self
.
assertTrue
(
instructor_role
.
has_user
(
self
.
user
))
def
test_create_course_after_delete
(
self
):
def
test_create_course_after_delete
(
self
):
"""
"""
Test that course creation works after deleting a course with the same URL
Test that course creation works after deleting a course with the same URL
...
@@ -1292,7 +1312,7 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
...
@@ -1292,7 +1312,7 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
test_course_data
=
self
.
assert_created_course
()
test_course_data
=
self
.
assert_created_course
()
course_id
=
_get_course_id
(
self
.
store
,
test_course_data
)
course_id
=
_get_course_id
(
self
.
store
,
test_course_data
)
delete_course
_and_groups
(
course_id
,
self
.
user
.
id
)
delete_course
(
course_id
,
self
.
user
.
id
)
self
.
assert_created_course
()
self
.
assert_created_course
()
...
...
cms/djangoapps/contentstore/tests/test_course_listing.py
View file @
511e6536
...
@@ -15,7 +15,7 @@ from opaque_keys.edx.locations import CourseLocator
...
@@ -15,7 +15,7 @@ from opaque_keys.edx.locations import CourseLocator
from
common.test.utils
import
XssTestMixin
from
common.test.utils
import
XssTestMixin
from
contentstore.tests.utils
import
AjaxEnabledTestClient
from
contentstore.tests.utils
import
AjaxEnabledTestClient
from
contentstore.utils
import
delete_course
_and_groups
from
contentstore.utils
import
delete_course
from
contentstore.views.course
import
(
from
contentstore.views.course
import
(
AccessListFallback
,
AccessListFallback
,
_accessible_courses_iter
,
_accessible_courses_iter
,
...
@@ -298,7 +298,7 @@ class TestCourseListing(ModuleStoreTestCase, XssTestMixin):
...
@@ -298,7 +298,7 @@ class TestCourseListing(ModuleStoreTestCase, XssTestMixin):
self
.
assertEqual
(
courses_list
,
courses_list_by_groups
)
self
.
assertEqual
(
courses_list
,
courses_list_by_groups
)
# now delete this course and re-add user to instructor group of this course
# now delete this course and re-add user to instructor group of this course
delete_course
_and_groups
(
course_key
,
self
.
user
.
id
)
delete_course
(
course_key
,
self
.
user
.
id
)
CourseInstructorRole
(
course_key
)
.
add_users
(
self
.
user
)
CourseInstructorRole
(
course_key
)
.
add_users
(
self
.
user
)
...
...
cms/djangoapps/contentstore/tests/test_users_default_role.py
View file @
511e6536
...
@@ -3,7 +3,7 @@ Unit tests for checking default forum role "Student" of a user when he creates a
...
@@ -3,7 +3,7 @@ Unit tests for checking default forum role "Student" of a user when he creates a
after deleting it creates same course again
after deleting it creates same course again
"""
"""
from
contentstore.tests.utils
import
AjaxEnabledTestClient
from
contentstore.tests.utils
import
AjaxEnabledTestClient
from
contentstore.utils
import
delete_course
_and_groups
,
reverse_url
from
contentstore.utils
import
delete_course
,
reverse_url
from
courseware.tests.factories
import
UserFactory
from
courseware.tests.factories
import
UserFactory
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
...
@@ -60,7 +60,7 @@ class TestUsersDefaultRole(ModuleStoreTestCase):
...
@@ -60,7 +60,7 @@ class TestUsersDefaultRole(ModuleStoreTestCase):
# check that user has his default "Student" forum role for this course
# check that user has his default "Student" forum role for this course
self
.
assertTrue
(
self
.
user
.
roles
.
filter
(
name
=
"Student"
,
course_id
=
self
.
course_key
))
self
.
assertTrue
(
self
.
user
.
roles
.
filter
(
name
=
"Student"
,
course_id
=
self
.
course_key
))
delete_course
_and_groups
(
self
.
course_key
,
self
.
user
.
id
)
delete_course
(
self
.
course_key
,
self
.
user
.
id
)
# check that user's enrollment for this course is not deleted
# check that user's enrollment for this course is not deleted
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course_key
))
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course_key
))
...
@@ -78,7 +78,7 @@ class TestUsersDefaultRole(ModuleStoreTestCase):
...
@@ -78,7 +78,7 @@ class TestUsersDefaultRole(ModuleStoreTestCase):
self
.
assertTrue
(
self
.
user
.
roles
.
filter
(
name
=
"Student"
,
course_id
=
self
.
course_key
))
self
.
assertTrue
(
self
.
user
.
roles
.
filter
(
name
=
"Student"
,
course_id
=
self
.
course_key
))
# delete this course and recreate this course with same user
# delete this course and recreate this course with same user
delete_course
_and_groups
(
self
.
course_key
,
self
.
user
.
id
)
delete_course
(
self
.
course_key
,
self
.
user
.
id
)
resp
=
self
.
_create_course_with_given_location
(
self
.
course_key
)
resp
=
self
.
_create_course_with_given_location
(
self
.
course_key
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
...
@@ -96,7 +96,7 @@ class TestUsersDefaultRole(ModuleStoreTestCase):
...
@@ -96,7 +96,7 @@ class TestUsersDefaultRole(ModuleStoreTestCase):
# check that user has enrollment and his default "Student" forum role for this course
# check that user has enrollment and his default "Student" forum role for this course
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course_key
))
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course_key
))
# delete this course and recreate this course with same user
# delete this course and recreate this course with same user
delete_course
_and_groups
(
self
.
course_key
,
self
.
user
.
id
)
delete_course
(
self
.
course_key
,
self
.
user
.
id
)
# now create same course with different name case ('uppercase')
# now create same course with different name case ('uppercase')
new_course_key
=
self
.
course_key
.
replace
(
course
=
self
.
course_key
.
course
.
upper
())
new_course_key
=
self
.
course_key
.
replace
(
course
=
self
.
course_key
.
course
.
upper
())
...
...
cms/djangoapps/contentstore/utils.py
View file @
511e6536
...
@@ -61,22 +61,38 @@ def remove_all_instructors(course_key):
...
@@ -61,22 +61,38 @@ def remove_all_instructors(course_key):
instructor_role
.
remove_users
(
*
instructor_role
.
users_with_role
())
instructor_role
.
remove_users
(
*
instructor_role
.
users_with_role
())
def
delete_course
_and_groups
(
course_key
,
user_id
):
def
delete_course
(
course_key
,
user_id
,
keep_instructors
=
False
):
"""
"""
This deletes the courseware associated with a course_key as well as cleaning update_item
Delete course from module store and if specified remove user and
the various user table stuff (groups, permissions, etc.)
groups permissions from course.
"""
_delete_course_from_modulestore
(
course_key
,
user_id
)
if
not
keep_instructors
:
_remove_instructors
(
course_key
)
def
_delete_course_from_modulestore
(
course_key
,
user_id
):
"""
Delete course from MongoDB. Deleting course will fire a signal which will result into
deletion of the courseware associated with a course_key.
"""
"""
module_store
=
modulestore
()
module_store
=
modulestore
()
with
module_store
.
bulk_operations
(
course_key
):
with
module_store
.
bulk_operations
(
course_key
):
module_store
.
delete_course
(
course_key
,
user_id
)
module_store
.
delete_course
(
course_key
,
user_id
)
print
'removing User permissions from course....'
# in the django layer, we need to remove all the user permissions groups associated with this course
def
_remove_instructors
(
course_key
):
try
:
"""
remove_all_instructors
(
course_key
)
In the django layer, remove all the user/groups permissions associated with this course
except
Exception
as
err
:
"""
log
.
error
(
"Error in deleting course groups for {0}: {1}"
.
format
(
course_key
,
err
))
print
'removing User permissions from course....'
try
:
remove_all_instructors
(
course_key
)
except
Exception
as
err
:
log
.
error
(
"Error in deleting course groups for {0}: {1}"
.
format
(
course_key
,
err
))
def
get_lms_link_for_item
(
location
,
preview
=
False
):
def
get_lms_link_for_item
(
location
,
preview
=
False
):
...
...
cms/djangoapps/contentstore/views/course.py
View file @
511e6536
...
@@ -22,6 +22,8 @@ from django.views.decorators.http import require_GET, require_http_methods
...
@@ -22,6 +22,8 @@ from django.views.decorators.http import require_GET, require_http_methods
from
opaque_keys
import
InvalidKeyError
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.locations
import
Location
from
opaque_keys.edx.locations
import
Location
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
openedx.core.djangoapps.waffle_utils
import
WaffleSwitchNamespace
from
contentstore.course_group_config
import
(
from
contentstore.course_group_config
import
(
COHORT_SCHEME
,
COHORT_SCHEME
,
...
@@ -100,6 +102,8 @@ __all__ = ['course_info_handler', 'course_handler', 'course_listing',
...
@@ -100,6 +102,8 @@ __all__ = ['course_info_handler', 'course_handler', 'course_listing',
'textbooks_list_handler'
,
'textbooks_detail_handler'
,
'textbooks_list_handler'
,
'textbooks_detail_handler'
,
'group_configurations_list_handler'
,
'group_configurations_detail_handler'
]
'group_configurations_list_handler'
,
'group_configurations_detail_handler'
]
WAFFLE_NAMESPACE
=
'studio_home'
class
AccessListFallback
(
Exception
):
class
AccessListFallback
(
Exception
):
"""
"""
...
@@ -352,9 +356,15 @@ def get_in_process_course_actions(request):
...
@@ -352,9 +356,15 @@ def get_in_process_course_actions(request):
]
]
def
_accessible_courses_summary_iter
(
request
):
def
_accessible_courses_summary_iter
(
request
,
org
=
None
):
"""
"""
List all courses available to the logged in user by iterating through all the courses
List all courses available to the logged in user by iterating through all the courses
Arguments:
request: the request object
org (string): if not None, this value will limit the courses returned. An empty
string will result in no courses, and otherwise only courses with the
specified org will be returned. The default value is None.
"""
"""
def
course_filter
(
course_summary
):
def
course_filter
(
course_summary
):
"""
"""
...
@@ -366,15 +376,19 @@ def _accessible_courses_summary_iter(request):
...
@@ -366,15 +376,19 @@ def _accessible_courses_summary_iter(request):
return
False
return
False
return
has_studio_read_access
(
request
.
user
,
course_summary
.
id
)
return
has_studio_read_access
(
request
.
user
,
course_summary
.
id
)
if
org
is
not
None
:
courses_summary
=
six
.
moves
.
filter
(
course_filter
,
modulestore
()
.
get_course_summaries
())
courses_summary
=
[]
if
org
==
''
else
CourseOverview
.
get_all_courses
(
orgs
=
[
org
])
else
:
courses_summary
=
modulestore
()
.
get_course_summaries
()
courses_summary
=
six
.
moves
.
filter
(
course_filter
,
courses_summary
)
in_process_course_actions
=
get_in_process_course_actions
(
request
)
in_process_course_actions
=
get_in_process_course_actions
(
request
)
return
courses_summary
,
in_process_course_actions
return
courses_summary
,
in_process_course_actions
def
_accessible_courses_iter
(
request
):
def
_accessible_courses_iter
(
request
):
"""
"""
List all courses available to the logged in user by iterating through all the courses
List all courses available to the logged in user by iterating through all the courses.
This method is only used by tests.
"""
"""
def
course_filter
(
course
):
def
course_filter
(
course
):
"""
"""
...
@@ -458,9 +472,18 @@ def course_listing(request):
...
@@ -458,9 +472,18 @@ def course_listing(request):
"""
"""
List all courses available to the logged in user
List all courses available to the logged in user
"""
"""
courses_iter
,
in_process_course_actions
=
get_courses_accessible_to_user
(
request
)
optimization_enabled
=
GlobalStaff
()
.
has_user
(
request
.
user
)
and
\
WaffleSwitchNamespace
(
name
=
WAFFLE_NAMESPACE
)
.
is_enabled
(
u'enable_global_staff_optimization'
)
if
optimization_enabled
:
org
=
request
.
GET
.
get
(
'org'
,
''
)
show_libraries
=
LIBRARIES_ENABLED
and
request
.
GET
.
get
(
'libraries'
,
'false'
)
.
lower
()
==
'true'
else
:
org
=
None
show_libraries
=
LIBRARIES_ENABLED
courses_iter
,
in_process_course_actions
=
get_courses_accessible_to_user
(
request
,
org
)
user
=
request
.
user
user
=
request
.
user
libraries
=
_accessible_libraries_iter
(
request
.
user
)
if
LIBRARIES_ENABLED
else
[]
libraries
=
_accessible_libraries_iter
(
request
.
user
)
if
show_libraries
else
[]
def
format_in_process_course_view
(
uca
):
def
format_in_process_course_view
(
uca
):
"""
"""
...
@@ -502,15 +525,16 @@ def course_listing(request):
...
@@ -502,15 +525,16 @@ def course_listing(request):
return
render_to_response
(
u'index.html'
,
{
return
render_to_response
(
u'index.html'
,
{
u'courses'
:
list
(
courses_iter
),
u'courses'
:
list
(
courses_iter
),
u'in_process_course_actions'
:
in_process_course_actions
,
u'in_process_course_actions'
:
in_process_course_actions
,
u'libraries_enabled'
:
LIBRARIES_ENABLED
,
u'libraries_enabled'
:
show_libraries
,
u'libraries'
:
[
format_library_for_view
(
lib
)
for
lib
in
libraries
],
u'libraries'
:
[
format_library_for_view
(
lib
)
for
lib
in
libraries
],
u'show_new_library_button'
:
get_library_creator_status
(
user
),
u'show_new_library_button'
:
show_libraries
and
get_library_creator_status
(
user
),
u'user'
:
user
,
u'user'
:
user
,
u'request_course_creator_url'
:
reverse
(
u'contentstore.views.request_course_creator'
),
u'request_course_creator_url'
:
reverse
(
u'contentstore.views.request_course_creator'
),
u'course_creator_status'
:
_get_course_creator_status
(
user
),
u'course_creator_status'
:
_get_course_creator_status
(
user
),
u'rerun_creator_status'
:
GlobalStaff
()
.
has_user
(
user
),
u'rerun_creator_status'
:
GlobalStaff
()
.
has_user
(
user
),
u'allow_unicode_course_id'
:
settings
.
FEATURES
.
get
(
u'ALLOW_UNICODE_COURSE_ID'
,
False
),
u'allow_unicode_course_id'
:
settings
.
FEATURES
.
get
(
u'ALLOW_UNICODE_COURSE_ID'
,
False
),
u'allow_course_reruns'
:
settings
.
FEATURES
.
get
(
u'ALLOW_COURSE_RERUNS'
,
True
),
u'allow_course_reruns'
:
settings
.
FEATURES
.
get
(
u'ALLOW_COURSE_RERUNS'
,
True
),
u'optimization_enabled'
:
optimization_enabled
})
})
...
@@ -605,14 +629,22 @@ def course_index(request, course_key):
...
@@ -605,14 +629,22 @@ def course_index(request, course_key):
})
})
def
get_courses_accessible_to_user
(
request
):
def
get_courses_accessible_to_user
(
request
,
org
=
None
):
"""
"""
Try to get all courses by first reversing django groups and fallback to old method if it fails
Try to get all courses by first reversing django groups and fallback to old method if it fails
Note: overhead of pymongo reads will increase if getting courses from django groups fails
Note: overhead of pymongo reads will increase if getting courses from django groups fails
Arguments:
request: the request object
org (string): for global staff users ONLY, this value will be used to limit
the courses returned. A value of None will have no effect (all courses
returned), an empty string will result in no courses, and otherwise only courses with the
specified org will be returned. The default value is None.
"""
"""
if
GlobalStaff
()
.
has_user
(
request
.
user
):
if
GlobalStaff
()
.
has_user
(
request
.
user
):
# user has global access so no need to get courses from django groups
# user has global access so no need to get courses from django groups
courses
,
in_process_course_actions
=
_accessible_courses_summary_iter
(
request
)
courses
,
in_process_course_actions
=
_accessible_courses_summary_iter
(
request
,
org
)
else
:
else
:
try
:
try
:
courses
,
in_process_course_actions
=
_accessible_courses_list_from_groups
(
request
)
courses
,
in_process_course_actions
=
_accessible_courses_list_from_groups
(
request
)
...
...
cms/envs/common.py
View file @
511e6536
...
@@ -59,7 +59,7 @@ from lms.envs.common import (
...
@@ -59,7 +59,7 @@ from lms.envs.common import (
# The following setting is included as it is used to check whether to
# The following setting is included as it is used to check whether to
# display credit eligibility table on the CMS or not.
# display credit eligibility table on the CMS or not.
ENABLE_CREDIT_ELIGIBILITY
,
YOUTUBE_API_KEY
,
ENABLE_CREDIT_ELIGIBILITY
,
YOUTUBE_API_KEY
,
DEFAULT_COURSE_ABOUT_IMAGE_URL
,
COURSE_MODE_DEFAULTS
,
DEFAULT_COURSE_ABOUT_IMAGE_URL
,
# Django REST framework configuration
# Django REST framework configuration
REST_FRAMEWORK
,
REST_FRAMEWORK
,
...
...
cms/static/js/views/modals/move_xblock_modal.js
View file @
511e6536
...
@@ -89,6 +89,10 @@ function($, Backbone, _, gettext, BaseView, XBlockViewUtils, MoveXBlockUtils, Ht
...
@@ -89,6 +89,10 @@ function($, Backbone, _, gettext, BaseView, XBlockViewUtils, MoveXBlockUtils, Ht
Feedback
.
prototype
.
outFocus
.
apply
(
this
);
Feedback
.
prototype
.
outFocus
.
apply
(
this
);
},
},
resize
:
function
()
{
// Do Nothing. Overridden to use our own styling instead of one provided by base modal
},
focusModal
:
function
()
{
focusModal
:
function
()
{
Feedback
.
prototype
.
inFocus
.
apply
(
this
,
[
this
.
options
.
modalWindowClass
]);
Feedback
.
prototype
.
inFocus
.
apply
(
this
,
[
this
.
options
.
modalWindowClass
]);
$
(
this
.
options
.
modalWindowClass
).
focus
();
$
(
this
.
options
.
modalWindowClass
).
focus
();
...
...
cms/static/sass/elements/_modal-window.scss
View file @
511e6536
...
@@ -288,8 +288,11 @@
...
@@ -288,8 +288,11 @@
// ------------------------
// ------------------------
// Move XBlock Modal
// Move XBlock Modal
.modal-window.move-modal
{
.move-modal
{
top
:
10%
!
important
;
position
:
fixed
;
left
:
50%
;
top
:
50%
;
@include
transform
(
translate
(
-50%
,-
50%
));
}
}
.move-xblock-modal
{
.move-xblock-modal
{
...
...
cms/static/sass/views/_dashboard.scss
View file @
511e6536
...
@@ -625,6 +625,18 @@
...
@@ -625,6 +625,18 @@
}
}
.optimization-form
{
margin-bottom
:
$baseline
;
label
{
font-size
:
1
.4rem
;
}
.form-actions
{
margin-top
:
(
$baseline
/
2
);
}
}
// ====================
// ====================
// ELEM: new user form
// ELEM: new user form
...
...
cms/templates/index.html
View file @
511e6536
...
@@ -183,6 +183,30 @@ from openedx.core.djangolib.markup import HTML, Text
...
@@ -183,6 +183,30 @@ from openedx.core.djangolib.markup import HTML, Text
</div>
</div>
% endif
% endif
%if optimization_enabled:
<div
class=
"optimization-form"
>
<h2
class=
"title title-3"
>
${_("Organization and Library Settings")}
</h2>
<form
class=
"form"
action=
"/home/"
method=
"GET"
>
<fieldset
class=
"form-group"
>
<div
class=
"field"
>
<label
class=
"field-label"
>
${_("Show all courses in organization:")}
<input
class=
"field-input input-text"
type=
"text"
name=
"org"
placeholder=
"${_('For example, MITx')}"
/>
</label>
</div>
<div
class=
"field"
>
<label
class=
"field-label"
>
${_("Show content libraries")}
<input
class=
"field-input input-text"
type=
"checkbox"
value=
"true"
name=
"libraries"
/>
</label>
</div>
</fieldset>
<div
class=
"form-actions"
>
<button
class=
"btn-brand btn-base"
type=
"submit"
>
${_("Submit")}
</button>
</div>
</form>
</div>
%endif
<!-- STATE: processing courses -->
<!-- STATE: processing courses -->
%if allow_course_reruns and rerun_creator_status and len(in_process_course_actions) > 0:
%if allow_course_reruns and rerun_creator_status and len(in_process_course_actions) > 0:
<div
class=
"courses courses-processing"
>
<div
class=
"courses courses-processing"
>
...
@@ -296,7 +320,7 @@ from openedx.core.djangolib.markup import HTML, Text
...
@@ -296,7 +320,7 @@ from openedx.core.djangolib.markup import HTML, Text
</ul>
</ul>
% endif
% endif
%if len(courses) > 0:
%if len(courses) > 0
or optimization_enabled
:
<div
class=
"courses courses-tab active"
>
<div
class=
"courses courses-tab active"
>
<ul
class=
"list-courses"
>
<ul
class=
"list-courses"
>
%for course_info in sorted(courses, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''):
%for course_info in sorted(courses, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''):
...
...
common/djangoapps/course_modes/models.py
View file @
511e6536
...
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta
...
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta
import
pytz
import
pytz
from
config_models.models
import
ConfigurationModel
from
config_models.models
import
ConfigurationModel
from
django.conf
import
settings
from
django.core.exceptions
import
ValidationError
from
django.core.exceptions
import
ValidationError
from
django.db
import
models
from
django.db
import
models
from
django.db.models
import
Q
from
django.db.models
import
Q
...
@@ -119,8 +120,18 @@ class CourseMode(models.Model):
...
@@ -119,8 +120,18 @@ class CourseMode(models.Model):
NO_ID_PROFESSIONAL_MODE
=
"no-id-professional"
NO_ID_PROFESSIONAL_MODE
=
"no-id-professional"
CREDIT_MODE
=
"credit"
CREDIT_MODE
=
"credit"
DEFAULT_MODE
=
Mode
(
AUDIT
,
_
(
'Audit'
),
0
,
''
,
'usd'
,
None
,
None
,
None
,
None
)
DEFAULT_MODE
=
Mode
(
DEFAULT_MODE_SLUG
=
AUDIT
settings
.
COURSE_MODE_DEFAULTS
[
'slug'
],
settings
.
COURSE_MODE_DEFAULTS
[
'name'
],
settings
.
COURSE_MODE_DEFAULTS
[
'min_price'
],
settings
.
COURSE_MODE_DEFAULTS
[
'suggested_prices'
],
settings
.
COURSE_MODE_DEFAULTS
[
'currency'
],
settings
.
COURSE_MODE_DEFAULTS
[
'expiration_datetime'
],
settings
.
COURSE_MODE_DEFAULTS
[
'description'
],
settings
.
COURSE_MODE_DEFAULTS
[
'sku'
],
settings
.
COURSE_MODE_DEFAULTS
[
'bulk_sku'
],
)
DEFAULT_MODE_SLUG
=
settings
.
COURSE_MODE_DEFAULTS
[
'slug'
]
# Modes utilized for audit/free enrollments
# Modes utilized for audit/free enrollments
AUDIT_MODES
=
[
AUDIT
,
HONOR
]
AUDIT_MODES
=
[
AUDIT
,
HONOR
]
...
...
lms/envs/aws.py
View file @
511e6536
...
@@ -137,6 +137,9 @@ if STATIC_URL_BASE:
...
@@ -137,6 +137,9 @@ if STATIC_URL_BASE:
# DEFAULT_COURSE_ABOUT_IMAGE_URL specifies the default image to show for courses that don't provide one
# DEFAULT_COURSE_ABOUT_IMAGE_URL specifies the default image to show for courses that don't provide one
DEFAULT_COURSE_ABOUT_IMAGE_URL
=
ENV_TOKENS
.
get
(
'DEFAULT_COURSE_ABOUT_IMAGE_URL'
,
DEFAULT_COURSE_ABOUT_IMAGE_URL
)
DEFAULT_COURSE_ABOUT_IMAGE_URL
=
ENV_TOKENS
.
get
(
'DEFAULT_COURSE_ABOUT_IMAGE_URL'
,
DEFAULT_COURSE_ABOUT_IMAGE_URL
)
# COURSE_MODE_DEFAULTS specifies the course mode to use for courses that do not set one
COURSE_MODE_DEFAULTS
=
ENV_TOKENS
.
get
(
'COURSE_MODE_DEFAULTS'
,
COURSE_MODE_DEFAULTS
)
# MEDIA_ROOT specifies the directory where user-uploaded files are stored.
# MEDIA_ROOT specifies the directory where user-uploaded files are stored.
MEDIA_ROOT
=
ENV_TOKENS
.
get
(
'MEDIA_ROOT'
,
MEDIA_ROOT
)
MEDIA_ROOT
=
ENV_TOKENS
.
get
(
'MEDIA_ROOT'
,
MEDIA_ROOT
)
MEDIA_URL
=
ENV_TOKENS
.
get
(
'MEDIA_URL'
,
MEDIA_URL
)
MEDIA_URL
=
ENV_TOKENS
.
get
(
'MEDIA_URL'
,
MEDIA_URL
)
...
...
lms/envs/common.py
View file @
511e6536
...
@@ -613,6 +613,18 @@ COURSE_SETTINGS = {
...
@@ -613,6 +613,18 @@ COURSE_SETTINGS = {
}
}
}
}
COURSE_MODE_DEFAULTS
=
{
'bulk_sku'
:
None
,
'currency'
:
'usd'
,
'description'
:
None
,
'expiration_datetime'
:
None
,
'min_price'
:
0
,
'name'
:
_
(
'Audit'
),
'sku'
:
None
,
'slug'
:
'audit'
,
'suggested_prices'
:
''
,
}
# IP addresses that are allowed to reload the course, etc.
# IP addresses that are allowed to reload the course, etc.
# TODO (vshnayder): Will probably need to change as we get real access control in.
# TODO (vshnayder): Will probably need to change as we get real access control in.
LMS_MIGRATION_ALLOWED_IPS
=
[]
LMS_MIGRATION_ALLOWED_IPS
=
[]
...
...
requirements/edx/base.txt
View file @
511e6536
...
@@ -98,7 +98,7 @@ python-dateutil==2.1
...
@@ -98,7 +98,7 @@ python-dateutil==2.1
git+https://github.com/edx/python-social-auth@758985102cee98f440fae44ed99617b7cfef3473#egg=python-social-auth==0.2.21.edx.a
git+https://github.com/edx/python-social-auth@758985102cee98f440fae44ed99617b7cfef3473#egg=python-social-auth==0.2.21.edx.a
pytz==2016.7
pytz==2016.7
pysrt==0.4.7
pysrt==0.4.7
PyYAML==3.1
0
PyYAML==3.1
2
requests==2.9.1
requests==2.9.1
requests-oauthlib==0.4.1
requests-oauthlib==0.4.1
rules==1.1.1
rules==1.1.1
...
...
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