Example #1
0
def delete_course_and_groups(course_id, commit=False):
    """
    This deletes the courseware associated with a course_id as well as cleaning update_item
    the various user table stuff (groups, permissions, etc.)
    """
    module_store = modulestore('direct')
    content_store = contentstore()

    course_id_dict = Location.parse_course_id(course_id)
    module_store.ignore_write_events_on_courses.append('{org}/{course}'.format(**course_id_dict))

    loc = CourseDescriptor.id_to_location(course_id)
    if delete_course(module_store, content_store, loc, commit):

        print 'removing User permissions from course....'
        # in the django layer, we need to remove all the user permissions groups associated with this course
        if commit:
            try:
                staff_role = CourseStaffRole(loc)
                staff_role.remove_users(*staff_role.users_with_role())
                instructor_role = CourseInstructorRole(loc)
                instructor_role.remove_users(*instructor_role.users_with_role())
            except Exception as err:
                log.error("Error in deleting course groups for {0}: {1}".format(loc, err))

            # remove location of this course from loc_mapper and cache
            loc_mapper().delete_course_mapping(loc)
Example #2
0
def get_all_course_role_groupnames(location, role, use_filter=True):
    '''
    Get all of the possible groupnames for this role location pair. If use_filter==True,
    only return the ones defined in the groups collection.
    '''
    location = Locator.to_locator_or_location(location)

    groupnames = []
    if isinstance(location, Location):
        try:
            groupnames.append('{0}_{1}'.format(role, location.course_id))
        except InvalidLocationError:  # will occur on old locations where location is not of category course
            pass
        try:
            locator = loc_mapper().translate_location(location.course_id, location, False, False)
            groupnames.append('{0}_{1}'.format(role, locator.package_id))
        except (InvalidLocationError, ItemNotFoundError):
            pass
        # least preferred role_course format for legacy reasons
        groupnames.append('{0}_{1}'.format(role, location.course))
    elif isinstance(location, CourseLocator):
        groupnames.append('{0}_{1}'.format(role, location.package_id))
        old_location = loc_mapper().translate_locator_to_location(location, get_course=True)
        if old_location:
            # the slashified version of the course_id (myu/mycourse/myrun)
            groupnames.append('{0}_{1}'.format(role, old_location.course_id))
            # add the least desirable but sometimes occurring format.
            groupnames.append('{0}_{1}'.format(role, old_location.course))
    # filter to the ones which exist
    default = groupnames[0]
    if use_filter:
        groupnames = [group.name for group in Group.objects.filter(name__in=groupnames)]
    return groupnames, default
Example #3
0
def course_info_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None):
    """
    GET
        html: return html for editing the course info handouts and updates.
    """
    course_location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
    course_old_location = loc_mapper().translate_locator_to_location(course_location)
    if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
        if not has_access(request.user, course_location):
            raise PermissionDenied()

        course_module = modulestore().get_item(course_old_location)

        handouts_old_location = course_old_location.replace(category='course_info', name='handouts')
        handouts_locator = loc_mapper().translate_location(
            course_old_location.course_id, handouts_old_location, False, True
        )

        update_location = course_old_location.replace(category='course_info', name='updates')
        update_locator = loc_mapper().translate_location(
            course_old_location.course_id, update_location, False, True
        )

        return render_to_response(
            'course_info.html',
            {
                'context_course': course_module,
                'updates_url': update_locator.url_reverse('course_info_update/'),
                'handouts_locator': handouts_locator,
                'base_asset_url': StaticContent.get_base_url_path_for_course_assets(course_old_location) + '/'
            }
        )
    else:
        return HttpResponseBadRequest("Only supports html requests")
Example #4
0
def get_all_course_role_groupnames(location, role, use_filter=True):
    '''
    Get all of the possible groupnames for this role location pair. If use_filter==True,
    only return the ones defined in the groups collection.
    '''
    location = Locator.to_locator_or_location(location)

    # hack: check for existence of a group name in the legacy LMS format <role>_<course>
    # if it exists, then use that one, otherwise use a <role>_<course_id> which contains
    # more information
    groupnames = []
    try:
        groupnames.append('{0}_{1}'.format(role, location.course_id))
    except InvalidLocationError:  # will occur on old locations where location is not of category course
        pass
    if isinstance(location, Location):
        # least preferred role_course format
        groupnames.append('{0}_{1}'.format(role, location.course))
        try:
            locator = loc_mapper().translate_location(location.course_id, location, False, False)
            groupnames.append('{0}_{1}'.format(role, locator.course_id))
        except (InvalidLocationError, ItemNotFoundError):
            pass
    elif isinstance(location, CourseLocator):
        old_location = loc_mapper().translate_locator_to_location(location, get_course=True)
        if old_location:
            # the slashified version of the course_id (myu/mycourse/myrun)
            groupnames.append('{0}_{1}'.format(role, old_location.course_id))
            # add the least desirable but sometimes occurring format.
            groupnames.append('{0}_{1}'.format(role, old_location.course))
    # filter to the ones which exist
    default = groupnames[0]
    if use_filter:
        groupnames = [group for group in groupnames if Group.objects.filter(name=group).exists()]
    return groupnames, default
Example #5
0
def course_info_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
    """
    GET
        html: return html for editing the course info handouts and updates.
    """
    __, course_module = _get_locator_and_course(package_id, branch, version_guid, block, request.user)
    if "text/html" in request.META.get("HTTP_ACCEPT", "text/html"):
        handouts_old_location = course_module.location.replace(category="course_info", name="handouts")
        handouts_locator = loc_mapper().translate_location(
            course_module.location.course_id, handouts_old_location, False, True
        )

        update_location = course_module.location.replace(category="course_info", name="updates")
        update_locator = loc_mapper().translate_location(course_module.location.course_id, update_location, False, True)

        return render_to_response(
            "course_info.html",
            {
                "context_course": course_module,
                "updates_url": update_locator.url_reverse("course_info_update/"),
                "handouts_locator": handouts_locator,
                "base_asset_url": StaticContent.get_base_url_path_for_course_assets(course_module.location) + "/",
            },
        )
    else:
        return HttpResponseBadRequest("Only supports html requests")
Example #6
0
        def check_equality(source_locator, duplicate_locator):
            original_item = self.get_item_from_modulestore(source_locator, draft=True)
            duplicated_item = self.get_item_from_modulestore(duplicate_locator, draft=True)

            self.assertNotEqual(
                original_item.location,
                duplicated_item.location,
                "Location of duplicate should be different from original"
            )
            # Set the location and display name to be the same so we can make sure the rest of the duplicate is equal.
            duplicated_item.location = original_item.location
            duplicated_item.display_name = original_item.display_name

            # Children will also be duplicated, so for the purposes of testing equality, we will set
            # the children to the original after recursively checking the children.
            if original_item.has_children:
                self.assertEqual(
                    len(original_item.children),
                    len(duplicated_item.children),
                    "Duplicated item differs in number of children"
                )
                for i in xrange(len(original_item.children)):
                    source_locator = loc_mapper().translate_location(
                        self.course.location.course_id, Location(original_item.children[i]), False, True
                    )
                    duplicate_locator = loc_mapper().translate_location(
                        self.course.location.course_id, Location(duplicated_item.children[i]), False, True
                    )
                    if not check_equality(source_locator, duplicate_locator):
                        return False
                duplicated_item.children = original_item.children

            return original_item == duplicated_item
Example #7
0
def course_info_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
    """
    GET
        html: return html for editing the course info handouts and updates.
    """
    __, course_module = _get_locator_and_course(
        package_id, branch, version_guid, block, request.user
    )
    if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
        handouts_old_location = course_module.location.replace(category='course_info', name='handouts')
        handouts_locator = loc_mapper().translate_location(
            course_module.location.course_id, handouts_old_location, False, True
        )

        update_location = course_module.location.replace(category='course_info', name='updates')
        update_locator = loc_mapper().translate_location(
            course_module.location.course_id, update_location, False, True
        )

        return render_to_response(
            'course_info.html',
            {
                'context_course': course_module,
                'updates_url': update_locator.url_reverse('course_info_update/'),
                'handouts_locator': handouts_locator,
                'base_asset_url': StaticContent.get_base_url_path_for_course_assets(course_module.location) + '/'
            }
        )
    else:
        return HttpResponseBadRequest("Only supports html requests")
    def test_get_course_list_with_same_course_id(self):
        """
        Test getting courses with same id but with different name case. Then try to delete one of them and
        check that it is properly deleted and other one is accessible
        """
        request = self.factory.get('/course')
        request.user = self.user

        course_location_caps = Location(['i4x', 'Org', 'COURSE', 'course', 'Run'])
        self._create_course_with_access_groups(course_location_caps, 'group_name_with_dots', self.user)

        # get courses through iterating all courses
        courses_list = _accessible_courses_list(request)
        self.assertEqual(len(courses_list), 1)

        # get courses by reversing group name formats
        courses_list_by_groups = _accessible_courses_list_from_groups(request)
        self.assertEqual(len(courses_list_by_groups), 1)
        # check both course lists have same courses
        self.assertEqual(courses_list, courses_list_by_groups)

        # now create another course with same course_id but different name case
        course_location_camel = Location(['i4x', 'Org', 'Course', 'course', 'Run'])
        self._create_course_with_access_groups(course_location_camel, 'group_name_with_dots', self.user)

        # test that get courses through iterating all courses returns both courses
        courses_list = _accessible_courses_list(request)
        self.assertEqual(len(courses_list), 2)

        # test that get courses by reversing group name formats returns only one course
        courses_list_by_groups = _accessible_courses_list_from_groups(request)
        self.assertEqual(len(courses_list_by_groups), 1)

        course_locator = loc_mapper().translate_location(course_location_caps.course_id, course_location_caps)
        outline_url = course_locator.url_reverse('course/')
        # now delete first course (course_location_caps) and check that it is no longer accessible
        delete_course_and_groups(course_location_caps.course_id, commit=True)
        # add user to this course instructor group since he was removed from that group on course delete
        instructor_group_name = CourseInstructorRole(course_locator)._group_names[0]  # pylint: disable=protected-access
        group, __ = Group.objects.get_or_create(name=instructor_group_name)
        self.user.groups.add(group)

        # test that get courses through iterating all courses now returns one course
        courses_list = _accessible_courses_list(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(request)
        self.assertEqual(len(courses_list_by_groups), 1)

        # now check that deleted course in not accessible
        response = self.client.get(outline_url, HTTP_ACCEPT='application/json')
        self.assertEqual(response.status_code, 403)

        # now check that other course in accessible
        course_locator = loc_mapper().translate_location(course_location_camel.course_id, course_location_camel)
        outline_url = course_locator.url_reverse('course/')
        response = self.client.get(outline_url, HTTP_ACCEPT='application/json')
        self.assertEqual(response.status_code, 200)
    def test_post_course_update(self):
        """
        Test that a user can successfully post on course updates and handouts of a course
        whose location in not in loc_mapper
        """
        # create a course via the view handler
        course_location = Location(['i4x', 'Org_1', 'Course_1', 'course', 'Run_1'])
        course_locator = loc_mapper().translate_location(
            course_location.course_id, course_location, False, True
        )
        self.client.ajax_post(
            course_locator.url_reverse('course'),
            {
                'org': course_location.org,
                'number': course_location.course,
                'display_name': 'test course',
                'run': course_location.name,
            }
        )

        branch = u'draft'
        version = None
        block = u'updates'
        updates_locator = BlockUsageLocator(
            package_id=course_location.course_id.replace('/', '.'), branch=branch, version_guid=version, block_id=block
        )

        content = u"Sample update"
        payload = {'content': content, 'date': 'January 8, 2013'}
        course_update_url = updates_locator.url_reverse('course_info_update')
        resp = self.client.ajax_post(course_update_url, payload)

        # check that response status is 200 not 400
        self.assertEqual(resp.status_code, 200)

        payload = json.loads(resp.content)
        self.assertHTMLEqual(payload['content'], content)

        # now test that calling translate_location returns a locator whose block_id is 'updates'
        updates_location = course_location.replace(category='course_info', name=block)
        updates_locator = loc_mapper().translate_location(course_location.course_id, updates_location)
        self.assertTrue(isinstance(updates_locator, BlockUsageLocator))
        self.assertEqual(updates_locator.block_id, block)

        # check posting on handouts
        block = u'handouts'
        handouts_locator = BlockUsageLocator(
            package_id=updates_locator.package_id, branch=updates_locator.branch, version_guid=version, block_id=block
        )
        course_handouts_url = handouts_locator.url_reverse('xblock')
        content = u"Sample handout"
        payload = {"data": content}
        resp = self.client.ajax_post(course_handouts_url, payload)

        # check that response status is 200 not 500
        self.assertEqual(resp.status_code, 200)

        payload = json.loads(resp.content)
        self.assertHTMLEqual(payload['data'], content)
Example #10
0
def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
    """
    The restful handler for static tabs.

    GET
        html: return page for editing static tabs
        json: not supported
    PUT or POST
        json: update the tab order. It is expected that the request body contains a JSON-encoded dict with entry "tabs".
        The value for "tabs" is an array of tab locators, indicating the desired order of the tabs.

    Creating a tab, deleting a tab, or changing its contents is not supported through this method.
    Instead use the general xblock URL (see item.xblock_handler).
    """
    locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
    if not has_course_access(request.user, locator):
        raise PermissionDenied()

    old_location = loc_mapper().translate_locator_to_location(locator)
    store = get_modulestore(old_location)
    course_item = store.get_item(old_location)

    if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
        if request.method == 'GET':
            raise NotImplementedError('coming soon')
        else:
            if 'tabs' in request.json:
                return reorder_tabs_handler(course_item, request)
            elif 'tab_id_locator' in request.json:
                return edit_tab_handler(course_item, request)
            else:
                raise NotImplementedError('Creating or changing tab content is not supported.')

    elif request.method == 'GET':  # assume html
        # get all tabs from the tabs list: static tabs (a.k.a. user-created tabs) and built-in tabs
        # present in the same order they are displayed in LMS

        tabs_to_render = []
        for tab in CourseTabList.iterate_displayable_cms(
                course_item,
                settings,
        ):
            if isinstance(tab, StaticTab):
                # static tab needs its locator information to render itself as an xmodule
                static_tab_loc = old_location.replace(category='static_tab', name=tab.url_slug)
                tab.locator = loc_mapper().translate_location(
                    course_item.location.course_id, static_tab_loc, False, True
                )
            tabs_to_render.append(tab)

        return render_to_response('edit-tabs.html', {
            'context_course': course_item,
            'tabs_to_render': tabs_to_render,
            'course_locator': locator,
            'lms_link': get_lms_link_for_item(course_item.location),
        })
    else:
        return HttpResponseNotFound()
Example #11
0
    def _infer_course_id_try(self, location):
        """
        Create, Update, Delete operations don't require a fully-specified course_id, but
        there's no complete & sound general way to compute the course_id except via the
        proper modulestore. This method attempts several sound but not complete methods.
        :param location: an old style Location
        """
        if isinstance(location, CourseLocator):
            return location.package_id
        elif isinstance(location, basestring):
            try:
                location = Location(location)
            except InvalidLocationError:
                # try to parse as a course_id
                try:
                    Location.parse_course_id(location)
                    # it's already a course_id
                    return location
                except ValueError:
                    # cannot interpret the location
                    return None

        # location is a Location at this point
        if location.category == 'course':  # easiest case
            return location.course_id
        # try finding in loc_mapper
        try:
            # see if the loc mapper knows the course id (requires double translation)
            locator = loc_mapper().translate_location_to_course_locator(None, location)
            location = loc_mapper().translate_locator_to_location(locator, get_course=True)
            return location.course_id
        except ItemNotFoundError:
            pass
        # expensive query against all location-based modulestores to look for location.
        for store in self.modulestores.itervalues():
            if isinstance(location, store.reference_type):
                try:
                    xblock = store.get_item(location)
                    course_id = self._get_course_id_from_block(xblock, store)
                    if course_id is not None:
                        return course_id
                except NotImplementedError:
                    blocks = store.get_items(location)
                    if len(blocks) == 1:
                        block = blocks[0]
                        try:
                            return block.course_id
                        except UndefinedContext:
                            pass
                except ItemNotFoundError:
                    pass
        # if we get here, it must be in a Locator based store, but we won't be able to find
        # it.
        return None
Example #12
0
    def get_items(self, location, course_id=None, depth=0, qualifiers=None):
        """
        Returns a list of XModuleDescriptor instances for the items
        that match location. Any element of location that is None is treated
        as a wildcard that matches any value. NOTE: don't use this to look for courses
        as the course_id is required. Use get_courses.

        location: either a Location possibly w/ None as wildcards for category or name or
        a Locator with at least a package_id and branch but possibly no block_id.

        depth: An argument that some module stores may use to prefetch
            descendents of the queried modules for more efficient results later
            in the request. The depth is counted in the number of calls to
            get_children() to cache. None indicates to cache all descendents
        """
        if not (course_id or hasattr(location, 'package_id')):
            raise Exception("Must pass in a course_id when calling get_items()")

        store = self._get_modulestore_for_courseid(course_id or getattr(location, 'package_id'))
        # translate won't work w/ missing fields so work around it
        if store.reference_type == Location:
            if not self.use_locations:
                if getattr(location, 'block_id', False):
                    location = self._incoming_reference_adaptor(store, course_id, location)
                else:
                    # get the course's location
                    location = loc_mapper().translate_locator_to_location(location, get_course=True)
                    # now remove the unknowns
                    location = location.replace(
                        category=qualifiers.get('category', None),
                        name=None
                    )
        else:
            if self.use_locations:
                if not isinstance(location, Location):
                    location = Location(location)
                try:
                    location.ensure_fully_specified()
                    location = loc_mapper().translate_location(
                        course_id, location, location.revision == 'published', True
                    )
                except InsufficientSpecificationError:
                    # construct the Locator by hand
                    if location.category is not None and qualifiers.get('category', False):
                        qualifiers['category'] = location.category
                    location = loc_mapper().translate_location_to_course_locator(
                        course_id, location, location.revision == 'published'
                    )
        xblocks = store.get_items(location, course_id, depth, qualifiers)
        xblocks = [self._outgoing_xblock_adaptor(store, course_id, xblock) for xblock in xblocks]
        return xblocks
Example #13
0
    def ensure_loc_maps_exist(self, store_name):
        """
        Ensure location maps exist for every course in the modulestore whose
        name is the given name (mostly used for 'xml'). It creates maps for any
        missing ones.

        NOTE: will only work if the given store is Location based. If it's not,
        it raises NotImplementedError
        """
        store = self.modulestores[store_name]
        if store.reference_type != Location:
            raise ValueError(u"Cannot create maps from %s" % store.reference_type)
        for course in store.get_courses():
            loc_mapper().translate_location(course.location.course_id, course.location)
Example #14
0
 def mapper(found_id):
     """
     Convert the found id to BlockUsageLocator block_id
     """
     location = from_base_addr.replace(category=None, name=found_id)
     # NOTE without category, it cannot create a new mapping if there's not one already
     return loc_mapper().translate_location(course_id, location).block_id
Example #15
0
def orphan_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
    """
    View for handling orphan related requests. GET gets all of the current orphans.
    DELETE removes all orphans (requires is_staff access)

    An orphan is a block whose category is not in the DETACHED_CATEGORY list, is not the root, and is not reachable
    from the root via children

    :param request:
    :param package_id: Locator syntax package_id
    """
    location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
    # DHM: when split becomes back-end, move or conditionalize this conversion
    old_location = loc_mapper().translate_locator_to_location(location)
    if request.method == 'GET':
        if has_course_access(request.user, old_location):
            return JsonResponse(modulestore().get_orphans(old_location, 'draft'))
        else:
            raise PermissionDenied()
    if request.method == 'DELETE':
        if request.user.is_staff:
            items = modulestore().get_orphans(old_location, 'draft')
            for itemloc in items:
                modulestore('draft').delete_item(itemloc, delete_all_versions=True)
            return JsonResponse({'deleted': items})
        else:
            raise PermissionDenied()
Example #16
0
    def drop_mongo_collections(modulestore_type=MONGO_MODULESTORE_TYPE):
        """
        If using a Mongo-backed modulestore & contentstore, drop the collections.
        """
        store = modulestore()
        if hasattr(store, '_get_modulestore_by_type'):
            store = store._get_modulestore_by_type(modulestore_type)  # pylint: disable=W0212
        if hasattr(store, 'collection'):
            connection = store.collection.database.connection
            store.collection.drop()
            connection.close()
        elif hasattr(store, 'close_all_connections'):
            store.close_all_connections()
        elif hasattr(store, 'db'):
            connection = store.db.connection
            connection.drop_database(store.db.name)
            connection.close()

        if contentstore().fs_files:
            db = contentstore().fs_files.database
            db.connection.drop_database(db)
            db.connection.close()

        location_mapper = loc_mapper()
        if location_mapper.db:
            location_mapper.location_map.drop()
            location_mapper.db.connection.close()
Example #17
0
def _get_module_info(usage_loc, rewrite_static_links=True):
    """
    metadata, data, id representation of a leaf module fetcher.
    :param usage_loc: A BlockUsageLocator
    """
    old_location = loc_mapper().translate_locator_to_location(usage_loc)
    store = get_modulestore(old_location)
    try:
        module = store.get_item(old_location)
    except ItemNotFoundError:
        if old_location.category in CREATE_IF_NOT_FOUND:
            # Create a new one for certain categories only. Used for course info handouts.
            store.create_and_save_xmodule(old_location)
            module = store.get_item(old_location)
        else:
            raise

    data = getattr(module, 'data', '')
    if rewrite_static_links:
        # we pass a partially bogus course_id as we don't have the RUN information passed yet
        # through the CMS. Also the contentstore is also not RUN-aware at this point in time.
        data = replace_static_urls(
            data,
            None,
            course_id=module.location.org + '/' + module.location.course + '/BOGUS_RUN_REPLACE_WHEN_AVAILABLE'
        )

    # Note that children aren't being returned until we have a use case.
    return {
        'id': unicode(usage_loc),
        'data': data,
        'metadata': own_metadata(module)
    }
Example #18
0
def _course_json(course, course_id, url_name, position=0):
    locator = loc_mapper().translate_location(course_id, course.location, published=False, add_entry_if_missing=True)
    is_container = course.has_children

    category = course.category

    result = {
        'display_name': course.display_name,
        'id': unicode(locator),
        'category': category,
        'is_draft': getattr(course, 'is_draft', False),
        'is_container': is_container
    }

    if category in ['sequential', 'chapter']:
        url_name = url_name + '/' + course.url_name
    elif category == "vertical":
        result['unit_url'] = url_name + '/' + str(position)
    elif category == "video":
        result[category + '_url'] = course.html5_sources[0] if len(course.html5_sources) > 0 else ""

    if is_container:
        children = []
        for idx, child in enumerate(course.get_children()):
            try:
                children.append(_course_json(child, course_id, url_name, (idx + 1)))
            except:
                continue

        result['children'] = children

    return result
Example #19
0
def get_cms_course_link(course):
    """
    Returns a link to course_index for editing the course in cms,
    assuming that the course is actually cms-backed.
    """
    locator = loc_mapper().translate_location(course.location.course_id, course.location, False, True)
    return "//" + settings.CMS_BASE + locator.url_reverse("course/", "")
Example #20
0
def get_course_settings(request, org, course, name):
    """
    Send models and views as well as html for editing the course settings to
    the client.

    org, course, name: Attributes of the Location for the item to edit
    """
    location = get_location_and_verify_access(request, org, course, name)

    course_module = modulestore().get_item(location)

    new_loc = loc_mapper().translate_location(location.course_id, location, False, True)
    upload_asset_url = new_loc.url_reverse('assets/', '')

    return render_to_response('settings.html', {
        'context_course': course_module,
        'course_location': location,
        'details_url': reverse(course_settings_updates,
                               kwargs={"org": org,
                                       "course": course,
                                       "name": name,
                                       "section": "details"}),
        'about_page_editable': not settings.MITX_FEATURES.get(
            'ENABLE_MKTG_SITE', False
        ),
        'upload_asset_url': upload_asset_url
    })
    def handle(self, *args, **options):
        if len(args) < 2:
            raise CommandError(
                "rollback_split_course requires 2 arguments (org offering)"
            )

        try:
            locator = CourseLocator(org=args[0], offering=args[1])
        except ValueError:
            raise CommandError("Invalid org or offering string {}, {}".format(*args))

        location = loc_mapper().translate_locator_to_location(locator, get_course=True)
        if not location:
            raise CommandError(
                "This course does not exist in the old Mongo store. "
                "This command is designed to rollback a course, not delete "
                "it entirely."
            )
        old_mongo_course = modulestore('direct').get_item(location)
        if not old_mongo_course:
            raise CommandError(
                "This course does not exist in the old Mongo store. "
                "This command is designed to rollback a course, not delete "
                "it entirely."
            )

        try:
            modulestore('split').delete_course(locator)
        except ItemNotFoundError:
            raise CommandError("No course found with locator {}".format(locator))

        print(
            'Course rolled back successfully. To delete this course entirely, '
            'call the "delete_course" management command.'
        )
Example #22
0
    def setUp(self):
        """Create initial data."""
        super(Basetranscripts, self).setUp()
        self.unicode_locator = unicode(loc_mapper().translate_location(
            self.course.location.course_id, self.course.location, False, True
        ))

        # Add video module
        data = {
            'parent_locator': self.unicode_locator,
            'category': 'video',
            'type': 'video'
        }
        resp = self.client.ajax_post('/xblock', data)
        self.item_location = self._get_location(resp)
        self.assertEqual(resp.status_code, 200)

        # hI10vDNYz4M - valid Youtube ID with transcripts.
        # JMD_ifUUfsU, AKqURZnYqpk, DYpADpL7jAY - valid Youtube IDs without transcripts.
        data = '<video youtube="0.75:JMD_ifUUfsU,1.0:hI10vDNYz4M,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY" />'
        modulestore().update_item(self.item_location, data)

        self.item = modulestore().get_item(self.item_location)

        # Remove all transcripts for current module.
        self.clear_subs_content()
Example #23
0
    def setUp(self):
        """
        These tests need a user in the DB so that the django Test Client
        can log them in.
        They inherit from the ModuleStoreTestCase class so that the mongodb collection
        will be cleared out before each test case execution and deleted
        afterwards.
        """
        uname = 'testuser'
        email = '*****@*****.**'
        password = '******'

        # Create the user so we can log them in.
        self.user = User.objects.create_user(uname, email, password)

        # Note that we do not actually need to do anything
        # for registration if we directly mark them active.
        self.user.is_active = True
        # Staff has access to view all courses
        self.user.is_staff = True
        self.user.save()

        self.client = AjaxEnabledTestClient()
        self.client.login(username=uname, password=password)

        self.course = CourseFactory.create(
            org='MITx',
            number='999',
            display_name='Robot Super Course',
        )
        self.course_location = self.course.location
        self.course_locator = loc_mapper().translate_location(
            self.course.location.course_id, self.course.location, False, True
        )
Example #24
0
def mobi_course_action(request, course_id, action):
    if action not in ["structure", "updates", "handouts"]:
        return JsonResponse(status=404)

    try:
        if action != "structure":
            course_locator = BlockUsageLocator(package_id=course_id, branch='draft', version_guid=None, block_id=action)
            course_location = loc_mapper().translate_locator_to_location(course_locator)

            def get_course_info_by_action(location, action):
                """
                return data depend on action
                """
                if action == "updates":
                    return get_course_updates(location, None)
                elif action == "handouts":
                    module = get_modulestore(location).get_item(location)
                    return {"data": getattr(module, 'data', '')}
                else:
                    return {"error": 20000, "errmsg": "some error occur!"}

            if not course_location:
                return JsonResponse([])
            else:
                return JsonResponse(get_course_info_by_action(course_location, action))
        else:
            return JsonResponse(_course_json(request, course_id))
    except:
        return HttpResponseBadRequest("Fail to get data")
 def get_section_grader_type(location):
     old_location = loc_mapper().translate_locator_to_location(location)
     descriptor = get_modulestore(old_location).get_item(old_location)
     return {
         "graderType": descriptor.format if descriptor.format is not None else 'notgraded',
         "location": unicode(location),
     }
    def test_no_ol_course_update(self):
        '''Test trying to add to a saved course_update which is not an ol.'''
        # get the updates and set to something wrong
        location = self.course.location.replace(category='course_info', name='updates')
        modulestore('direct').create_and_save_xmodule(location)
        course_updates = modulestore('direct').get_item(location)
        course_updates.data = 'bad news'
        modulestore('direct').update_item(course_updates, self.user.id)

        init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">'
        content = init_content + '</iframe>'
        payload = {'content': content, 'date': 'January 8, 2013'}

        update_locator = loc_mapper().translate_location(
            self.course.location.course_id, location, False, True
        )
        course_update_url = update_locator.url_reverse('course_info_update/')
        resp = self.client.ajax_post(course_update_url, payload)

        payload = json.loads(resp.content)

        self.assertHTMLEqual(payload['content'], content)

        # now confirm that the bad news and the iframe make up 2 updates
        resp = self.client.get_json(course_update_url)
        payload = json.loads(resp.content)
        self.assertTrue(len(payload) == 2)
Example #27
0
def course_index(request, course_id, branch, version_guid, block):
    """
    Display an editable course overview.

    org, course, name: Attributes of the Location for the item to edit
    """
    location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
    # TODO: when converting to split backend, if location does not have a usage_id,
    # we'll need to get the course's root block_id
    if not has_access(request.user, location):
        raise PermissionDenied()


    old_location = loc_mapper().translate_locator_to_location(location)

    lms_link = get_lms_link_for_item(old_location)

    course = modulestore().get_item(old_location, depth=3)
    sections = course.get_children()

    return render_to_response('overview.html', {
        'context_course': course,
        'lms_link': lms_link,
        'sections': sections,
        'course_graders': json.dumps(
            CourseGradingModel.fetch(course.location).graders
        ),
        'parent_location': course.location,
        'new_section_category': 'chapter',
        'new_subsection_category': 'sequential',
        'new_unit_category': 'vertical',
        'category': 'vertical'
    })
    def test_preview_handler_locator(self):
        """
        Test for calling get_preview_html when descriptor.location is a Locator.
        """
        course = CourseFactory.create()
        html = ItemFactory.create(
            parent_location=course.location,
            category="html",
            data={'data': "<html>foobar</html>"}
        )

        locator = loc_mapper().translate_location(
            course.location.course_id, html.location, True, True
        )

        # Change the stored location to a locator.
        html.location = locator
        html.save()

        request = RequestFactory().get('/dummy-url')
        request.user = UserFactory()
        request.session = {}

        # Must call get_preview_fragment directly, as going through xblock RESTful API will attempt
        # to use item.location as a Location.
        html = get_preview_fragment(request, html).content
        # Verify student view html is returned, and there are no old locations in it.
        self.assertRegexpMatches(
            html,
            'data-usage-id="MITx.999.Robot_Super_Course;_branch;_published;_block;_html_[0-9]*"'
        )
        self.assertRegexpMatches(html, '<html>foobar</html>')
        self.assertNotRegexpMatches(html, 'i4x')
Example #29
0
def course_audit_api(request, course_id, operation):
    re_json = {"success": False}

    request_method = request.method
    if request_method != "POST":
        return JsonResponse(re_json)
    # get course location and module infomation
    try:
        course_location_info = course_id.split('.')
        locator = BlockUsageLocator(package_id=course_id, branch='draft', version_guid=None, block_id=course_location_info[-1])
        course_location = loc_mapper().translate_locator_to_location(locator)
        course_module = get_modulestore(course_location).get_item(course_location)

        instructors = CourseInstructorRole(locator).users_with_role()
        if len(instructors) <= 0:
            return JsonResponse(re_json)

        user = instructors[0]

        meta_json = {}
        if operation == "pass":
            meta_json["course_audit"] = 1
        elif operation == "offline":
            meta_json["course_audit"] = 0
        else:
            return JsonResponse(re_json)

        re_json["success"] = True
        CourseMetadata.update_from_json(course_module, meta_json, True, user)
        return JsonResponse(re_json)
    except:
        return JsonResponse(re_json)
Example #30
0
    def get_courses(self):
        '''
        Returns a list containing the top level XModuleDescriptors of the courses
        in this modulestore.
        '''
        # order the modulestores and ensure no dupes (default may be a dupe of a named store)
        # remove 'draft' as we know it's a functional dupe of 'direct' (ugly hardcoding)
        stores = set([value for key, value in self.modulestores.iteritems() if key != 'draft'])
        stores = sorted(stores, cmp=_compare_stores)

        courses = {}  # a dictionary of stringified course locations to course objects
        has_locators = any(issubclass(CourseLocator, store.reference_type) for store in stores)
        for store in stores:
            store_courses = store.get_courses()
            # filter out ones which were fetched from earlier stores but locations may not be ==
            for course in store_courses:
                course_location = unicode(course.location)
                if course_location not in courses:
                    if has_locators and isinstance(course.location, Location):
                        # see if a locator version of course is in the result
                        try:
                            # if there's no existing mapping, then the course can't have been in split
                            course_locator = loc_mapper().translate_location(
                                course.location,
                                add_entry_if_missing=False
                            )
                            if unicode(course_locator) not in courses:
                                courses[course_location] = course
                        except ItemNotFoundError:
                            courses[course_location] = course
                    else:
                        courses[course_location] = course

        return courses.values()
Example #31
0
def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
    """
    The restful handler for xblock requests.

    DELETE
        json: delete this xblock instance from the course. Supports query parameters "recurse" to delete
        all children and "all_versions" to delete from all (mongo) versions.
    GET
        json: returns representation of the xblock (locator id, data, and metadata).
              if ?fields=graderType, it returns the graderType for the unit instead of the above.
        html: returns HTML for rendering the xblock (which includes both the "preview" view and the "editor" view)
    PUT or POST
        json: if xblock locator is specified, update the xblock instance. The json payload can contain
              these fields, all optional:
                :data: the new value for the data.
                :children: the locator ids of children for this xblock.
                :metadata: new values for the metadata fields. Any whose values are None will be deleted not set
                       to None! Absent ones will be left alone.
                :nullout: which metadata fields to set to None
                :graderType: change how this unit is graded
                :publish: can be one of three values, 'make_public, 'make_private', or 'create_draft'
              The JSON representation on the updated xblock (minus children) is returned.

              if xblock locator is not specified, create a new xblock instance, either by duplicating
              an existing xblock, or creating an entirely new one. The json playload can contain
              these fields:
                :parent_locator: parent for new xblock, required for both duplicate and create new instance
                :duplicate_source_locator: if present, use this as the source for creating a duplicate copy
                :category: type of xblock, required if duplicate_source_locator is not present.
                :display_name: name for new xblock, optional
                :boilerplate: template name for populating fields, optional and only used
                     if duplicate_source_locator is not present
              The locator (and old-style id) for the created xblock (minus children) is returned.
    """
    if package_id is not None:
        locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
        if not has_course_access(request.user, locator):
            raise PermissionDenied()
        old_location = loc_mapper().translate_locator_to_location(locator)

        if request.method == 'GET':
            accept_header = request.META.get('HTTP_ACCEPT', 'application/json')

            if 'application/json' in accept_header:
                fields = request.REQUEST.get('fields', '').split(',')
                if 'graderType' in fields:
                    # right now can't combine output of this w/ output of _get_module_info, but worthy goal
                    return JsonResponse(CourseGradingModel.get_section_grader_type(locator))
                # TODO: pass fields to _get_module_info and only return those
                rsp = _get_module_info(locator)
                return JsonResponse(rsp)
            else:
                return HttpResponse(status=406)

        elif request.method == 'DELETE':
            delete_children = str_to_bool(request.REQUEST.get('recurse', 'False'))
            delete_all_versions = str_to_bool(request.REQUEST.get('all_versions', 'False'))

            return _delete_item_at_location(old_location, delete_children, delete_all_versions, request.user)
        else:  # Since we have a package_id, we are updating an existing xblock.
            return _save_item(
                request,
                locator,
                old_location,
                data=request.json.get('data'),
                children=request.json.get('children'),
                metadata=request.json.get('metadata'),
                nullout=request.json.get('nullout'),
                grader_type=request.json.get('graderType'),
                publish=request.json.get('publish'),
            )
    elif request.method in ('PUT', 'POST'):
        if 'duplicate_source_locator' in request.json:
            parent_locator = BlockUsageLocator(request.json['parent_locator'])
            duplicate_source_locator = BlockUsageLocator(request.json['duplicate_source_locator'])

            # _duplicate_item is dealing with locations to facilitate the recursive call for
            # duplicating children.
            parent_location = loc_mapper().translate_locator_to_location(parent_locator)
            duplicate_source_location = loc_mapper().translate_locator_to_location(duplicate_source_locator)
            dest_location = _duplicate_item(
                parent_location,
                duplicate_source_location,
                request.json.get('display_name'),
                request.user,
            )
            course_location = loc_mapper().translate_locator_to_location(BlockUsageLocator(parent_locator), get_course=True)
            dest_locator = loc_mapper().translate_location(course_location.course_id, dest_location, False, True)
            return JsonResponse({"locator": unicode(dest_locator)})
        else:
            return _create_item(request)
    else:
        return HttpResponseBadRequest(
            "Only instance creation is supported without a package_id.",
            content_type="text/plain"
        )
Example #32
0
def create_new_course(request):
    """
    Create a new course.

    Returns the URL for the course overview page.
    """
    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')

    try:
        dest_location = Location(u'i4x', org, number, u'course', run)
    except InvalidLocationError as error:
        return JsonResponse({
            "ErrMsg":
            _("Unable to create course '{name}'.\n\n{err}").format(
                name=display_name, err=error.message)
        })

    # see if the course already exists
    existing_course = None
    try:
        existing_course = modulestore('direct').get_item(dest_location)
    except ItemNotFoundError:
        pass
    if existing_course is not None:
        return JsonResponse({
            'ErrMsg':
            _('There is already a course defined with the same '
              'organization, course number, and course run. Please '
              'change either organization or course number to be '
              'unique.'),
            'OrgErrMsg':
            _('Please change either the organization or '
              'course number so that it is unique.'),
            'CourseErrMsg':
            _('Please change either the organization or '
              'course number so that it is unique.'),
        })

    # dhm: this query breaks the abstraction, but I'll fix it when I do my suspended refactoring of this
    # file for new locators. get_items should accept a query rather than requiring it be a legal location
    course_search_location = bson.son.SON({
        '_id.tag':
        'i4x',
        # cannot pass regex to Location constructor; thus this hack
        # pylint: disable=E1101
        '_id.org':
        re.compile(u'^{}$'.format(dest_location.org),
                   re.IGNORECASE | re.UNICODE),
        # pylint: disable=E1101
        '_id.course':
        re.compile(u'^{}$'.format(dest_location.course),
                   re.IGNORECASE | re.UNICODE),
        '_id.category':
        'course',
    })
    courses = modulestore().collection.find(course_search_location,
                                            fields=('_id'))
    if courses.count() > 0:
        return JsonResponse({
            'ErrMsg':
            _('There is already a course defined with the same '
              'organization and course number. Please '
              'change at least one field to be unique.'),
            'OrgErrMsg':
            _('Please change either the organization or '
              'course number so that it is unique.'),
            'CourseErrMsg':
            _('Please change either the organization or '
              'course number so that it is unique.'),
        })

    # instantiate the CourseDescriptor and then persist it
    # note: no system to pass
    if display_name is None:
        metadata = {}
    else:
        metadata = {'display_name': display_name}
    modulestore('direct').create_and_save_xmodule(dest_location,
                                                  metadata=metadata)
    new_course = modulestore('direct').get_item(dest_location)

    # clone a default 'about' overview module as well
    dest_about_location = dest_location.replace(category='about',
                                                name='overview')
    overview_template = AboutDescriptor.get_template('overview.yaml')
    modulestore('direct').create_and_save_xmodule(
        dest_about_location,
        system=new_course.system,
        definition_data=overview_template.get('data'))

    initialize_course_tabs(new_course, request.user)

    new_location = loc_mapper().translate_location(
        new_course.location.course_id, new_course.location, False, True)
    # 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_location).add_users(request.user)
    auth.add_users(request.user, CourseStaffRole(new_location), request.user)

    # seed the forums
    seed_permissions_roles(new_course.location.course_id)

    # auto-enroll the course creator in the course so that "View Live" will
    # work.
    CourseEnrollment.enroll(request.user, new_course.location.course_id)

    return JsonResponse({'url': new_location.url_reverse("course/", "")})
Example #33
0
def _course_team_user(request, locator, email):
    """
    Handle the add, remove, promote, demote requests ensuring the requester has authority
    """
    # check that logged in user has permissions to this item
    if has_access(request.user, locator, role=INSTRUCTOR_ROLE_NAME):
        # instructors have full permissions
        pass
    elif has_access(request.user, locator,
                    role=STAFF_ROLE_NAME) and email == request.user.email:
        # staff can only affect themselves
        pass
    else:
        msg = {"error": _("Insufficient permissions")}
        return JsonResponse(msg, 400)

    try:
        user = User.objects.get(email=email)
    except:
        msg = {
            "error":
            _("Could not find user by email address '{email}'.").format(
                email=email),
        }
        return JsonResponse(msg, 404)

    # role hierarchy: "instructor" has more permissions than "staff" (in a course)
    roles = ["instructor", "staff"]

    if request.method == "GET":
        # just return info about the user
        msg = {
            "email": user.email,
            "active": user.is_active,
            "role": None,
        }
        # what's the highest role that this user has?
        groupnames = set(g.name for g in user.groups.all())
        for role in roles:
            role_groupname = get_course_groupname_for_role(locator, role)
            if role_groupname in groupnames:
                msg["role"] = role
                break
        return JsonResponse(msg)

    # can't modify an inactive user
    if not user.is_active:
        msg = {
            "error":
            _('User {email} has registered but has not yet activated his/her account.'
              ).format(email=email),
        }
        return JsonResponse(msg, 400)

    # make sure that the role groups exist
    groups = {}
    for role in roles:
        groupname = get_course_groupname_for_role(locator, role)
        group, __ = Group.objects.get_or_create(name=groupname)
        groups[role] = group

    if request.method == "DELETE":
        # remove all roles in this course from this user: but fail if the user
        # is the last instructor in the course team
        instructors = set(groups["instructor"].user_set.all())
        staff = set(groups["staff"].user_set.all())
        if user in instructors and len(instructors) == 1:
            msg = {
                "error":
                _("You may not remove the last instructor from a course")
            }
            return JsonResponse(msg, 400)

        if user in instructors:
            user.groups.remove(groups["instructor"])
        if user in staff:
            user.groups.remove(groups["staff"])
        user.save()
        return JsonResponse()

    # all other operations require the requesting user to specify a role
    role = request.json.get("role", request.POST.get("role"))
    if role is None:
        return JsonResponse({"error": _("`role` is required")}, 400)

    old_location = loc_mapper().translate_locator_to_location(locator)
    if role == "instructor":
        if not has_access(request.user, locator, role=INSTRUCTOR_ROLE_NAME):
            msg = {"error": _("Only instructors may create other instructors")}
            return JsonResponse(msg, 400)
        user.groups.add(groups["instructor"])
        user.save()
        # auto-enroll the course creator in the course so that "View Live" will work.
        CourseEnrollment.enroll(user, old_location.course_id)
    elif role == "staff":
        # if we're trying to downgrade a user from "instructor" to "staff",
        # make sure we have at least one other instructor in the course team.
        instructors = set(groups["instructor"].user_set.all())
        if user in instructors:
            if len(instructors) == 1:
                msg = {
                    "error":
                    _("You may not remove the last instructor from a course")
                }
                return JsonResponse(msg, 400)
            user.groups.remove(groups["instructor"])
        user.groups.add(groups["staff"])
        user.save()
        # auto-enroll the course creator in the course so that "View Live" will work.
        CourseEnrollment.enroll(user, old_location.course_id)

    return JsonResponse()
Example #34
0
    def test_course_updates_compatibility(self):
        '''
        Test that course updates doesn't break on old data (content in 'data' field).
        Note: new data will save as list in 'items' field.
        '''
        # get the updates and populate 'data' field with some data.
        location = self.course.location.replace(category='course_info', name='updates')
        modulestore('direct').create_and_save_xmodule(location)
        course_updates = modulestore('direct').get_item(location)
        update_date = u"January 23, 2014"
        update_content = u"Hello world!"
        update_data = u"<ol><li><h2>" + update_date + "</h2>" + update_content + "</li></ol>"
        course_updates.data = update_data
        modulestore('direct').update_item(course_updates, self.user)

        update_locator = loc_mapper().translate_location(
            self.course.location.course_id, location, False, True
        )
        # test getting all updates list
        course_update_url = update_locator.url_reverse('course_info_update/')
        resp = self.client.get_json(course_update_url)
        payload = json.loads(resp.content)
        self.assertEqual(payload, [{u'date': update_date, u'content': update_content, u'id': 1}])
        self.assertTrue(len(payload) == 1)

        # test getting single update item
        first_update_url = update_locator.url_reverse('course_info_update', str(payload[0]['id']))
        resp = self.client.get_json(first_update_url)
        payload = json.loads(resp.content)
        self.assertEqual(payload, {u'date': u'January 23, 2014', u'content': u'Hello world!', u'id': 1})
        self.assertHTMLEqual(update_date, payload['date'])
        self.assertHTMLEqual(update_content, payload['content'])

        # test that while updating it converts old data (with string format in 'data' field)
        # to new data (with list format in 'items' field) and respectively updates 'data' field.
        course_updates = modulestore('direct').get_item(location)
        self.assertEqual(course_updates.items, [])
        # now try to update first update item
        update_content = 'Testing'
        payload = {'content': update_content, 'date': update_date}
        resp = self.client.ajax_post(
            course_update_url + '/1', payload, HTTP_X_HTTP_METHOD_OVERRIDE="PUT", REQUEST_METHOD="POST"
        )
        self.assertHTMLEqual(update_content, json.loads(resp.content)['content'])
        course_updates = modulestore('direct').get_item(location)
        self.assertEqual(course_updates.items, [{u'date': update_date, u'content': update_content, u'id': 1}])
        # course_updates 'data' field should update accordingly
        update_data = u"<section><article><h2>{date}</h2>{content}</article></section>".format(date=update_date, content=update_content)
        self.assertEqual(course_updates.data, update_data)

        # test delete course update item (soft delete)
        course_updates = modulestore('direct').get_item(location)
        self.assertEqual(course_updates.items, [{u'date': update_date, u'content': update_content, u'id': 1}])
        # now try to delete first update item
        resp = self.client.delete(course_update_url + '/1')
        self.assertEqual(json.loads(resp.content), [])
        # confirm that course update is soft deleted ('status' flag set to 'deleted') in db
        course_updates = modulestore('direct').get_item(location)
        self.assertEqual(course_updates.items,
                         [{u'date': update_date, u'content': update_content, u'id': 1, u'status': 'deleted'}])

        # now try to get deleted update
        resp = self.client.get_json(course_update_url + '/1')
        payload = json.loads(resp.content)
        self.assertEqual(payload.get('error'), u"Course update not found.")
        self.assertEqual(resp.status_code, 404)

        # now check that course update don't munges html
        update_content = u"""&lt;problem>
                           &lt;p>&lt;/p>
                           &lt;multiplechoiceresponse>
                           <pre>&lt;problem>
                               &lt;p>&lt;/p></pre>
                           <div><foo>bar</foo></div>"""
        payload = {'content': update_content, 'date': update_date}
        resp = self.client.ajax_post(
            course_update_url, payload, REQUEST_METHOD="POST"
        )
        self.assertHTMLEqual(update_content, json.loads(resp.content)['content'])
 def _get_locator(self, resp):
     """ Returns the locator and old-style location (as a string) from the response returned by a create operation. """
     locator = json.loads(resp.content).get('locator')
     return locator, loc_mapper().translate_locator_to_location(
         BlockUsageLocator(locator)).url()
Example #36
0
def subsection_handler(request,
                       tag=None,
                       package_id=None,
                       branch=None,
                       version_guid=None,
                       block=None):
    """
    The restful handler for subsection-specific requests.

    GET
        html: return html page for editing a subsection
        json: not currently supported
    """
    if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
        locator = BlockUsageLocator(package_id=package_id,
                                    branch=branch,
                                    version_guid=version_guid,
                                    block_id=block)
        try:
            old_location, course, item, lms_link = _get_item_in_course(
                request, locator)
        except ItemNotFoundError:
            return HttpResponseBadRequest()

        preview_link = get_lms_link_for_item(
            old_location, course_id=course.location.course_id, preview=True)

        # make sure that location references a 'sequential', otherwise return
        # BadRequest
        if item.location.category != 'sequential':
            return HttpResponseBadRequest()

        parent = get_parent_xblock(item)

        # remove all metadata from the generic dictionary that is presented in a
        # more normalized UI. We only want to display the XBlocks fields, not
        # the fields from any mixins that have been added
        fields = getattr(item, 'unmixed_class', item.__class__).fields

        policy_metadata = dict(
            (field.name, field.read_from(item)) for field in fields.values()
            if field.name not in ['display_name', 'start', 'due', 'format']
            and field.scope == Scope.settings)

        can_view_live = False
        subsection_units = item.get_children()
        for unit in subsection_units:
            state = compute_publish_state(unit)
            if state in (PublishState.public, PublishState.draft):
                can_view_live = True
                break

        course_locator = loc_mapper().translate_location(
            course.location.course_id, course.location, False, True)

        return render_to_response(
            'edit_subsection.html', {
                'subsection':
                item,
                'context_course':
                course,
                'new_unit_category':
                'vertical',
                'lms_link':
                lms_link,
                'preview_link':
                preview_link,
                'course_graders':
                json.dumps(CourseGradingModel.fetch(course_locator).graders),
                'parent_item':
                parent,
                'locator':
                locator,
                'policy_metadata':
                policy_metadata,
                'subsection_units':
                subsection_units,
                'can_view_live':
                can_view_live
            })
    else:
        return HttpResponseBadRequest("Only supports html requests")
Example #37
0
 def setUp(self):
     super(AssetsTestCase, self).setUp()
     location = loc_mapper().translate_location(
         self.course.location.course_id, self.course.location, False, True)
     self.url = location.url_reverse('assets/', '')
Example #38
0
def add_staff_markup(user, block, view, frag, context):  # pylint: disable=unused-argument
    """
    Updates the supplied module with a new get_html function that wraps
    the output of the old get_html function with additional information
    for admin users only, including a histogram of student answers, the
    definition of the xmodule, and a link to view the module in Studio
    if it is a Studio edited, mongo stored course.

    Does nothing if module is a SequenceModule.
    """
    # TODO: make this more general, eg use an XModule attribute instead
    if isinstance(block, VerticalModule) and (
            not context or not context.get('child_of_vertical', False)):
        # check that the course is a mongo backed Studio course before doing work
        is_mongo_course = modulestore().get_modulestore_type(
            block.course_id) == MONGO_MODULESTORE_TYPE
        is_studio_course = block.course_edit_method == "Studio"

        if is_studio_course and is_mongo_course:
            # get relative url/location of unit in Studio
            locator = loc_mapper().translate_location(block.course_id,
                                                      block.location, False,
                                                      True)
            # build edit link to unit in CMS
            edit_link = "//" + settings.CMS_BASE + locator.url_reverse(
                'unit', '')
            # return edit link in rendered HTML for display
            return wrap_fragment(
                frag,
                render_to_string("edit_unit_link.html", {
                    'frag_content': frag.content,
                    'edit_link': edit_link
                }))
        else:
            return frag

    if isinstance(block, SequenceModule):
        return frag

    block_id = block.id
    if block.has_score and settings.FEATURES.get(
            'DISPLAY_HISTOGRAMS_TO_STAFF'):
        histogram = grade_histogram(block_id)
        render_histogram = len(histogram) > 0
    else:
        histogram = None
        render_histogram = False

    if settings.FEATURES.get('ENABLE_LMS_MIGRATION') and hasattr(
            block.runtime, 'filestore'):
        [filepath, filename] = getattr(block, 'xml_attributes',
                                       {}).get('filename', ['', None])
        osfs = block.runtime.filestore
        if filename is not None and osfs.exists(filename):
            # if original, unmangled filename exists then use it (github
            # doesn't like symlinks)
            filepath = filename
        data_dir = block.static_asset_path or osfs.root_path.rsplit('/')[-1]
        giturl = block.giturl or 'https://github.com/MITx'
        edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath)
    else:
        edit_link = False
        # Need to define all the variables that are about to be used
        giturl = ""
        data_dir = ""

    source_file = block.source_file  # source used to generate the problem XML, eg latex or word

    # useful to indicate to staff if problem has been released or not
    # TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
    now = datetime.datetime.now(UTC())
    is_released = "unknown"
    mstart = block.start

    if mstart is not None:
        is_released = "<font color='red'>Yes!</font>" if (
            now > mstart) else "<font color='green'>Not yet</font>"

    field_contents = []
    for name, field in block.fields.items():
        try:
            field_contents.append((name, field.read_from(block)))
        except InvalidScopeError:
            log.warning("Unable to read field in Staff Debug information",
                        exc_info=True)
            field_contents.append((name, "WARNING: Unable to read field"))

    staff_context = {
        'fields':
        field_contents,
        'xml_attributes':
        getattr(block, 'xml_attributes', {}),
        'location':
        block.location,
        'xqa_key':
        block.xqa_key,
        'source_file':
        source_file,
        'source_url':
        '%s/%s/tree/master/%s' % (giturl, data_dir, source_file),
        'category':
        str(block.__class__.__name__),
        # Template uses element_id in js function names, so can't allow dashes
        'element_id':
        block.location.html_id().replace('-', '_'),
        'edit_link':
        edit_link,
        'user':
        user,
        'xqa_server':
        settings.FEATURES.get('USE_XQA_SERVER',
                              'http://*****:*****@content-qa.mitx.mit.edu/xqa'),
        'histogram':
        json.dumps(histogram),
        'render_histogram':
        render_histogram,
        'block_content':
        frag.content,
        'is_released':
        is_released,
    }
    return wrap_fragment(
        frag, render_to_string("staff_problem_info.html", staff_context))
Example #39
0
    def test_course_update(self):
        '''Go through each interface and ensure it works.'''
        def get_response(content, date):
            """
            Helper method for making call to server and returning response.

            Does not supply a provided_id.
            """
            payload = {'content': content, 'date': date}
            url = update_locator.url_reverse('course_info_update/')

            resp = self.client.ajax_post(url, payload)
            self.assertContains(resp, '', status_code=200)

            return json.loads(resp.content)

        course_locator = loc_mapper().translate_location(
            self.course.location.course_id, self.course.location, False, True
        )
        resp = self.client.get_html(course_locator.url_reverse('course_info/'))
        self.assertContains(resp, 'Course Updates', status_code=200)
        update_locator = loc_mapper().translate_location(
            self.course.location.course_id, self.course.location.replace(category='course_info', name='updates'),
            False, True
        )

        init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">'
        content = init_content + '</iframe>'
        payload = get_response(content, 'January 8, 2013')
        self.assertHTMLEqual(payload['content'], content)

        first_update_url = update_locator.url_reverse('course_info_update', str(payload['id']))
        content += '<div>div <p>p<br/></p></div>'
        payload['content'] = content
        # POST requests were coming in w/ these header values causing an error; so, repro error here
        resp = self.client.ajax_post(
            first_update_url, payload, HTTP_X_HTTP_METHOD_OVERRIDE="PUT", REQUEST_METHOD="POST"
        )

        self.assertHTMLEqual(content, json.loads(resp.content)['content'],
                             "iframe w/ div")
        # refetch using provided id
        refetched = self.client.get_json(first_update_url)
        self.assertHTMLEqual(
            content, json.loads(refetched.content)['content'], "get w/ provided id"
        )

        # now put in an evil update
        content = '<ol/>'
        payload = get_response(content, 'January 11, 2013')
        self.assertHTMLEqual(content, payload['content'], "self closing ol")

        course_update_url = update_locator.url_reverse('course_info_update/')
        resp = self.client.get_json(course_update_url)
        payload = json.loads(resp.content)
        self.assertTrue(len(payload) == 2)

        # try json w/o required fields
        self.assertContains(
            self.client.ajax_post(course_update_url, {'garbage': 1}),
            'Failed to save', status_code=400
        )

        # test an update with text in the tail of the header
        content = 'outside <strong>inside</strong> after'
        payload = get_response(content, 'June 22, 2000')
        self.assertHTMLEqual(content, payload['content'], "text outside tag")

        # now try to update a non-existent update
        content = 'blah blah'
        payload = {'content': content, 'date': 'January 21, 2013'}
        self.assertContains(
            self.client.ajax_post(course_update_url + '/9', payload),
            'Failed to save', status_code=400
        )

        # update w/ malformed html
        content = '<garbage tag No closing brace to force <span>error</span>'
        payload = {'content': content,
                   'date': 'January 11, 2013'}

        self.assertContains(
            self.client.ajax_post(course_update_url, payload),
            '<garbage'
        )

        # set to valid html which would break an xml parser
        content = "<p><br><br></p>"
        payload = get_response(content, 'January 11, 2013')
        self.assertHTMLEqual(content, payload['content'])

        # now try to delete a non-existent update
        self.assertContains(self.client.delete(course_update_url + '/19'), "delete", status_code=400)

        # now delete a real update
        content = 'blah blah'
        payload = get_response(content, 'January 28, 2013')
        this_id = payload['id']
        self.assertHTMLEqual(content, payload['content'], "single iframe")
        # first count the entries
        resp = self.client.get_json(course_update_url)
        payload = json.loads(resp.content)
        before_delete = len(payload)

        url = update_locator.url_reverse('course_info_update/', str(this_id))
        resp = self.client.delete(url)
        payload = json.loads(resp.content)
        self.assertTrue(len(payload) == before_delete - 1)
Example #40
0
def tabs_handler(request,
                 tag=None,
                 package_id=None,
                 branch=None,
                 version_guid=None,
                 block=None):
    """
    The restful handler for static tabs.

    GET
        html: return page for editing static tabs
        json: not supported
    PUT or POST
        json: update the tab order. It is expected that the request body contains a JSON-encoded dict with entry "tabs".
        The value for "tabs" is an array of tab locators, indicating the desired order of the tabs.

    Creating a tab, deleting a tab, or changing its contents is not supported through this method.
    Instead use the general xblock URL (see item.xblock_handler).
    """
    locator = BlockUsageLocator(package_id=package_id,
                                branch=branch,
                                version_guid=version_guid,
                                block_id=block)
    if not has_access(request.user, locator):
        raise PermissionDenied()

    old_location = loc_mapper().translate_locator_to_location(locator)
    store = get_modulestore(old_location)
    course_item = store.get_item(old_location)

    if 'application/json' in request.META.get('HTTP_ACCEPT',
                                              'application/json'):
        if request.method == 'GET':
            raise NotImplementedError('coming soon')
        else:
            if 'tabs' in request.json:

                def get_location_for_tab(tab):
                    """  Returns the location (old-style) for a tab. """
                    return loc_mapper().translate_locator_to_location(
                        BlockUsageLocator(tab))

                tabs = request.json['tabs']

                # get list of existing static tabs in course
                # make sure they are the same lengths (i.e. the number of passed in tabs equals the number
                # that we know about) otherwise we will inadvertently drop some!
                existing_static_tabs = [
                    t for t in course_item.tabs if t['type'] == 'static_tab'
                ]
                if len(existing_static_tabs) != len(tabs):
                    return JsonResponse(
                        {
                            "error":
                            "number of tabs must be {}".format(
                                len(existing_static_tabs))
                        },
                        status=400)

                # load all reference tabs, return BadRequest if we can't find any of them
                tab_items = []
                for tab in tabs:
                    item = modulestore('direct').get_item(
                        get_location_for_tab(tab))
                    if item is None:
                        return JsonResponse(
                            {
                                "error":
                                "no tab for found location {}".format(tab)
                            },
                            status=400)

                    tab_items.append(item)

                # now just go through the existing course_tabs and re-order the static tabs
                reordered_tabs = []
                static_tab_idx = 0
                for tab in course_item.tabs:
                    if tab['type'] == 'static_tab':
                        reordered_tabs.append({
                            'type':
                            'static_tab',
                            'name':
                            tab_items[static_tab_idx].display_name,
                            'url_slug':
                            tab_items[static_tab_idx].location.name,
                        })
                        static_tab_idx += 1
                    else:
                        reordered_tabs.append(tab)

                # OK, re-assemble the static tabs in the new order
                course_item.tabs = reordered_tabs
                modulestore('direct').update_metadata(
                    course_item.location, own_metadata(course_item))
                return JsonResponse()
            else:
                raise NotImplementedError(
                    'Creating or changing tab content is not supported.')
    elif request.method == 'GET':  # assume html
        # see tabs have been uninitialized (e.g. supporting courses created before tab support in studio)
        if course_item.tabs is None or len(course_item.tabs) == 0:
            initialize_course_tabs(course_item)

        # first get all static tabs from the tabs list
        # we do this because this is also the order in which items are displayed in the LMS
        static_tabs_refs = [
            t for t in course_item.tabs if t['type'] == 'static_tab'
        ]

        static_tabs = []
        for static_tab_ref in static_tabs_refs:
            static_tab_loc = old_location.replace(
                category='static_tab', name=static_tab_ref['url_slug'])
            static_tabs.append(modulestore('direct').get_item(static_tab_loc))

        components = [
            loc_mapper().translate_location(course_item.location.course_id,
                                            static_tab.location, False, True)
            for static_tab in static_tabs
        ]

        return render_to_response(
            'edit-tabs.html', {
                'context_course': course_item,
                'components': components,
                'course_locator': locator
            })
    else:
        return HttpResponseNotFound()
Example #41
0
    def update_from_json(cls, course_locator, jsondict, user):
        """
        Decode the json into CourseDetails and save any changed attrs to the db
        """
        course_old_location = loc_mapper().translate_locator_to_location(
            course_locator)
        descriptor = get_modulestore(course_old_location).get_item(
            course_old_location)

        dirty = False

        # In the descriptor's setter, the date is converted to JSON using Date's to_json method.
        # Calling to_json on something that is already JSON doesn't work. Since reaching directly
        # into the model is nasty, convert the JSON Date to a Python date, which is what the
        # setter expects as input.
        date = Date()

        if 'start_date' in jsondict:
            converted = date.from_json(jsondict['start_date'])
        else:
            converted = None
        if converted != descriptor.start:
            dirty = True
            descriptor.start = converted

        if 'end_date' in jsondict:
            converted = date.from_json(jsondict['end_date'])
        else:
            converted = None

        if converted != descriptor.end:
            dirty = True
            descriptor.end = converted

        if 'enrollment_start' in jsondict:
            converted = date.from_json(jsondict['enrollment_start'])
        else:
            converted = None

        if converted != descriptor.enrollment_start:
            dirty = True
            descriptor.enrollment_start = converted

        if 'enrollment_end' in jsondict:
            converted = date.from_json(jsondict['enrollment_end'])
        else:
            converted = None

        if converted != descriptor.enrollment_end:
            dirty = True
            descriptor.enrollment_end = converted

        if 'course_image_name' in jsondict and jsondict[
                'course_image_name'] != descriptor.course_image:
            descriptor.course_image = jsondict['course_image_name']
            dirty = True

        if dirty:
            get_modulestore(course_old_location).update_item(
                descriptor, user.id)

        # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
        # to make faster, could compare against db or could have client send over a list of which fields changed.
        for about_type in [
                'syllabus', 'overview', 'effort', 'short_description'
        ]:
            cls.update_about_item(course_old_location, about_type,
                                  jsondict[about_type], descriptor, user)

        # recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
        cls.update_about_item(
            course_old_location, 'video',
            CourseDetails.recompose_video_tag(jsondict['intro_video']),
            descriptor, user)

        # Could just return jsondict w/o doing any db reads, but I put the reads in as a means to confirm
        # it persisted correctly
        return CourseDetails.fetch(course_locator)
Example #42
0
def _upload_asset(request, location):
    '''
    This method allows for POST uploading of files into the course asset
    library, which will be supported by GridFS in MongoDB.
    '''
    old_location = loc_mapper().translate_locator_to_location(location)
    # Does the course actually exist?!? Get anything from it to prove its
    # existence
    try:
        modulestore().get_item(old_location)
    except:
        # no return it as a Bad Request response
        logging.error("Could not find course: %s", old_location)
        return HttpResponseBadRequest()

    # get all filename
    course_reference = StaticContent.compute_location(old_location.org,
                                                      old_location.course,
                                                      old_location.name)

    filename_arr = [
        up_f["displayname"]
        for up_f in contentstore().get_all_content_for_course(
            course_reference, start=0, maxresults=-1, sort=None)[0]
    ] or []

    # compute a 'filename' which is similar to the location formatting, we're
    # using the 'filename' nomenclature since we're using a FileSystem paradigm
    # here. We're just imposing the Location string formatting expectations to
    # keep things a bit more consistent
    upload_file = request.FILES['file']
    filename = upload_file.name

    def acquire_purename_and_suffix(f_n):
        fn_sp = f_n.split(".")
        return ('.'.join(fn_sp[0:-1]), fn_sp[-1])

    if filename in filename_arr:
        # filter same suffix filename
        pure_filename, file_suffix = acquire_purename_and_suffix(filename)

        pattern_str = "(" + pure_filename.replace("(", "\(").replace(
            ")", "\)") + ")(" + "\()(\d+)(\))"
        pattern = re.compile(pattern_str)

        start_copy = 0
        for f in filename_arr:
            f_n, f_s = acquire_purename_and_suffix(f)

            if f_s != file_suffix:
                continue

            match_obj = pattern.search(f_n)

            if match_obj:
                start_copy = int(match_obj.groups()[2]) if int(
                    match_obj.groups()[2]) > start_copy else start_copy

        filename = pure_filename + "(" + str(start_copy +
                                             1) + ")." + file_suffix

    mime_type = upload_file.content_type

    content_loc = StaticContent.compute_location(old_location.org,
                                                 old_location.course, filename)

    chunked = upload_file.multiple_chunks()
    sc_partial = partial(StaticContent, content_loc, filename, mime_type)
    if chunked:
        content = sc_partial(upload_file.chunks())
        tempfile_path = upload_file.temporary_file_path()
    else:
        content = sc_partial(upload_file.read())
        tempfile_path = None

    # first let's see if a thumbnail can be created
    (thumbnail_content,
     thumbnail_location) = contentstore().generate_thumbnail(
         content, tempfile_path=tempfile_path)

    # delete cached thumbnail even if one couldn't be created this time (else
    # the old thumbnail will continue to show)
    del_cached_content(thumbnail_location)
    # now store thumbnail location only if we could create it
    if thumbnail_content is not None:
        content.thumbnail_location = thumbnail_location

    # then commit the content
    contentstore().save(content)
    del_cached_content(content.location)

    # readback the saved content - we need the database timestamp
    readback = contentstore().find(content.location)

    locked = getattr(content, 'locked', False)
    response_payload = {
        'asset':
        _get_asset_json(content.name, readback.last_modified_at,
                        content.location, content.thumbnail_location, locked),
        'msg':
        _('Upload completed')
    }

    return JsonResponse(response_payload)
Example #43
0
 def get_location_for_tab(tab):
     """  Returns the location (old-style) for a tab. """
     return loc_mapper().translate_locator_to_location(
         BlockUsageLocator(tab))
Example #44
0
def _course_team_user(request, locator, email):
    """
    Handle the add, remove, promote, demote requests ensuring the requester has authority
    """
    # check that logged in user has permissions to this item
    if has_course_access(request.user, locator, role=CourseInstructorRole):
        # instructors have full permissions
        pass
    elif has_course_access(request.user, locator, role=CourseStaffRole) and email == request.user.email:
        # staff can only affect themselves
        pass
    else:
        msg = {
            "error": _("Insufficient permissions")
        }
        return JsonResponse(msg, 400)

    try:
        user = User.objects.get(email=email)
    except Exception:
        msg = {
            "error": _("Could not find user by email address '{email}'.").format(email=email),
        }
        return JsonResponse(msg, 404)

    # role hierarchy: globalstaff > "instructor" > "staff" (in a course)
    if request.method == "GET":
        # just return info about the user
        msg = {
            "email": user.email,
            "active": user.is_active,
            "role": None,
        }
        # what's the highest role that this user has? (How should this report global staff?)
        for role in [CourseInstructorRole(locator), CourseStaffRole(locator)]:
            if role.has_user(user):
                msg["role"] = role.ROLE
                break
        return JsonResponse(msg)

    # can't modify an inactive user
    if not user.is_active:
        msg = {
            "error": _('User {email} has registered but has not yet activated his/her account.').format(email=email),
        }
        return JsonResponse(msg, 400)

    if request.method == "DELETE":
        try:
            try_remove_instructor(request, locator, user)
        except CannotOrphanCourse as oops:
            return JsonResponse(oops.msg, 400)

        auth.remove_users(request.user, CourseStaffRole(locator), user)
        return JsonResponse()

    # all other operations require the requesting user to specify a role
    role = request.json.get("role", request.POST.get("role"))
    if role is None:
        return JsonResponse({"error": _("`role` is required")}, 400)

    old_location = loc_mapper().translate_locator_to_location(locator)
    if role == "instructor":
        if not has_course_access(request.user, locator, role=CourseInstructorRole):
            msg = {
                "error": _("Only instructors may create other instructors")
            }
            return JsonResponse(msg, 400)
        auth.add_users(request.user, CourseInstructorRole(locator), user)
        # auto-enroll the course creator in the course so that "View Live" will work.
        CourseEnrollment.enroll(user, old_location.course_id)
    elif role == "staff":
        # add to staff regardless (can't do after removing from instructors as will no longer
        # be allowed)
        auth.add_users(request.user, CourseStaffRole(locator), user)
        try:
            try_remove_instructor(request, locator, user)
        except CannotOrphanCourse as oops:
            return JsonResponse(oops.msg, 400)

        # auto-enroll the course creator in the course so that "View Live" will work.
        CourseEnrollment.enroll(user, old_location.course_id)

    return JsonResponse()
Example #45
0
def xblock_view_handler(request,
                        package_id,
                        view_name,
                        tag=None,
                        branch=None,
                        version_guid=None,
                        block=None):
    """
    The restful handler for requests for rendered xblock views.

    Returns a json object containing two keys:
        html: The rendered html of the view
        resources: A list of tuples where the first element is the resource hash, and
            the second is the resource description
    """
    locator = BlockUsageLocator(package_id=package_id,
                                branch=branch,
                                version_guid=version_guid,
                                block_id=block)
    if not has_course_access(request.user, locator):
        raise PermissionDenied()
    old_location = loc_mapper().translate_locator_to_location(locator)

    accept_header = request.META.get('HTTP_ACCEPT', 'application/json')

    if 'application/json' in accept_header:
        store = get_modulestore(old_location)
        component = store.get_item(old_location)

        # wrap the generated fragment in the xmodule_editor div so that the javascript
        # can bind to it correctly
        component.runtime.wrappers.append(partial(wrap_xblock,
                                                  'StudioRuntime'))

        if view_name == 'studio_view':
            try:
                fragment = component.render('studio_view')
            # catch exceptions indiscriminately, since after this point they escape the
            # dungeon and surface as uneditable, unsaveable, and undeletable
            # component-goblins.
            except Exception as exc:  # pylint: disable=w0703
                log.debug("unable to render studio_view for %r",
                          component,
                          exc_info=True)
                fragment = Fragment(
                    render_to_string('html_error.html', {'message': str(exc)}))

            store.save_xmodule(component)
        elif view_name == 'student_view' and component.has_children:
            # For non-leaf xblocks on the unit page, show the special rendering
            # which links to the new container page.
            course_location = loc_mapper().translate_locator_to_location(
                locator, True)
            course = store.get_item(course_location)
            html = render_to_string('unit_container_xblock_component.html', {
                'course': course,
                'xblock': component,
                'locator': locator
            })
            return JsonResponse({
                'html': html,
                'resources': [],
            })
        elif view_name in ('student_view', 'container_preview'):
            is_container_view = (view_name == 'container_preview')

            # Only show the new style HTML for the container view, i.e. for non-verticals
            # Note: this special case logic can be removed once the unit page is replaced
            # with the new container view.
            is_read_only_view = is_container_view
            context = {
                'container_view': is_container_view,
                'read_only': is_read_only_view,
                'root_xblock': component
            }

            fragment = get_preview_fragment(request, component, context)
            # For old-style pages (such as unit and static pages), wrap the preview with
            # the component div. Note that the container view recursively adds headers
            # into the preview fragment, so we don't want to add another header here.
            if not is_container_view:
                fragment.content = render_to_string(
                    'component.html',
                    {
                        'preview':
                        fragment.content,
                        'label':
                        component.display_name
                        or component.scope_ids.block_type,

                        # Native XBlocks are responsible for persisting their own data,
                        # so they are also responsible for providing save/cancel buttons.
                        'show_save_cancel':
                        isinstance(component,
                                   xmodule.x_module.XModuleDescriptor),
                    })
        else:
            raise Http404

        hashed_resources = OrderedDict()
        for resource in fragment.resources:
            hashed_resources[hash_resource(resource)] = resource

        return JsonResponse({
            'html': fragment.content,
            'resources': hashed_resources.items()
        })

    else:
        return HttpResponse(status=406)
Example #46
0
    def setUp(self):
        super(ItemTest, self).setUp()

        self.course_locator = loc_mapper().translate_location(
            self.course.location.course_id, self.course.location, False, True)
        self.unicode_locator = unicode(self.course_locator)
Example #47
0
def xblock_handler(request,
                   tag=None,
                   course_id=None,
                   branch=None,
                   version_guid=None,
                   block=None):
    """
    The restful handler for xblock requests.

    DELETE
        json: delete this xblock instance from the course. Supports query parameters "recurse" to delete
        all children and "all_versions" to delete from all (mongo) versions.
    GET
        json: returns representation of the xblock (locator id, data, and metadata).
              if ?fields=graderType, it returns the graderType for the unit instead of the above.
        html: returns HTML for rendering the xblock (which includes both the "preview" view and the "editor" view)
    PUT or POST
        json: if xblock locator is specified, update the xblock instance. The json payload can contain
              these fields, all optional:
                :data: the new value for the data.
                :children: the locator ids of children for this xblock.
                :metadata: new values for the metadata fields. Any whose values are None will be deleted not set
                       to None! Absent ones will be left alone.
                :nullout: which metadata fields to set to None
                :graderType: change how this unit is graded
                :publish: can be one of three values, 'make_public, 'make_private', or 'create_draft'         
              The JSON representation on the updated xblock (minus children) is returned.

              if xblock locator is not specified, create a new xblock instance. The json playload can contain
              these fields:
                :parent_locator: parent for new xblock, required
                :category: type of xblock, required
                :display_name: name for new xblock, optional
                :boilerplate: template name for populating fields, optional
              The locator (and old-style id) for the created xblock (minus children) is returned.
    """
    if course_id is not None:
        locator = BlockUsageLocator(course_id=course_id,
                                    branch=branch,
                                    version_guid=version_guid,
                                    usage_id=block)
        if not has_access(request.user, locator):
            raise PermissionDenied()
        old_location = loc_mapper().translate_locator_to_location(locator)

        if request.method == 'GET':
            if 'application/json' in request.META.get('HTTP_ACCEPT',
                                                      'application/json'):
                fields = request.REQUEST.get('fields', '').split(',')
                if 'graderType' in fields:
                    # right now can't combine output of this w/ output of _get_module_info, but worthy goal
                    return JsonResponse(
                        CourseGradingModel.get_section_grader_type(locator))
                # TODO: pass fields to _get_module_info and only return those
                rsp = _get_module_info(locator)
                return JsonResponse(rsp)
            else:
                component = modulestore().get_item(old_location)
                # Wrap the generated fragment in the xmodule_editor div so that the javascript
                # can bind to it correctly
                component.runtime.wrappers.append(
                    partial(wrap_xblock, handler_prefix))

                try:
                    content = component.render('studio_view').content
                # catch exceptions indiscriminately, since after this point they escape the
                # dungeon and surface as uneditable, unsaveable, and undeletable
                # component-goblins.
                except Exception as exc:  # pylint: disable=W0703
                    log.debug("Unable to render studio_view for %r",
                              component,
                              exc_info=True)
                    content = render_to_string('html_error.html',
                                               {'message': str(exc)})

                return render_to_response(
                    'component.html', {
                        'preview': get_preview_html(request, component),
                        'editor': content
                    })
        elif request.method == 'DELETE':
            delete_children = str_to_bool(
                request.REQUEST.get('recurse', 'False'))
            delete_all_versions = str_to_bool(
                request.REQUEST.get('all_versions', 'False'))

            return _delete_item_at_location(old_location, delete_children,
                                            delete_all_versions)
        else:  # Since we have a course_id, we are updating an existing xblock.
            return _save_item(
                request,
                locator,
                old_location,
                data=request.json.get('data'),
                children=request.json.get('children'),
                metadata=request.json.get('metadata'),
                nullout=request.json.get('nullout'),
                grader_type=request.json.get('graderType'),
                publish=request.json.get('publish'),
            )
    elif request.method in ('PUT', 'POST'):
        return _create_item(request)
    else:
        return HttpResponseBadRequest(
            "Only instance creation is supported without a course_id.",
            content_type="text/plain")
Example #48
0
def _upload_asset(request, location):
    '''
    This method allows for POST uploading of files into the course asset
    library, which will be supported by GridFS in MongoDB.
    '''
    old_location = loc_mapper().translate_locator_to_location(location)

    # Does the course actually exist?!? Get anything from it to prove its
    # existence
    try:
        modulestore().get_item(old_location)
    except:
        # no return it as a Bad Request response
        logging.error("Could not find course: %s", old_location)
        return HttpResponseBadRequest()

    # compute a 'filename' which is similar to the location formatting, we're
    # using the 'filename' nomenclature since we're using a FileSystem paradigm
    # here. We're just imposing the Location string formatting expectations to
    # keep things a bit more consistent
    upload_file = request.FILES['file']
    filename = upload_file.name
    mime_type = upload_file.content_type

    content_loc = StaticContent.compute_location(old_location.org,
                                                 old_location.course, filename)

    chunked = upload_file.multiple_chunks()
    sc_partial = partial(StaticContent, content_loc, filename, mime_type)
    if chunked:
        content = sc_partial(upload_file.chunks())
        tempfile_path = upload_file.temporary_file_path()
    else:
        content = sc_partial(upload_file.read())
        tempfile_path = None

    # first let's see if a thumbnail can be created
    (thumbnail_content,
     thumbnail_location) = contentstore().generate_thumbnail(
         content, tempfile_path=tempfile_path)

    # delete cached thumbnail even if one couldn't be created this time (else
    # the old thumbnail will continue to show)
    del_cached_content(thumbnail_location)
    # now store thumbnail location only if we could create it
    if thumbnail_content is not None:
        content.thumbnail_location = thumbnail_location

    # then commit the content
    contentstore().save(content)
    del_cached_content(content.location)

    # readback the saved content - we need the database timestamp
    readback = contentstore().find(content.location)

    locked = getattr(content, 'locked', False)
    response_payload = {
        'asset':
        _get_asset_json(content.name, readback.last_modified_at,
                        content.location, content.thumbnail_location, locked),
        'msg':
        _('Upload completed')
    }

    return JsonResponse(response_payload)
Example #49
0
 def get_old_id(self, locator):
     """
     Converts new locator to old id format (forcing to non-draft).
     """
     return loc_mapper().translate_locator_to_location(BlockUsageLocator(locator)).replace(revision=None)
Example #50
0
def export_handler(request,
                   tag=None,
                   package_id=None,
                   branch=None,
                   version_guid=None,
                   block=None):
    """
    The restful handler for exporting a course.

    GET
        html: return html page for import page
        application/x-tgz: return tar.gz file containing exported course
        json: not supported

    Note that there are 2 ways to request the tar.gz file. The request header can specify
    application/x-tgz via HTTP_ACCEPT, or a query parameter can be used (?_accept=application/x-tgz).

    If the tar.gz file has been requested but the export operation fails, an HTML page will be returned
    which describes the error.
    """
    location = BlockUsageLocator(package_id=package_id,
                                 branch=branch,
                                 version_guid=version_guid,
                                 block_id=block)
    if not has_access(request.user, location):
        raise PermissionDenied()

    old_location = loc_mapper().translate_locator_to_location(location)
    course_module = modulestore().get_item(old_location)

    # an _accept URL parameter will be preferred over HTTP_ACCEPT in the header.
    requested_format = request.REQUEST.get(
        '_accept', request.META.get('HTTP_ACCEPT', 'text/html'))

    export_url = location.url_reverse('export') + '?_accept=application/x-tgz'
    if 'application/x-tgz' in requested_format:
        name = old_location.name
        export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
        root_dir = path(mkdtemp())

        try:
            export_to_xml(modulestore('direct'), contentstore(), old_location,
                          root_dir, name, modulestore())

        except SerializationError, e:
            logging.exception(
                'There was an error exporting course {0}. {1}'.format(
                    course_module.location, unicode(e)))
            unit = None
            failed_item = None
            parent = None
            try:
                failed_item = modulestore().get_instance(
                    course_module.location.course_id, e.location)
                parent_locs = modulestore().get_parent_locations(
                    failed_item.location, course_module.location.course_id)

                if len(parent_locs) > 0:
                    parent = modulestore().get_item(parent_locs[0])
                    if parent.location.category == 'vertical':
                        unit = parent
            except:
                # if we have a nested exception, then we'll show the more generic error message
                pass

            unit_locator = loc_mapper().translate_location(
                old_location.course_id, parent.location, False, True)

            return render_to_response(
                'export.html', {
                    'context_course':
                    course_module,
                    'in_err':
                    True,
                    'raw_err_msg':
                    str(e),
                    'failed_module':
                    failed_item,
                    'unit':
                    unit,
                    'edit_unit_url':
                    unit_locator.url_reverse("unit") if parent else "",
                    'course_home_url':
                    location.url_reverse("course"),
                    'export_url':
                    export_url
                })
        except Exception, e:
            logging.exception(
                'There was an error exporting course {0}. {1}'.format(
                    course_module.location, unicode(e)))
            return render_to_response(
                'export.html', {
                    'context_course': course_module,
                    'in_err': True,
                    'unit': None,
                    'raw_err_msg': str(e),
                    'course_home_url': location.url_reverse("course"),
                    'export_url': export_url
                })
Example #51
0
def _save_item(request,
               usage_loc,
               item_location,
               data=None,
               children=None,
               metadata=None,
               nullout=None,
               grader_type=None,
               publish=None):
    """
    Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
    nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
    to default).

    The item_location is still the old-style location whereas usage_loc is a BlockUsageLocator
    """
    store = get_modulestore(item_location)

    try:
        existing_item = store.get_item(item_location)
    except ItemNotFoundError:
        if item_location.category in CREATE_IF_NOT_FOUND:
            # New module at this location, for pages that are not pre-created.
            # Used for course info handouts.
            store.create_and_save_xmodule(item_location)
            existing_item = store.get_item(item_location)
        else:
            raise
    except InvalidLocationError:
        log.error("Can't find item by location.")
        return JsonResponse(
            {"error": "Can't find item by location: " + str(item_location)},
            404)

    if publish:
        if publish == 'make_private':
            _xmodule_recurse(existing_item,
                             lambda i: modulestore().unpublish(i.location))
        elif publish == 'create_draft':
            # This clones the existing item location to a draft location (the draft is
            # implicit, because modulestore is a Draft modulestore)
            modulestore().convert_to_draft(item_location)

    if data:
        store.update_item(item_location, data)
    else:
        data = existing_item.get_explicitly_set_fields_by_scope(Scope.content)

    if children is not None:
        children_ids = [
            loc_mapper().translate_locator_to_location(
                BlockUsageLocator(child_locator)).url()
            for child_locator in children
        ]
        store.update_children(item_location, children_ids)

    # cdodge: also commit any metadata which might have been passed along
    if nullout is not None or metadata is not None:
        # the postback is not the complete metadata, as there's system metadata which is
        # not presented to the end-user for editing. So let's use the original (existing_item) and
        # 'apply' the submitted metadata, so we don't end up deleting system metadata.
        if nullout is not None:
            for metadata_key in nullout:
                setattr(existing_item, metadata_key, None)

        # update existing metadata with submitted metadata (which can be partial)
        # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If
        # the intent is to make it None, use the nullout field
        if metadata is not None:
            for metadata_key, value in metadata.items():
                field = existing_item.fields[metadata_key]

                if value is None:
                    field.delete_from(existing_item)
                else:
                    try:
                        value = field.from_json(value)
                    except ValueError:
                        return JsonResponse({"error": "Invalid data"}, 400)
                    field.write_to(existing_item, value)

        # Save the data that we've just changed to the underlying
        # MongoKeyValueStore before we update the mongo datastore.
        existing_item.save()
        # commit to datastore
        store.update_metadata(item_location, own_metadata(existing_item))

        if existing_item.category == 'video':
            manage_video_subtitles_save(existing_item, existing_item)

    result = {
        'id': unicode(usage_loc),
        'data': data,
        'metadata': own_metadata(existing_item)
    }

    if grader_type is not None:
        result.update(
            CourseGradingModel.update_section_grader_type(
                existing_item, grader_type))

    # Make public after updating the xblock, in case the caller asked
    # for both an update and a publish.
    if publish and publish == 'make_public':
        _xmodule_recurse(
            existing_item,
            lambda i: modulestore().publish(i.location, request.user.id))

    # Note that children aren't being returned until we have a use case.
    return JsonResponse(result)
Example #52
0
 def test_nonexistent_locator(self):
     locator = loc_mapper().translate_location(self.old_course.location)
     errstring = "No course found with locator"
     with self.assertRaisesRegexp(CommandError, errstring):
         Command().handle(str(locator))
Example #53
0
def import_handler(request,
                   tag=None,
                   package_id=None,
                   branch=None,
                   version_guid=None,
                   block=None):
    """
    The restful handler for importing a course.

    GET
        html: return html page for import page
        json: not supported
    POST or PUT
        json: import a course via the .tar.gz file specified in request.FILES
    """
    location = BlockUsageLocator(package_id=package_id,
                                 branch=branch,
                                 version_guid=version_guid,
                                 block_id=block)
    if not has_access(request.user, location):
        raise PermissionDenied()

    old_location = loc_mapper().translate_locator_to_location(location)

    if 'application/json' in request.META.get('HTTP_ACCEPT',
                                              'application/json'):
        if request.method == 'GET':
            raise NotImplementedError('coming soon')
        else:
            data_root = path(settings.GITHUB_REPO_ROOT)
            course_subdir = "{0}-{1}-{2}".format(old_location.org,
                                                 old_location.course,
                                                 old_location.name)
            course_dir = data_root / course_subdir

            filename = request.FILES['course-data'].name
            if not filename.endswith('.tar.gz'):
                return JsonResponse(
                    {
                        'ErrMsg':
                        _('We only support uploading a .tar.gz file.'),
                        'Stage': 1
                    },
                    status=415)
            temp_filepath = course_dir / filename

            if not course_dir.isdir():
                os.mkdir(course_dir)

            logging.debug('importing course to {0}'.format(temp_filepath))

            # Get upload chunks byte ranges
            try:
                matches = CONTENT_RE.search(request.META["HTTP_CONTENT_RANGE"])
                content_range = matches.groupdict()
            except KeyError:  # Single chunk
                # no Content-Range header, so make one that will work
                content_range = {'start': 0, 'stop': 1, 'end': 2}

            # stream out the uploaded files in chunks to disk
            if int(content_range['start']) == 0:
                mode = "wb+"
            else:
                mode = "ab+"
                size = os.path.getsize(temp_filepath)
                # Check to make sure we haven't missed a chunk
                # This shouldn't happen, even if different instances are handling
                # the same session, but it's always better to catch errors earlier.
                if size < int(content_range['start']):
                    log.warning(
                        "Reported range %s does not match size downloaded so far %s",
                        content_range['start'], size)
                    return JsonResponse(
                        {
                            'ErrMsg':
                            _('File upload corrupted. Please try again'),
                            'Stage': 1
                        },
                        status=409)
                # The last request sometimes comes twice. This happens because
                # nginx sends a 499 error code when the response takes too long.
                elif size > int(content_range['stop']) and size == int(
                        content_range['end']):
                    return JsonResponse({'ImportStatus': 1})

            with open(temp_filepath, mode) as temp_file:
                for chunk in request.FILES['course-data'].chunks():
                    temp_file.write(chunk)

            size = os.path.getsize(temp_filepath)

            if int(content_range['stop']) != int(content_range['end']) - 1:
                # More chunks coming
                return JsonResponse({
                    "files": [{
                        "name": filename,
                        "size": size,
                        "deleteUrl": "",
                        "deleteType": "",
                        "url": location.url_reverse('import'),
                        "thumbnailUrl": ""
                    }]
                })

            else:  # This was the last chunk.

                # Use sessions to keep info about import progress
                session_status = request.session.setdefault(
                    "import_status", {})
                key = location.package_id + filename
                session_status[key] = 1
                request.session.modified = True

                # Do everything from now on in a try-finally block to make sure
                # everything is properly cleaned up.
                try:

                    tar_file = tarfile.open(temp_filepath)
                    try:
                        safetar_extractall(tar_file,
                                           (course_dir + '/').encode('utf-8'))
                    except SuspiciousOperation as exc:
                        return JsonResponse(
                            {
                                'ErrMsg': 'Unsafe tar file. Aborting import.',
                                'SuspiciousFileOperationMsg': exc.args[0],
                                'Stage': 1
                            },
                            status=400)
                    finally:
                        tar_file.close()

                    session_status[key] = 2
                    request.session.modified = True

                    # find the 'course.xml' file
                    def get_all_files(directory):
                        """
                        For each file in the directory, yield a 2-tuple of (file-name,
                        directory-path)
                        """
                        for dirpath, _dirnames, filenames in os.walk(
                                directory):
                            for filename in filenames:
                                yield (filename, dirpath)

                    def get_dir_for_fname(directory, filename):
                        """
                        Returns the dirpath for the first file found in the directory
                        with the given name.  If there is no file in the directory with
                        the specified name, return None.
                        """
                        for fname, dirpath in get_all_files(directory):
                            if fname == filename:
                                return dirpath
                        return None

                    fname = "course.xml"

                    dirpath = get_dir_for_fname(course_dir, fname)

                    if not dirpath:
                        return JsonResponse(
                            {
                                'ErrMsg':
                                _('Could not find the course.xml file in the package.'
                                  ),
                                'Stage':
                                2
                            },
                            status=415)

                    logging.debug('found course.xml at {0}'.format(dirpath))

                    if dirpath != course_dir:
                        for fname in os.listdir(dirpath):
                            shutil.move(dirpath / fname, course_dir)

                    _module_store, course_items = import_from_xml(
                        modulestore('direct'),
                        settings.GITHUB_REPO_ROOT, [course_subdir],
                        load_error_modules=False,
                        static_content_store=contentstore(),
                        target_location_namespace=old_location,
                        draft_store=modulestore())

                    logging.debug('new course at {0}'.format(
                        course_items[0].location))

                    session_status[key] = 3
                    request.session.modified = True

                    create_all_course_groups(request.user,
                                             course_items[0].location)
                    logging.debug('created all course groups at {0}'.format(
                        course_items[0].location))

                # Send errors to client with stage at which error occured.
                except Exception as exception:  # pylint: disable=W0703
                    return JsonResponse(
                        {
                            'ErrMsg': str(exception),
                            'Stage': session_status[key]
                        },
                        status=400)

                finally:
                    shutil.rmtree(course_dir)

                return JsonResponse({'Status': 'OK'})
    elif request.method == 'GET':  # assume html
        course_module = modulestore().get_item(old_location)
        return render_to_response(
            'import.html', {
                'context_course':
                course_module,
                'successful_import_redirect_url':
                location.url_reverse("course"),
                'import_status_url':
                location.url_reverse("import_status", "fillerName"),
            })
    else:
        return HttpResponseNotFound()
Example #54
0
    def test_get_all_users(self):
        """
        Test getting all authors for a course where their permissions run the gamut of allowed group
        types.
        """
        # first check the course creator.has explicit access (don't use has_access as is_staff
        # will trump the actual test)
        self.assertTrue(
            CourseInstructorRole(self.course_locator).has_user(self.user),
            "Didn't add creator as instructor."
        )
        users = copy.copy(self.users)
        # doesn't use role.users_with_role b/c it's verifying the roles.py behavior
        user_by_role = {}
        # add the misc users to the course in different groups
        for role in [CourseInstructorRole, CourseStaffRole]:
            user_by_role[role] = []
            # pylint: disable=protected-access
            groupnames = role(self.course_locator)._group_names
            self.assertGreater(len(groupnames), 1, "Only 0 or 1 groupname for {}".format(role.ROLE))
            # NOTE: this loop breaks the roles.py abstraction by purposely assigning
            # users to one of each possible groupname in order to test that has_course_access
            # and remove_user work
            for groupname in groupnames:
                group, _ = Group.objects.get_or_create(name=groupname)
                user = users.pop()
                user_by_role[role].append(user)
                user.groups.add(group)
                user.save()
                self.assertTrue(has_course_access(user, self.course_locator), "{} does not have access".format(user))
                self.assertTrue(has_course_access(user, self.course_location), "{} does not have access".format(user))

        response = self.client.get_html(self.course_locator.url_reverse('course_team'))
        for role in [CourseInstructorRole, CourseStaffRole]:
            for user in user_by_role[role]:
                self.assertContains(response, user.email)
        
        # test copying course permissions
        copy_course_location = Location(['i4x', 'copyu', 'copydept.mycourse', 'course', 'myrun'])
        copy_course_locator = loc_mapper().translate_location(
            copy_course_location.course_id, copy_course_location, False, True
        )
        for role in [CourseInstructorRole, CourseStaffRole]:
            auth.add_users(
                self.user,
                role(copy_course_locator),
                *role(self.course_locator).users_with_role()
            )
        # verify access in copy course and verify that removal from source course w/ the various
        # groupnames works
        for role in [CourseInstructorRole, CourseStaffRole]:
            for user in user_by_role[role]:
                # forcefully decache the groups: premise is that any real request will not have
                # multiple objects repr the same user but this test somehow uses different instance
                # in above add_users call
                if hasattr(user, '_groups'):
                    del user._groups

                self.assertTrue(has_course_access(user, copy_course_locator), "{} no copy access".format(user))
                self.assertTrue(has_course_access(user, copy_course_location), "{} no copy access".format(user))
                auth.remove_users(self.user, role(self.course_locator), user)
                self.assertFalse(has_course_access(user, self.course_locator), "{} remove didn't work".format(user))
Example #55
0
def checklists_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None, checklist_index=None):
    """
    The restful handler for checklists.

    GET
        html: return html page for all checklists
        json: return json representing all checklists. checklist_index is not supported for GET at this time.
    POST or PUT
        json: updates the checked state for items within a particular checklist. checklist_index is required.
    """
    location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
    if not has_access(request.user, location):
        raise PermissionDenied()

    old_location = loc_mapper().translate_locator_to_location(location)

    modulestore = get_modulestore(old_location)
    course_module = modulestore.get_item(old_location)

    json_request = 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json')
    if request.method == 'GET':
        # If course was created before checklists were introduced, copy them over
        # from the template.
        if not course_module.checklists:
            course_module.checklists = CourseDescriptor.checklists.default
            course_module.save()
            modulestore.update_metadata(old_location, own_metadata(course_module))

        expanded_checklists = expand_all_action_urls(course_module)
        if json_request:
            return JsonResponse(expanded_checklists)
        else:
            handler_url = location.url_reverse('checklists/', '')
            return render_to_response('checklists.html',
                                      {
                                          'handler_url': handler_url,
                                          # context_course is used by analytics
                                          'context_course': course_module,
                                          'checklists': expanded_checklists
                                      })
    elif json_request:
        # Can now assume POST or PUT because GET handled above.
        if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
            index = int(checklist_index)
            persisted_checklist = course_module.checklists[index]
            modified_checklist = json.loads(request.body)
            # Only thing the user can modify is the "checked" state.
            # We don't want to persist what comes back from the client because it will
            # include the expanded action URLs (which are non-portable).
            for item_index, item in enumerate(modified_checklist.get('items')):
                persisted_checklist['items'][item_index]['is_checked'] = item['is_checked']
            # seeming noop which triggers kvs to record that the metadata is
            # not default
            course_module.checklists = course_module.checklists
            course_module.save()
            modulestore.update_metadata(old_location, own_metadata(course_module))
            expanded_checklist = expand_checklist_action_url(course_module, persisted_checklist)
            return JsonResponse(expanded_checklist)
        else:
            return HttpResponseBadRequest(
                ("Could not save checklist state because the checklist index "
                 "was out of range or unspecified."),
                content_type="text/plain"
            )
    else:
        return HttpResponseNotFound()
Example #56
0
def unit_handler(request,
                 tag=None,
                 package_id=None,
                 branch=None,
                 version_guid=None,
                 block=None):
    """
    The restful handler for unit-specific requests.

    GET
        html: return html page for editing a unit
        json: not currently supported
    """
    if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
        locator = BlockUsageLocator(package_id=package_id,
                                    branch=branch,
                                    version_guid=version_guid,
                                    block_id=block)
        try:
            old_location, course, item, lms_link = _get_item_in_course(
                request, locator)
        except ItemNotFoundError:
            return HttpResponseBadRequest()

        component_templates = defaultdict(list)
        for category in COMPONENT_TYPES:
            component_class = _load_mixed_class(category)
            # add the default template
            # TODO: Once mixins are defined per-application, rather than per-runtime,
            # this should use a cms mixed-in class. (cpennington)
            if hasattr(component_class, 'display_name'):
                display_name = component_class.display_name.default or 'Blank'
            else:
                display_name = 'Blank'
            component_templates[category].append((
                display_name,
                category,
                False,  # No defaults have markdown (hardcoded current default)
                None  # no boilerplate for overrides
            ))
            # add boilerplates
            if hasattr(component_class, 'templates'):
                for template in component_class.templates():
                    filter_templates = getattr(component_class,
                                               'filter_templates', None)
                    if not filter_templates or filter_templates(
                            template, course):
                        component_templates[category].append(
                            (template['metadata'].get('display_name'),
                             category, template['metadata'].get('markdown')
                             is not None, template.get('template_id')))

        # Check if there are any advanced modules specified in the course policy.
        # These modules should be specified as a list of strings, where the strings
        # are the names of the modules in ADVANCED_COMPONENT_TYPES that should be
        # enabled for the course.
        course_advanced_keys = course.advanced_modules

        # Set component types according to course policy file
        if isinstance(course_advanced_keys, list):
            for category in course_advanced_keys:
                if category in ADVANCED_COMPONENT_TYPES:
                    # Do I need to allow for boilerplates or just defaults on the
                    # class? i.e., can an advanced have more than one entry in the
                    # menu? one for default and others for prefilled boilerplates?
                    try:
                        component_class = _load_mixed_class(category)

                        component_templates['advanced'].append((
                            component_class.display_name.default or category,
                            category,
                            False,
                            None  # don't override default data
                        ))
                    except PluginMissingError:
                        # dhm: I got this once but it can happen any time the
                        # course author configures an advanced component which does
                        # not exist on the server. This code here merely
                        # prevents any authors from trying to instantiate the
                        # non-existent component type by not showing it in the menu
                        pass
        else:
            log.error("Improper format for course advanced keys! %s",
                      course_advanced_keys)

        xblocks = item.get_children()
        locators = [
            loc_mapper().translate_location(course.location.course_id,
                                            xblock.location, False, True)
            for xblock in xblocks
        ]

        # TODO (cpennington): If we share units between courses,
        # this will need to change to check permissions correctly so as
        # to pick the correct parent subsection
        containing_subsection = get_parent_xblock(item)
        containing_section = get_parent_xblock(containing_subsection)

        # cdodge hack. We're having trouble previewing drafts via jump_to redirect
        # so let's generate the link url here

        # need to figure out where this item is in the list of children as the
        # preview will need this
        index = 1
        for child in containing_subsection.get_children():
            if child.location == item.location:
                break
            index = index + 1

        preview_lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE')

        preview_lms_link = (
            u'//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'
        ).format(preview_lms_base=preview_lms_base,
                 lms_base=settings.LMS_BASE,
                 org=course.location.org,
                 course=course.location.course,
                 course_name=course.location.name,
                 section=containing_section.location.name,
                 subsection=containing_subsection.location.name,
                 index=index)

        return render_to_response(
            'unit.html', {
                'context_course':
                course,
                'unit':
                item,
                'unit_locator':
                locator,
                'locators':
                locators,
                'component_templates':
                component_templates,
                'draft_preview_link':
                preview_lms_link,
                'published_preview_link':
                lms_link,
                'subsection':
                containing_subsection,
                'release_date':
                (get_default_time_display(containing_subsection.start)
                 if containing_subsection.start is not None else None),
                'section':
                containing_section,
                'new_unit_category':
                'vertical',
                'unit_state':
                compute_publish_state(item),
                'published_date':
                (get_default_time_display(item.published_date)
                 if item.published_date is not None else None),
            })
    else:
        return HttpResponseBadRequest("Only supports html requests")
    def forwards(self, orm):
        """
        Converts group table entries for write access and beta_test roles to course access roles table.
        """
        def get_modulestore(ms_type, key):
            """
            Find the modulestore of the given type trying the key first
            """
            try:
                store = modulestore(key)
                if isinstance(store, MixedModuleStore):
                    store = store.modulestores[key]
                if store.get_modulestore_type(None) == ms_type:
                    return store
                else:
                    return None
            except KeyError:
                return None

        # Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..."
        loc_map_collection = loc_mapper().location_map
        xml_ms = get_modulestore(XML_MODULESTORE_TYPE, 'xml')
        mongo_ms = get_modulestore(MONGO_MODULESTORE_TYPE, 'default')
        if mongo_ms is None:
            mongo_ms = get_modulestore(MONGO_MODULESTORE_TYPE, 'direct')

        query = Q(name__startswith='staff') | Q(
            name__startswith='instructor') | Q(name__startswith='beta_testers')
        for group in orm['auth.Group'].objects.filter(query).exclude(
                name__contains="/").all():

            def _migrate_users(correct_course_key, role):
                """
                Get all the users from the old group and migrate to this course key in the new table
                """
                log.info(u'Giving %s users access to %s', group.name,
                         correct_course_key)
                for user in orm['auth.user'].objects.filter(
                        groups=group).all():
                    entry = orm['student.courseaccessrole'](
                        role=role,
                        user=user,
                        org=correct_course_key.org,
                        course_id=correct_course_key,
                    )
                    try:
                        entry.save()
                    except IntegrityError:
                        pass

            parsed_entry = self.GROUP_ENTRY_RE.search(group.name)
            if parsed_entry is None:
                log.warn('Ignoring an unexpected unparsable entry %s',
                         group.name)
                continue
            role = parsed_entry.group('role_id')
            course_id_string = parsed_entry.group('course_id_string')
            # if it's a full course_id w/ dots, ignore it
            entry = loc_map_collection.find_one({
                'course_id':
                re.compile(r'^{}$'.format(course_id_string), re.IGNORECASE)
            })
            if entry is None:
                # check new table to see if it's been added as org permission
                if not orm['student.courseaccessrole'].objects.filter(
                        role=role,
                        org__iexact=course_id_string,
                ).exists():
                    # old auth was of form role_coursenum. Grant access to all such courses wildcarding org and run
                    # look in xml for matching courses
                    if xml_ms is not None:
                        for course in xml_ms.get_courses():
                            if course_id_string == course.id.course.lower():
                                _migrate_users(course.id, role)

                    if mongo_ms is not None:
                        mongo_query = re.compile(
                            ur'^{}$'.format(course_id_string), re.IGNORECASE)
                        for mongo_entry in mongo_ms.collection.find(
                            {
                                "_id.category": "course",
                                "_id.course": mongo_query
                            },
                                fields=["_id"]):
                            mongo_id_dict = mongo_entry['_id']
                            course_key = SlashSeparatedCourseKey(
                                mongo_id_dict['org'], mongo_id_dict['course'],
                                mongo_id_dict['name'])
                            _migrate_users(course_key, role)