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
bac223ac
Commit
bac223ac
authored
Jun 07, 2017
by
noraiz-anwar
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Enhance delete_course management command
parent
484ec256
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
109 additions
and
23 deletions
+109
-23
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
No files found.
cms/djangoapps/contentstore/management/commands/delete_course.py
View file @
bac223ac
...
@@ -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 @
bac223ac
...
@@ -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 @
bac223ac
...
@@ -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 @
bac223ac
...
@@ -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 @
bac223ac
...
@@ -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 @
bac223ac
...
@@ -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
):
...
...
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