Commit 88ad29d1 by Nimisha Asthagiri

LMS-11016 Studio server-side for course_listing and course_rerun.

Conflicts:
	cms/djangoapps/contentstore/tests/test_course_listing.py
parent fe4bce8b
......@@ -29,7 +29,7 @@ from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata
from opaque_keys.edx.keys import UsageKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation, CourseLocator
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint
......@@ -46,6 +46,7 @@ from student.models import CourseEnrollment
from student.roles import CourseCreatorRole, CourseInstructorRole
from opaque_keys import InvalidKeyError
from contentstore.tests.utils import get_url
from course_action_state.models import CourseRerunState, CourseRerunUIStateManager
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
......@@ -1561,6 +1562,107 @@ class MetadataSaveTestCase(ContentStoreTestCase):
pass
class RerunCourseTest(ContentStoreTestCase):
"""
Tests for Rerunning a course via the view handler
"""
def setUp(self):
super(RerunCourseTest, self).setUp()
self.destination_course_data = {
'org': 'MITx',
'number': '111',
'display_name': 'Robot Super Course',
'run': '2013_Spring'
}
self.destination_course_key = _get_course_id(self.destination_course_data)
def post_rerun_request(self, source_course_key, response_code=200):
"""Create and send an ajax post for the rerun request"""
# create data to post
rerun_course_data = {'source_course_key': unicode(source_course_key)}
rerun_course_data.update(self.destination_course_data)
# post the request
course_url = get_url('course_handler', self.destination_course_key, 'course_key_string')
response = self.client.ajax_post(course_url, rerun_course_data)
# verify response
self.assertEqual(response.status_code, response_code)
if response_code == 200:
self.assertNotIn('ErrMsg', parse_json(response))
def create_course_listing_html(self, course_key):
"""Creates html fragment that is created for the given course_key in the course listing section"""
return '<a class="course-link" href="/course/{}"'.format(course_key)
def create_unsucceeded_course_action_html(self, course_key):
"""Creates html fragment that is created for the given course_key in the unsucceeded course action section"""
# TODO LMS-11011 Update this once the Rerun UI is implemented.
return '<div class="unsucceeded-course-action" href="/course/{}"'.format(course_key)
def assertInCourseListing(self, course_key):
"""
Asserts that the given course key is in the accessible course listing section of the html
and NOT in the unsucceeded course action section of the html.
"""
course_listing_html = self.client.get_html('/course/')
self.assertIn(self.create_course_listing_html(course_key), course_listing_html.content)
self.assertNotIn(self.create_unsucceeded_course_action_html(course_key), course_listing_html.content)
def assertInUnsucceededCourseActions(self, course_key):
"""
Asserts that the given course key is in the unsucceeded course action section of the html
and NOT in the accessible course listing section of the html.
"""
course_listing_html = self.client.get_html('/course/')
self.assertNotIn(self.create_course_listing_html(course_key), course_listing_html.content)
# TODO Uncomment this once LMS-11011 is implemented.
# self.assertIn(self.create_unsucceeded_course_action_html(course_key), course_listing_html.content)
def test_rerun_course_success(self):
source_course = CourseFactory.create()
self.post_rerun_request(source_course.id)
# Verify that the course rerun action is marked succeeded
rerun_state = CourseRerunState.objects.find_first(course_key=self.destination_course_key)
self.assertEquals(rerun_state.state, CourseRerunUIStateManager.State.SUCCEEDED)
# Verify that the creator is now enrolled in the course.
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.destination_course_key))
# Verify both courses are in the course listing section
self.assertInCourseListing(source_course.id)
self.assertInCourseListing(self.destination_course_key)
def test_rerun_course_fail(self):
existent_course_key = CourseFactory.create().id
non_existent_course_key = CourseLocator("org", "non_existent_course", "run")
self.post_rerun_request(non_existent_course_key)
# Verify that the course rerun action is marked failed
rerun_state = CourseRerunState.objects.find_first(course_key=self.destination_course_key)
self.assertEquals(rerun_state.state, CourseRerunUIStateManager.State.FAILED)
self.assertIn("Cannot find a course at", rerun_state.message)
# Verify that the creator is not enrolled in the course.
self.assertFalse(CourseEnrollment.is_enrolled(self.user, non_existent_course_key))
# Verify that the existing course continues to be in the course listings
self.assertInCourseListing(existent_course_key)
# Verify that the failed course is NOT in the course listings
self.assertInUnsucceededCourseActions(non_existent_course_key)
def test_rerun_with_permission_denied(self):
with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}):
source_course = CourseFactory.create()
auth.add_users(self.user, CourseCreatorRole(), self.user)
self.user.is_staff = False
self.user.save()
self.post_rerun_request(source_course.id, 403)
class EntryPageTestCase(TestCase):
"""
Tests entry pages that aren't specific to a course.
......
......@@ -18,9 +18,10 @@ from student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff, Or
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
from xmodule.modulestore import ModuleStoreEnum
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey, CourseLocator
from xmodule.modulestore.django import modulestore
from xmodule.error_module import ErrorDescriptor
from course_action_state.models import CourseRerunState
TOTAL_COURSES_COUNT = 500
USER_COURSES_COUNT = 50
......@@ -76,11 +77,11 @@ class TestCourseListing(ModuleStoreTestCase):
self._create_course_with_access_groups(course_location, self.user)
# get courses through iterating all courses
courses_list = _accessible_courses_list(self.request)
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(len(courses_list), 1)
# get courses by reversing group name formats
courses_list_by_groups = _accessible_courses_list_from_groups(self.request)
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(len(courses_list_by_groups), 1)
# check both course lists have same courses
self.assertEqual(courses_list, courses_list_by_groups)
......@@ -98,11 +99,11 @@ class TestCourseListing(ModuleStoreTestCase):
self.assertIsInstance(modulestore().get_course(course_key), ErrorDescriptor)
# get courses through iterating all courses
courses_list = _accessible_courses_list(self.request)
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(courses_list, [])
# get courses by reversing group name formats
courses_list_by_groups = _accessible_courses_list_from_groups(self.request)
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(courses_list_by_groups, [])
def test_errored_course_regular_access(self):
......@@ -119,11 +120,11 @@ class TestCourseListing(ModuleStoreTestCase):
self.assertIsInstance(modulestore().get_course(course_key), ErrorDescriptor)
# get courses through iterating all courses
courses_list = _accessible_courses_list(self.request)
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(courses_list, [])
# get courses by reversing group name formats
courses_list_by_groups = _accessible_courses_list_from_groups(self.request)
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(courses_list_by_groups, [])
self.assertEqual(courses_list, courses_list_by_groups)
......@@ -135,11 +136,11 @@ class TestCourseListing(ModuleStoreTestCase):
self._create_course_with_access_groups(course_key, self.user)
# get courses through iterating all courses
courses_list = _accessible_courses_list(self.request)
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(len(courses_list), 1)
# get courses by reversing group name formats
courses_list_by_groups = _accessible_courses_list_from_groups(self.request)
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(len(courses_list_by_groups), 1)
# check both course lists have same courses
self.assertEqual(courses_list, courses_list_by_groups)
......@@ -150,7 +151,7 @@ class TestCourseListing(ModuleStoreTestCase):
CourseInstructorRole(course_key).add_users(self.user)
# test that get courses through iterating all courses now returns no course
courses_list = _accessible_courses_list(self.request)
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(len(courses_list), 0)
def test_course_listing_performance(self):
......@@ -175,22 +176,22 @@ class TestCourseListing(ModuleStoreTestCase):
# time the get courses by iterating through all courses
with Timer() as iteration_over_courses_time_1:
courses_list = _accessible_courses_list(self.request)
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(len(courses_list), USER_COURSES_COUNT)
# time again the get courses by iterating through all courses
with Timer() as iteration_over_courses_time_2:
courses_list = _accessible_courses_list(self.request)
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(len(courses_list), USER_COURSES_COUNT)
# time the get courses by reversing django groups
with Timer() as iteration_over_groups_time_1:
courses_list = _accessible_courses_list_from_groups(self.request)
courses_list, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(len(courses_list), USER_COURSES_COUNT)
# time again the get courses by reversing django groups
with Timer() as iteration_over_groups_time_2:
courses_list = _accessible_courses_list_from_groups(self.request)
courses_list, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(len(courses_list), USER_COURSES_COUNT)
# test that the time taken by getting courses through reversing django groups is lower then the time
......@@ -201,10 +202,10 @@ class TestCourseListing(ModuleStoreTestCase):
# Now count the db queries
store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo)
with check_mongo_calls(store, USER_COURSES_COUNT):
courses_list = _accessible_courses_list_from_groups(self.request)
_accessible_courses_list_from_groups(self.request)
with check_mongo_calls(store, 1):
courses_list = _accessible_courses_list(self.request)
_accessible_courses_list(self.request)
def test_get_course_list_with_same_course_id(self):
"""
......@@ -215,11 +216,11 @@ class TestCourseListing(ModuleStoreTestCase):
self._create_course_with_access_groups(course_location_caps, self.user)
# get courses through iterating all courses
courses_list = _accessible_courses_list(self.request)
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(len(courses_list), 1)
# get courses by reversing group name formats
courses_list_by_groups = _accessible_courses_list_from_groups(self.request)
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(len(courses_list_by_groups), 1)
# check both course lists have same courses
self.assertEqual(courses_list, courses_list_by_groups)
......@@ -229,22 +230,22 @@ class TestCourseListing(ModuleStoreTestCase):
self._create_course_with_access_groups(course_location_camel, self.user)
# test that get courses through iterating all courses returns both courses
courses_list = _accessible_courses_list(self.request)
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(len(courses_list), 2)
# test that get courses by reversing group name formats returns both courses
courses_list_by_groups = _accessible_courses_list_from_groups(self.request)
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(len(courses_list_by_groups), 2)
# now delete first course (course_location_caps) and check that it is no longer accessible
delete_course_and_groups(course_location_caps, self.user.id)
# test that get courses through iterating all courses now returns one course
courses_list = _accessible_courses_list(self.request)
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(len(courses_list), 1)
# test that get courses by reversing group name formats also returns one course
courses_list_by_groups = _accessible_courses_list_from_groups(self.request)
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(len(courses_list_by_groups), 1)
# now check that deleted course is not accessible
......@@ -282,7 +283,7 @@ class TestCourseListing(ModuleStoreTestCase):
}},
)
courses_list = _accessible_courses_list_from_groups(self.request)
courses_list, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(len(courses_list), 1, courses_list)
@ddt.data(OrgStaffRole('AwesomeOrg'), OrgInstructorRole('AwesomeOrg'))
......@@ -310,5 +311,34 @@ class TestCourseListing(ModuleStoreTestCase):
with self.assertRaises(AccessListFallback):
_accessible_courses_list_from_groups(self.request)
courses_list = _accessible_courses_list(self.request)
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(len(courses_list), 2)
def test_course_listing_with_actions_in_progress(self):
sourse_course_key = CourseLocator('source-Org', 'source-Course', 'source-Run')
num_courses_to_create = 3
courses = [
self._create_course_with_access_groups(CourseLocator('Org', 'CreatedCourse' + str(num), 'Run'), self.user)
for num in range(0, num_courses_to_create)
]
courses_in_progress = [
self._create_course_with_access_groups(CourseLocator('Org', 'InProgressCourse' + str(num), 'Run'), self.user)
for num in range(0, num_courses_to_create)
]
# simulate initiation of course actions
for course in courses_in_progress:
CourseRerunState.objects.initiated(sourse_course_key, destination_course_key=course.id, user=self.user)
# verify return values
for method in (_accessible_courses_list_from_groups, _accessible_courses_list):
def set_of_course_keys(course_list, key_attribute_name='id'):
"""Returns a python set of course keys by accessing the key with the given attribute name."""
return set(getattr(c, key_attribute_name) for c in course_list)
found_courses, unsucceeded_course_actions = method(self.request)
self.assertSetEqual(set_of_course_keys(courses + courses_in_progress), set_of_course_keys(found_courses))
self.assertSetEqual(
set_of_course_keys(courses_in_progress), set_of_course_keys(unsucceeded_course_actions, 'course_key')
)
......@@ -95,6 +95,7 @@ class CourseTestCase(ModuleStoreTestCase):
client = Client()
if authenticate:
client.login(username=nonstaff.username, password=password)
nonstaff.is_authenticated = True
return client, nonstaff
def populate_course(self):
......
......@@ -9,6 +9,8 @@ from pytz import UTC
from django.conf import settings
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from django_comment_common.models import assign_default_role
from django_comment_common.utils import seed_permissions_roles
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore import ModuleStoreEnum
......@@ -16,6 +18,8 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from opaque_keys.edx.keys import UsageKey, CourseKey
from student.roles import CourseInstructorRole, CourseStaffRole
from student.models import CourseEnrollment
from student import auth
log = logging.getLogger(__name__)
......@@ -26,25 +30,58 @@ NOTES_PANEL = {"name": _("My Notes"), "type": "notes"}
EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL]])
def delete_course_and_groups(course_id, user_id):
def add_instructor(course_key, requesting_user, new_instructor):
"""
This deletes the courseware associated with a course_id as well as cleaning update_item
Adds given user as instructor and staff to the given course,
after verifying that the requesting_user has permission to do so.
"""
# can't use auth.add_users here b/c it requires user to already have Instructor perms in this course
CourseInstructorRole(course_key).add_users(new_instructor)
auth.add_users(requesting_user, CourseStaffRole(course_key), new_instructor)
def initialize_permissions(course_key, user_who_created_course):
"""
Initializes a new course by enrolling the course creator as a student,
and initializing Forum by seeding its permissions and assigning default roles.
"""
# seed the forums
seed_permissions_roles(course_key)
# auto-enroll the course creator in the course so that "View Live" will work.
CourseEnrollment.enroll(user_who_created_course, course_key)
# set default forum roles (assign 'Student' role)
assign_default_role(course_key, user_who_created_course)
def remove_all_instructors(course_key):
"""
Removes given user as instructor and staff to the given course,
after verifying that the requesting_user has permission to do so.
"""
staff_role = CourseStaffRole(course_key)
staff_role.remove_users(*staff_role.users_with_role())
instructor_role = CourseInstructorRole(course_key)
instructor_role.remove_users(*instructor_role.users_with_role())
def delete_course_and_groups(course_key, user_id):
"""
This deletes the courseware associated with a course_key as well as cleaning update_item
the various user table stuff (groups, permissions, etc.)
"""
module_store = modulestore()
with module_store.bulk_write_operations(course_id):
module_store.delete_course(course_id, user_id)
with module_store.bulk_write_operations(course_key):
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
try:
staff_role = CourseStaffRole(course_id)
staff_role.remove_users(*staff_role.users_with_role())
instructor_role = CourseInstructorRole(course_id)
instructor_role.remove_users(*instructor_role.users_with_role())
remove_all_instructors(course_key)
except Exception as err:
log.error("Error in deleting course groups for {0}: {1}".format(course_id, err))
log.error("Error in deleting course groups for {0}: {1}".format(course_key, err))
def get_lms_link_for_item(location, preview=False):
......@@ -64,19 +101,19 @@ def get_lms_link_for_item(location, preview=False):
else:
lms_base = settings.LMS_BASE
return u"//{lms_base}/courses/{course_id}/jump_to/{location}".format(
return u"//{lms_base}/courses/{course_key}/jump_to/{location}".format(
lms_base=lms_base,
course_id=location.course_key.to_deprecated_string(),
course_key=location.course_key.to_deprecated_string(),
location=location.to_deprecated_string(),
)
def get_lms_link_for_about_page(course_id):
def get_lms_link_for_about_page(course_key):
"""
Returns the url to the course about page from the location tuple.
"""
assert(isinstance(course_id, CourseKey))
assert(isinstance(course_key, CourseKey))
if settings.FEATURES.get('ENABLE_MKTG_SITE', False):
if not hasattr(settings, 'MKTG_URLS'):
......@@ -101,9 +138,9 @@ def get_lms_link_for_about_page(course_id):
else:
return None
return u"//{about_base_url}/courses/{course_id}/about".format(
return u"//{about_base_url}/courses/{course_key}/about".format(
about_base_url=about_base,
course_id=course_id.to_deprecated_string()
course_key=course_key.to_deprecated_string()
)
......
......@@ -30,14 +30,16 @@ from opaque_keys.edx.locations import Location, SlashSeparatedCourseKey
from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
from contentstore.utils import (
add_instructor,
initialize_permissions,
get_lms_link_for_item,
add_extra_panel_tab,
remove_extra_panel_tab,
reverse_course_url,
reverse_usage_url,
reverse_url,
)
from models.settings.course_details import CourseDetails, CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata
from util.json_request import expect_json
......@@ -50,21 +52,20 @@ from .component import (
ADVANCED_COMPONENT_POLICY_KEY,
SPLIT_TEST_COMPONENT_TYPE,
)
from django_comment_common.models import assign_default_role
from django_comment_common.utils import seed_permissions_roles
from student.models import CourseEnrollment
from student.roles import CourseRole, UserBasedRole
from .tasks import rerun_course
from opaque_keys.edx.keys import CourseKey
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested
from contentstore import utils
from student.roles import CourseInstructorRole, CourseStaffRole, CourseCreatorRole, GlobalStaff
from student.roles import (
CourseInstructorRole, CourseStaffRole, CourseCreatorRole, GlobalStaff, UserBasedRole
)
from student import auth
from course_action_state.models import CourseRerunState, CourseRerunUIStateManager
from microsite_configuration import microsite
__all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler',
'settings_handler',
'grading_handler',
......@@ -123,7 +124,7 @@ def course_handler(request, course_key_string=None):
if request.method == 'GET':
return JsonResponse(_course_json(request, CourseKey.from_string(course_key_string)))
elif request.method == 'POST': # not sure if this is only post. If one will have ids, it goes after access
return create_new_course(request)
return _create_or_rerun_course(request)
elif not has_course_access(request.user, CourseKey.from_string(course_key_string)):
raise PermissionDenied()
elif request.method == 'PUT':
......@@ -171,26 +172,36 @@ def _accessible_courses_list(request):
"""
List all courses available to the logged in user by iterating through all the courses
"""
courses = modulestore().get_courses()
def course_permission_filter(course_key):
"""Filter out courses that user doesn't have access to"""
if GlobalStaff().has_user(request.user):
return True
else:
return has_course_access(request.user, course_key)
# filter out courses that we don't have access to
def course_filter(course):
"""
Get courses to which this user has access
Filter out unusable and inaccessible courses
"""
if isinstance(course, ErrorDescriptor):
return False
if GlobalStaff().has_user(request.user):
return course.location.course != 'templates'
# pylint: disable=fixme
# TODO remove this condition when templates purged from db
if course.location.course == 'templates':
return False
return (has_course_access(request.user, course.id)
# pylint: disable=fixme
# TODO remove this condition when templates purged from db
and course.location.course != 'templates'
)
courses = filter(course_filter, courses)
return courses
return course_permission_filter(course.id)
courses = filter(course_filter, modulestore().get_courses())
unsucceeded_course_actions = [
crs for crs in
CourseRerunState.objects.find_all(
exclude_args={'state': CourseRerunUIStateManager.State.SUCCEEDED}, should_display=True
)
if course_permission_filter(crs.course_key)
]
return courses, unsucceeded_course_actions
def _accessible_courses_list_from_groups(request):
......@@ -198,6 +209,7 @@ def _accessible_courses_list_from_groups(request):
List all courses available to the logged in user by reversing access group names
"""
courses_list = {}
unsucceeded_course_actions = []
instructor_courses = UserBasedRole(request.user, CourseInstructorRole.ROLE).courses_with_role()
staff_courses = UserBasedRole(request.user, CourseStaffRole.ROLE).courses_with_role()
......@@ -209,6 +221,15 @@ def _accessible_courses_list_from_groups(request):
# If the course_access does not have a course_id, it's an org-based role, so we fall back
raise AccessListFallback
if course_key not in courses_list:
# check for any course action state for this course
unsucceeded_course_actions.extend(
CourseRerunState.objects.find_all(
exclude_args={'state': CourseRerunUIStateManager.State.SUCCEEDED},
should_display=True,
course_key=course_key,
)
)
# check for the course itself
try:
course = modulestore().get_course(course_key)
except ItemNotFoundError:
......@@ -218,7 +239,7 @@ def _accessible_courses_list_from_groups(request):
# ignore deleted or errored courses
courses_list[course_key] = course
return courses_list.values()
return courses_list.values(), unsucceeded_course_actions
@login_required
......@@ -231,14 +252,14 @@ def course_listing(request):
"""
if GlobalStaff().has_user(request.user):
# user has global access so no need to get courses from django groups
courses = _accessible_courses_list(request)
courses, unsucceeded_course_actions = _accessible_courses_list(request)
else:
try:
courses = _accessible_courses_list_from_groups(request)
courses, unsucceeded_course_actions = _accessible_courses_list_from_groups(request)
except AccessListFallback:
# user have some old groups or there was some error getting courses from django groups
# so fallback to iterating through all courses
courses = _accessible_courses_list(request)
courses, unsucceeded_course_actions = _accessible_courses_list(request)
def format_course_for_view(course):
"""
......@@ -253,8 +274,17 @@ def course_listing(request):
course.location.name
)
# remove any courses in courses that are also in the unsucceeded_course_actions list
unsucceeded_action_course_keys = [uca.course_key for uca in unsucceeded_course_actions]
courses = [
format_course_for_view(c)
for c in courses
if not isinstance(c, ErrorDescriptor) and (c.id not in unsucceeded_action_course_keys)
]
return render_to_response('index.html', {
'courses': [format_course_for_view(c) for c in courses if not isinstance(c, ErrorDescriptor)],
'courses': courses,
'unsucceeded_course_actions': unsucceeded_course_actions,
'user': request.user,
'request_course_creator_url': reverse('contentstore.views.request_course_creator'),
'course_creator_status': _get_course_creator_status(request.user),
......@@ -289,80 +319,36 @@ def course_index(request, course_key):
@expect_json
def create_new_course(request):
def _create_or_rerun_course(request):
"""
Create a new course.
Returns the URL for the course overview page.
To be called by requests that create a new destination course (i.e., create_new_course and rerun_course)
Returns the destination course_key and overriding fields for the new course.
Raises InvalidLocationError and InvalidKeyError
"""
if not auth.has_access(request.user, CourseCreatorRole()):
raise PermissionDenied()
org = request.json.get('org')
number = request.json.get('number')
display_name = request.json.get('display_name')
run = request.json.get('run')
# allow/disable unicode characters in course_id according to settings
if not settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID'):
if _has_non_ascii_characters(org) or _has_non_ascii_characters(number) or _has_non_ascii_characters(run):
return JsonResponse(
{'error': _('Special characters not allowed in organization, course number, and course run.')},
status=400
)
try:
org = request.json.get('org')
number = request.json.get('number')
display_name = request.json.get('display_name')
run = request.json.get('run')
# allow/disable unicode characters in course_id according to settings
if not settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID'):
if _has_non_ascii_characters(org) or _has_non_ascii_characters(number) or _has_non_ascii_characters(run):
return JsonResponse(
{'error': _('Special characters not allowed in organization, course number, and course run.')},
status=400
)
course_key = SlashSeparatedCourseKey(org, number, run)
fields = {'display_name': display_name} if display_name is not None else {}
# instantiate the CourseDescriptor and then persist it
# note: no system to pass
if display_name is None:
metadata = {}
if 'source_course_key' in request.json:
return _rerun_course(request, course_key, fields)
else:
metadata = {'display_name': display_name}
# Set a unique wiki_slug for newly created courses. To maintain active wiki_slugs for
# existing xml courses this cannot be changed in CourseDescriptor.
# # TODO get rid of defining wiki slug in this org/course/run specific way and reconcile
# w/ xmodule.course_module.CourseDescriptor.__init__
wiki_slug = u"{0}.{1}.{2}".format(course_key.org, course_key.course, course_key.run)
definition_data = {'wiki_slug': wiki_slug}
# Create the course then fetch it from the modulestore
# Check if role permissions group for a course named like this already exists
# Important because role groups are case insensitive
if CourseRole.course_group_already_exists(course_key):
raise InvalidLocationError()
fields = {}
fields.update(definition_data)
fields.update(metadata)
# Creating the course raises InvalidLocationError if an existing course with this org/name is found
new_course = modulestore().create_course(
course_key.org,
course_key.course,
course_key.run,
request.user.id,
fields=fields,
)
# can't use auth.add_users here b/c it requires request.user to already have Instructor perms in this course
# however, we can assume that b/c this user had authority to create the course, the user can add themselves
CourseInstructorRole(new_course.id).add_users(request.user)
auth.add_users(request.user, CourseStaffRole(new_course.id), request.user)
# seed the forums
seed_permissions_roles(new_course.id)
# auto-enroll the course creator in the course so that "View Live" will
# work.
CourseEnrollment.enroll(request.user, new_course.id)
_users_assign_default_role(new_course.id)
return JsonResponse({
'url': reverse_course_url('course_handler', new_course.id)
})
return _create_new_course(request, course_key, fields)
except InvalidLocationError:
return JsonResponse({
......@@ -384,13 +370,62 @@ def create_new_course(request):
)
def _users_assign_default_role(course_id):
def _create_new_course(request, course_key, fields):
"""
Create a new course.
Returns the URL for the course overview page.
"""
# Set a unique wiki_slug for newly created courses. To maintain active wiki_slugs for
# existing xml courses this cannot be changed in CourseDescriptor.
# # TODO get rid of defining wiki slug in this org/course/run specific way and reconcile
# w/ xmodule.course_module.CourseDescriptor.__init__
wiki_slug = u"{0}.{1}.{2}".format(course_key.org, course_key.course, course_key.run)
definition_data = {'wiki_slug': wiki_slug}
fields.update(definition_data)
# Creating the course raises InvalidLocationError if an existing course with this org/name is found
new_course = modulestore().create_course(
course_key.org,
course_key.course,
course_key.run,
request.user.id,
fields=fields,
)
# Make sure user has instructor and staff access to the new course
add_instructor(new_course.id, request.user, request.user)
# Initialize permissions for user in the new course
initialize_permissions(new_course.id, request.user)
return JsonResponse({
'url': reverse_course_url('course_handler', new_course.id)
})
def _rerun_course(request, destination_course_key, fields):
"""
Assign 'Student' role to all previous users (if any) for this course
Reruns an existing course.
Returns the URL for the course listing page.
"""
enrollments = CourseEnrollment.objects.filter(course_id=course_id)
for enrollment in enrollments:
assign_default_role(course_id, enrollment.user)
source_course_key = CourseKey.from_string(request.json.get('source_course_key'))
# verify user has access to the original course
if not has_course_access(request.user, source_course_key):
raise PermissionDenied()
# Make sure user has instructor and staff access to the destination course
# so the user can see the updated status for that course
add_instructor(destination_course_key, request.user, request.user)
# Mark the action as initiated
CourseRerunState.objects.initiated(source_course_key, destination_course_key, request.user)
# Rerun the course as a new celery task
rerun_course.delay(source_course_key, destination_course_key, request.user.id, fields)
# Return course listing page
return JsonResponse({'url': reverse_url('course_handler')})
# pylint: disable=unused-argument
......
"""
This file contains celery tasks for contentstore views
"""
from celery.task import task
from django.contrib.auth.models import User
from xmodule.modulestore.django import modulestore
from course_action_state.models import CourseRerunState
from contentstore.utils import initialize_permissions
@task()
def rerun_course(source_course_key, destination_course_key, user_id, fields=None):
"""
Reruns a course in a new celery task.
"""
try:
modulestore().clone_course(source_course_key, destination_course_key, user_id, fields=fields)
# set initial permissions for the user to access the course.
initialize_permissions(destination_course_key, User.objects.get(id=user_id))
# update state: Succeeded
CourseRerunState.objects.succeeded(course_key=destination_course_key)
# catch all exceptions so we can update the state and properly cleanup the course.
except Exception as exc: # pylint: disable=broad-except
# update state: Failed
CourseRerunState.objects.failed(course_key=destination_course_key, exception=exc)
# cleanup any remnants of the course
modulestore().delete_course(destination_course_key, user_id)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment