Ejemplo n.º 1
0
    def test_old_location_helpers(self):
        """
        Test the functions intended to help with the conversion from old locations to locators
        """
        location_tuple = ('i4x', 'mit', 'eecs.6002x', 'course', 't3_2013')
        location = Location(location_tuple)
        self.assertEqual(location, Locator.to_locator_or_location(location))
        self.assertEqual(location, Locator.to_locator_or_location(location_tuple))
        self.assertEqual(location, Locator.to_locator_or_location(list(location_tuple)))
        self.assertEqual(location, Locator.to_locator_or_location(location.dict()))

        locator = BlockUsageLocator(package_id='foo.bar', branch='alpha', block_id='deep')
        self.assertEqual(locator, Locator.to_locator_or_location(locator))
        self.assertEqual(locator.as_course_locator(), Locator.to_locator_or_location(locator.as_course_locator()))
        self.assertEqual(location, Locator.to_locator_or_location(location.url()))
        self.assertEqual(locator, Locator.to_locator_or_location(locator.url()))
        self.assertEqual(locator, Locator.to_locator_or_location(locator.__dict__))

        asset_location = Location(['c4x', 'mit', 'eecs.6002x', 'asset', 'selfie.jpeg'])
        self.assertEqual(asset_location, Locator.to_locator_or_location(asset_location))
        self.assertEqual(asset_location, Locator.to_locator_or_location(asset_location.url()))

        def_location_url = "defx://version/" + '{:024x}'.format(random.randrange(16 ** 24))
        self.assertEqual(DefinitionLocator(def_location_url), Locator.to_locator_or_location(def_location_url))

        with self.assertRaises(ValueError):
            Locator.to_locator_or_location(22)
        with self.assertRaises(ValueError):
            Locator.to_locator_or_location("hello.world.not.a.url")
        self.assertIsNone(Locator.parse_url("unknown://foo.bar/baz"))
Ejemplo n.º 2
0
    def _construct(cls, system, contents, error_msg, location):

        if isinstance(location, dict) and 'course' in location:
            location = Location(location)
        if isinstance(location, Location) and location.name is None:
            location = location.replace(
                category='error',
                # Pick a unique url_name -- the sha1 hash of the contents.
                # NOTE: We could try to pull out the url_name of the errored descriptor,
                # but url_names aren't guaranteed to be unique between descriptor types,
                # and ErrorDescriptor can wrap any type.  When the wrapped module is fixed,
                # it will be written out with the original url_name.
                name=hashlib.sha1(contents.encode('utf8')).hexdigest())

        # real metadata stays in the content, but add a display name
        model_data = {
            'error_msg': str(error_msg),
            'contents': contents,
            'display_name': 'Error: ' + location.url(),
            'location': location,
            'category': 'error'
        }
        return cls(
            system,
            model_data,
        )
Ejemplo n.º 3
0
    def _construct(cls, system, contents, error_msg, location):
        location = Location(location)

        if location.category == "error":
            location = location.replace(
                # Pick a unique url_name -- the sha1 hash of the contents.
                # NOTE: We could try to pull out the url_name of the errored descriptor,
                # but url_names aren't guaranteed to be unique between descriptor types,
                # and ErrorDescriptor can wrap any type.  When the wrapped module is fixed,
                # it will be written out with the original url_name.
                name=hashlib.sha1(contents.encode("utf8")).hexdigest()
            )

        # real metadata stays in the content, but add a display name
        field_data = DictFieldData(
            {
                "error_msg": str(error_msg),
                "contents": contents,
                "display_name": "Error: " + location.url(),
                "location": location,
                "category": "error",
            }
        )
        return system.construct_xblock_from_class(
            cls,
            # The error module doesn't use scoped data, and thus doesn't need
            # real scope keys
            ScopeIds("error", None, location, location),
            field_data,
        )
Ejemplo n.º 4
0
def _clone_modules(modulestore, modules, dest_location):
    for module in modules:
        original_loc = Location(module.location)

        if original_loc.category != 'course':
            module.location = module.location._replace(
                tag=dest_location.tag, org=dest_location.org, course=dest_location.course)
        else:
            # on the course module we also have to update the module name
            module.location = module.location._replace(
                tag=dest_location.tag, org=dest_location.org, course=dest_location.course, name=dest_location.name)

        print "Cloning module {0} to {1}....".format(original_loc, module.location)

        # NOTE: usage of the the internal module.xblock_kvs._data does not include any 'default' values for the fields
        modulestore.update_item(module.location, module.xblock_kvs._data)

        # repoint children
        if module.has_children:
            new_children = []
            for child_loc_url in module.children:
                child_loc = Location(child_loc_url)
                child_loc = child_loc._replace(
                    tag=dest_location.tag,
                    org=dest_location.org,
                    course=dest_location.course
                )
                new_children.append(child_loc.url())

            modulestore.update_children(module.location, new_children)

        # save metadata
        modulestore.update_metadata(module.location, own_metadata(module))
Ejemplo n.º 5
0
    def _construct(cls, system, contents, error_msg, location):

        if isinstance(location, dict) and 'course' in location:
            location = Location(location)
        if isinstance(location, Location) and location.name is None:
            location = location.replace(
                category='error',
                # Pick a unique url_name -- the sha1 hash of the contents.
                # NOTE: We could try to pull out the url_name of the errored descriptor,
                # but url_names aren't guaranteed to be unique between descriptor types,
                # and ErrorDescriptor can wrap any type.  When the wrapped module is fixed,
                # it will be written out with the original url_name.
                name=hashlib.sha1(contents.encode('utf8')).hexdigest())

        # real metadata stays in the content, but add a display name
        field_data = DictFieldData({
            'error_msg': str(error_msg),
            'contents': contents,
            'display_name': 'Error: ' + location.url(),
            'location': location,
            'category': 'error'
        })
        return system.construct_xblock_from_class(
            cls,
            field_data,
            # The error module doesn't use scoped data, and thus doesn't need
            # real scope keys
            ScopeIds('error', None, location, location))
Ejemplo n.º 6
0
def delete_item(request):
    item_location = request.POST['id']
    item_location = Location(item_location)

    # check permissions for this user within this course
    if not has_access(request.user, item_location):
        raise PermissionDenied()

    # optional parameter to delete all children (default False)
    delete_children = request.POST.get('delete_children', False)
    delete_all_versions = request.POST.get('delete_all_versions', False)

    store = get_modulestore(item_location)

    item = store.get_item(item_location)

    if delete_children:
        _xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions))
    else:
        store.delete_item(item.location, delete_all_versions)

    # cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
    if delete_all_versions:
        parent_locs = modulestore('direct').get_parent_locations(item_location, None)

        for parent_loc in parent_locs:
            parent = modulestore('direct').get_item(parent_loc)
            item_url = item_location.url()
            if item_url in parent.children:
                children = parent.children
                children.remove(item_url)
                parent.children = children
                modulestore('direct').update_children(parent.location, parent.children)

    return JsonResponse()
Ejemplo n.º 7
0
 def _create_item(self, category, name, data, metadata, parent_category, parent_name, runtime):
     """
     Create the item of the given category and block id in split and old mongo, add it to the optional
     parent. The parent category is only needed because old mongo requires it for the id.
     """
     location = Location('i4x', 'test_org', 'test_course', category, name)
     self.old_mongo.create_and_save_xmodule(location, data, metadata, runtime)
     if isinstance(data, basestring):
         fields = {'data': data}
     else:
         fields = data.copy()
     fields.update(metadata)
     if parent_name:
         # add child to parent in mongo
         parent_location = Location('i4x', 'test_org', 'test_course', parent_category, parent_name)
         parent = self.old_mongo.get_item(parent_location)
         parent.children.append(location.url())
         self.old_mongo.update_item(parent, self.userid)
         # create pointer for split
         course_or_parent_locator = BlockUsageLocator(
             package_id=self.split_package_id,
             branch='draft',
             block_id=parent_name
         )
     else:
         course_or_parent_locator = CourseLocator(
             package_id='test_org.test_course.runid',
             branch='draft',
         )
     self.split_mongo.create_item(course_or_parent_locator, category, self.userid, block_id=name, fields=fields)
Ejemplo n.º 8
0
    def _construct(cls, system, contents, error_msg, location):

        if isinstance(location, dict) and 'course' in location:
            location = Location(location)
        if isinstance(location, Location) and location.name is None:
            location = location.replace(
                category='error',
                # Pick a unique url_name -- the sha1 hash of the contents.
                # NOTE: We could try to pull out the url_name of the errored descriptor,
                # but url_names aren't guaranteed to be unique between descriptor types,
                # and ErrorDescriptor can wrap any type.  When the wrapped module is fixed,
                # it will be written out with the original url_name.
                name=hashlib.sha1(contents.encode('utf8')).hexdigest()
            )

        # real metadata stays in the content, but add a display name
        field_data = DictFieldData({
            'error_msg': str(error_msg),
            'contents': contents,
            'display_name': 'Error: ' + location.url(),
            'location': location,
            'category': 'error'
        })
        return system.construct_xblock_from_class(
            cls,
            field_data,
            # The error module doesn't use scoped data, and thus doesn't need
            # real scope keys
            ScopeIds('error', None, location, location)
        )
Ejemplo n.º 9
0
 def _create_item(self, category, name, data, metadata, parent_category, parent_name, runtime):
     """
     Create the item of the given category and block id in split and old mongo, add it to the optional
     parent. The parent category is only needed because old mongo requires it for the id.
     """
     location = Location('i4x', 'test_org', 'test_course', category, name)
     self.old_mongo.create_and_save_xmodule(location, data, metadata, runtime)
     if isinstance(data, basestring):
         fields = {'data': data}
     else:
         fields = data.copy()
     fields.update(metadata)
     if parent_name:
         # add child to parent in mongo
         parent_location = Location('i4x', 'test_org', 'test_course', parent_category, parent_name)
         parent = self.old_mongo.get_item(parent_location)
         parent.children.append(location.url())
         self.old_mongo.update_children(parent_location, parent.children)
         # create pointer for split
         course_or_parent_locator = BlockUsageLocator(
             course_id=self.split_course_id,
             branch='draft',
             usage_id=parent_name
         )
     else:
         course_or_parent_locator = CourseLocator(
             course_id='test_org.test_course.runid',
             branch='draft',
         )
     self.split_mongo.create_item(course_or_parent_locator, category, self.userid, usage_id=name, fields=fields)
Ejemplo n.º 10
0
def _clone_modules(modulestore, modules, source_location, dest_location):
    for module in modules:
        original_loc = Location(module.location)

        if original_loc.category != "course":
            module.location = module.location._replace(
                tag=dest_location.tag, org=dest_location.org, course=dest_location.course
            )
        else:
            # on the course module we also have to update the module name
            module.location = module.location._replace(
                tag=dest_location.tag, org=dest_location.org, course=dest_location.course, name=dest_location.name
            )

        print "Cloning module {0} to {1}....".format(original_loc, module.location)

        if "data" in module.fields and module.fields["data"].is_set_on(module) and isinstance(module.data, basestring):
            module.data = rewrite_nonportable_content_links(
                source_location.course_id, dest_location.course_id, module.data
            )

        # repoint children
        if module.has_children:
            new_children = []
            for child_loc_url in module.children:
                child_loc = Location(child_loc_url)
                child_loc = child_loc._replace(
                    tag=dest_location.tag, org=dest_location.org, course=dest_location.course
                )
                new_children.append(child_loc.url())

            module.children = new_children

        modulestore.update_item(module, "**replace_user**")
Ejemplo n.º 11
0
def delete_item(request):
    item_location = request.POST['id']
    item_loc = Location(item_location)

    # check permissions for this user within this course
    if not has_access(request.user, item_location):
        raise PermissionDenied()

    # optional parameter to delete all children (default False)
    delete_children = request.POST.get('delete_children', False)
    delete_all_versions = request.POST.get('delete_all_versions', False)

    store = get_modulestore(item_location)

    item = store.get_item(item_location)

    if delete_children:
        _xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions))
    else:
        store.delete_item(item.location, delete_all_versions)

    # cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
    if delete_all_versions:
        parent_locs = modulestore('direct').get_parent_locations(item_loc, None)

        for parent_loc in parent_locs:
            parent = modulestore('direct').get_item(parent_loc)
            item_url = item_loc.url()
            if item_url in parent.children:
                children = parent.children
                children.remove(item_url)
                parent.children = children
                modulestore('direct').update_children(parent.location, parent.children)

    return HttpResponse()
Ejemplo n.º 12
0
    def _construct(cls, system, contents, error_msg, location):

        if isinstance(location, dict) and 'course' in location:
            location = Location(location)
        if isinstance(location, Location) and location.name is None:
            location = location.replace(
                category='error',
                # Pick a unique url_name -- the sha1 hash of the contents.
                # NOTE: We could try to pull out the url_name of the errored descriptor,
                # but url_names aren't guaranteed to be unique between descriptor types,
                # and ErrorDescriptor can wrap any type.  When the wrapped module is fixed,
                # it will be written out with the original url_name.
                name=hashlib.sha1(contents.encode('utf8')).hexdigest()
            )

        # real metadata stays in the content, but add a display name
        model_data = {
            'error_msg': str(error_msg),
            'contents': contents,
            'display_name': 'Error: ' + location.url(),
            'location': location,
            'category': 'error'
        }
        return cls(
            system,
            model_data,
        )
Ejemplo n.º 13
0
    def export_all_for_course(self, course_location, output_directory,
                              assets_policy_file):
        """
        Export all of this course's assets to the output_directory. Export all of the assets'
        attributes to the policy file.

        :param course_location: the Location of type 'course'
        :param output_directory: the directory under which to put all the asset files
        :param assets_policy_file: the filename for the policy file which should be in the same
        directory as the other policy files.
        """
        policy = {}
        assets = self.get_all_content_for_course(course_location)

        for asset in assets:
            asset_location = Location(asset['_id'])
            self.export(asset_location, output_directory)
            for attr, value in asset.iteritems():
                if attr not in [
                        '_id', 'md5', 'uploadDate', 'length', 'chunkSize'
                ]:
                    policy.setdefault(asset_location.url(), {})[attr] = value

        with open(assets_policy_file, 'w') as f:
            json.dump(policy, f)
Ejemplo n.º 14
0
    def create_item(self, course_or_parent_loc, category, user_id=None, **kwargs):
        """
        Create and return the item. If parent_loc is a specific location v a course id,
        it installs the new item as a child of the parent (if the parent_loc is a specific
        xblock reference).

        :param course_or_parent_loc: Can be a course_id (org/course/run), CourseLocator,
        Location, or BlockUsageLocator but must be what the persistence modulestore expects
        """
        # find the store for the course
        course_id = self._infer_course_id_try(course_or_parent_loc)
        if course_id is None:
            raise ItemNotFoundError(u"Cannot find modulestore for %s" % course_or_parent_loc)

        store = self._get_modulestore_for_courseid(course_id)

        location = kwargs.pop('location', None)
        # invoke its create_item
        if isinstance(store, MongoModuleStore):
            block_id = kwargs.pop('block_id', getattr(location, 'name', uuid4().hex))
            # convert parent loc if it's legit
            if isinstance(course_or_parent_loc, basestring):
                parent_loc = None
                if location is None:
                    loc_dict = Location.parse_course_id(course_id)
                    loc_dict['name'] = block_id
                    location = Location(category=category, **loc_dict)
            else:
                parent_loc = course_or_parent_loc
                # must have a legitimate location, compute if appropriate
                if location is None:
                    location = parent_loc.replace(category=category, name=block_id)
            # do the actual creation
            xblock = store.create_and_save_xmodule(location, **kwargs)
            # don't forget to attach to parent
            if parent_loc is not None and not 'detached' in xblock._class_tags:
                parent = store.get_item(parent_loc)
                parent.children.append(location.url())
                store.update_item(parent)
        elif isinstance(store, SplitMongoModuleStore):
            if isinstance(course_or_parent_loc, basestring):  # course_id
                course_or_parent_loc = loc_mapper().translate_location_to_course_locator(
                    # hardcode draft version until we figure out how we're handling branches from app
                    course_or_parent_loc, None, published=False
                )
            elif not isinstance(course_or_parent_loc, CourseLocator):
                raise ValueError(u"Cannot create a child of {} in split. Wrong repr.".format(course_or_parent_loc))

            # split handles all the fields in one dict not separated by scope
            fields = kwargs.get('fields', {})
            fields.update(kwargs.pop('metadata', {}))
            fields.update(kwargs.pop('definition_data', {}))
            kwargs['fields'] = fields

            xblock = store.create_item(course_or_parent_loc, category, user_id, **kwargs)
        else:
            raise NotImplementedError(u"Cannot create an item on store %s" % store)

        return xblock
Ejemplo n.º 15
0
    def test_old_location_helpers(self):
        """
        Test the functions intended to help with the conversion from old locations to locators
        """
        location_tuple = ('i4x', 'mit', 'eecs.6002x', 'course', 't3_2013')
        location = Location(location_tuple)
        self.assertEqual(location, Locator.to_locator_or_location(location))
        self.assertEqual(location,
                         Locator.to_locator_or_location(location_tuple))
        self.assertEqual(location,
                         Locator.to_locator_or_location(list(location_tuple)))
        self.assertEqual(location,
                         Locator.to_locator_or_location(location.dict()))

        locator = BlockUsageLocator(course_id='foo.bar',
                                    branch='alpha',
                                    usage_id='deep')
        self.assertEqual(locator, Locator.to_locator_or_location(locator))
        self.assertEqual(
            locator.as_course_locator(),
            Locator.to_locator_or_location(locator.as_course_locator()))
        self.assertEqual(location,
                         Locator.to_locator_or_location(location.url()))
        self.assertEqual(locator,
                         Locator.to_locator_or_location(locator.url()))
        self.assertEqual(locator,
                         Locator.to_locator_or_location(locator.__dict__))

        asset_location = Location(
            ['c4x', 'mit', 'eecs.6002x', 'asset', 'selfie.jpeg'])
        self.assertEqual(asset_location,
                         Locator.to_locator_or_location(asset_location))
        self.assertEqual(asset_location,
                         Locator.to_locator_or_location(asset_location.url()))

        def_location_url = "defx://version/" + '{:024x}'.format(
            random.randrange(16**24))
        self.assertEqual(DefinitionLocator(def_location_url),
                         Locator.to_locator_or_location(def_location_url))

        with self.assertRaises(ValueError):
            Locator.to_locator_or_location(22)
        with self.assertRaises(ValueError):
            Locator.to_locator_or_location("hello.world.not.a.url")
        self.assertIsNone(Locator.parse_url("unknown://foo.bar/baz"))
Ejemplo n.º 16
0
def _clone_modules(modulestore, modules, source_location, dest_location):
    for module in modules:
        original_loc = Location(module.location)

        if original_loc.category != 'course':
            new_location = module.location._replace(
                tag=dest_location.tag,
                org=dest_location.org,
                course=dest_location.course
            )
            module.scope_ids = module.scope_ids._replace(
                def_id=new_location,
                usage_id=new_location
            )
        else:
            # on the course module we also have to update the module name
            new_location = module.location._replace(
                tag=dest_location.tag,
                org=dest_location.org,
                course=dest_location.course,
                name=dest_location.name
            )
            module.scope_ids = module.scope_ids._replace(
                def_id=new_location,
                usage_id=new_location
            )

        print "Cloning module {0} to {1}....".format(original_loc, module.location)

        # NOTE: usage of the the internal module.xblock_kvs._data does not include any 'default' values for the fields
        data = module.xblock_kvs._data
        if isinstance(data, basestring):
            data = rewrite_nonportable_content_links(
                source_location.course_id, dest_location.course_id, data)

        modulestore.update_item(module.location, data)

        # repoint children
        if module.has_children:
            new_children = []
            for child_loc_url in module.children:
                child_loc = Location(child_loc_url)
                child_loc = child_loc._replace(
                    tag=dest_location.tag,
                    org=dest_location.org,
                    course=dest_location.course
                )
                new_children.append(child_loc.url())

            modulestore.update_children(module.location, new_children)

        # save metadata
        modulestore.update_metadata(module.location, own_metadata(module))
Ejemplo n.º 17
0
    def test_metadata_inheritance(self):
        module_store = modulestore('direct')
        import_from_xml(module_store, 'common/test/data/', ['full'])

        course = module_store.get_item(
            Location(
                ['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))

        verticals = module_store.get_items(
            ['i4x', 'edX', 'full', 'vertical', None, None])

        # let's assert on the metadata_inheritance on an existing vertical
        for vertical in verticals:
            self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key)

        self.assertGreater(len(verticals), 0)

        new_component_location = Location('i4x', 'edX', 'full', 'html',
                                          'new_component')
        source_template_location = Location('i4x', 'edx', 'templates', 'html',
                                            'Blank_HTML_Page')

        # crate a new module and add it as a child to a vertical
        module_store.clone_item(source_template_location,
                                new_component_location)
        parent = verticals[0]
        module_store.update_children(
            parent.location, parent.children + [new_component_location.url()])

        # flush the cache
        module_store.refresh_cached_metadata_inheritance_tree(
            new_component_location)
        new_module = module_store.get_item(new_component_location)

        # check for grace period definition which should be defined at the course level
        self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod)

        self.assertEqual(course.lms.xqa_key, new_module.lms.xqa_key)

        #
        # now let's define an override at the leaf node level
        #
        new_module.lms.graceperiod = timedelta(1)
        module_store.update_metadata(new_module.location,
                                     own_metadata(new_module))

        # flush the cache and refetch
        module_store.refresh_cached_metadata_inheritance_tree(
            new_component_location)
        new_module = module_store.get_item(new_component_location)

        self.assertEqual(timedelta(1), new_module.lms.graceperiod)
Ejemplo n.º 18
0
    def test_metadata_inheritance(self):
        module_store = modulestore('direct')
        import_from_xml(module_store, 'common/test/data/', ['full'])

        course = module_store.get_item(Location(
            ['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))

        verticals = module_store.get_items(
            ['i4x', 'edX', 'full', 'vertical', None, None])

        # let's assert on the metadata_inheritance on an existing vertical
        for vertical in verticals:
            self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key)

        self.assertGreater(len(verticals), 0)

        new_component_location = Location(
            'i4x', 'edX', 'full', 'html', 'new_component')
        source_template_location = Location(
            'i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')

        # crate a new module and add it as a child to a vertical
        module_store.clone_item(
            source_template_location, new_component_location)
        parent = verticals[0]
        module_store.update_children(parent.location, parent.children + [
                                     new_component_location.url()])

        # flush the cache
        module_store.refresh_cached_metadata_inheritance_tree(
            new_component_location)
        new_module = module_store.get_item(new_component_location)

        # check for grace period definition which should be defined at the
        # course level
        self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod)

        self.assertEqual(course.lms.xqa_key, new_module.lms.xqa_key)

        #
        # now let's define an override at the leaf node level
        #
        new_module.lms.graceperiod = timedelta(1)
        module_store.update_metadata(
            new_module.location, own_metadata(new_module))

        # flush the cache and refetch
        module_store.refresh_cached_metadata_inheritance_tree(
            new_component_location)
        new_module = module_store.get_item(new_component_location)

        self.assertEqual(timedelta(1), new_module.lms.graceperiod)
Ejemplo n.º 19
0
def _clone_modules(modulestore, modules, source_location, dest_location):
    for module in modules:
        original_loc = Location(module.location)

        if original_loc.category != 'course':
            new_location = module.location._replace(
                tag=dest_location.tag,
                org=dest_location.org,
                course=dest_location.course)
            module.scope_ids = module.scope_ids._replace(def_id=new_location,
                                                         usage_id=new_location)
        else:
            # on the course module we also have to update the module name
            new_location = module.location._replace(
                tag=dest_location.tag,
                org=dest_location.org,
                course=dest_location.course,
                name=dest_location.name)
            module.scope_ids = module.scope_ids._replace(def_id=new_location,
                                                         usage_id=new_location)

        print "Cloning module {0} to {1}....".format(original_loc,
                                                     module.location)

        # NOTE: usage of the the internal module.xblock_kvs._data does not include any 'default' values for the fields
        data = module.xblock_kvs._data
        if isinstance(data, basestring):
            data = rewrite_nonportable_content_links(source_location.course_id,
                                                     dest_location.course_id,
                                                     data)

        modulestore.update_item(module.location, data)

        # repoint children
        if module.has_children:
            new_children = []
            for child_loc_url in module.children:
                child_loc = Location(child_loc_url)
                child_loc = child_loc._replace(tag=dest_location.tag,
                                               org=dest_location.org,
                                               course=dest_location.course)
                new_children.append(child_loc.url())

            modulestore.update_children(module.location, new_children)

        # save metadata
        modulestore.update_metadata(module.location, own_metadata(module))
Ejemplo n.º 20
0
def _clone_modules(modulestore, modules, source_location, dest_location):
    for module in modules:
        original_loc = Location(module.location)

        if original_loc.category != 'course':
            module.location = module.location._replace(
                tag=dest_location.tag,
                org=dest_location.org,
                course=dest_location.course
            )
        else:
            # on the course module we also have to update the module name
            module.location = module.location._replace(
                tag=dest_location.tag,
                org=dest_location.org,
                course=dest_location.course,
                name=dest_location.name
            )

        print "Cloning module {0} to {1}....".format(original_loc, module.location)

        if 'data' in module.fields and module.fields['data'].is_set_on(module) and isinstance(module.data, basestring):
            module.data = rewrite_nonportable_content_links(
                source_location.course_id, dest_location.course_id, module.data
            )

        # repoint children
        if module.has_children:
            new_children = []
            for child_loc_url in module.children:
                child_loc = Location(child_loc_url)
                child_loc = child_loc._replace(
                    tag=dest_location.tag,
                    org=dest_location.org,
                    course=dest_location.course
                )
                new_children.append(child_loc.url())

            module.children = new_children

        modulestore.update_item(module, '**replace_user**')
Ejemplo n.º 21
0
    def export_all_for_course(self, course_location, output_directory, assets_policy_file):
        """
        Export all of this course's assets to the output_directory. Export all of the assets'
        attributes to the policy file.

        :param course_location: the Location of type 'course'
        :param output_directory: the directory under which to put all the asset files
        :param assets_policy_file: the filename for the policy file which should be in the same
        directory as the other policy files.
        """
        policy = {}
        assets = self.get_all_content_for_course(course_location)

        for asset in assets:
            asset_location = Location(asset['_id'])
            self.export(asset_location, output_directory)
            for attr, value in asset.iteritems():
                if attr not in ['_id', 'md5', 'uploadDate', 'length', 'chunkSize']:
                    policy.setdefault(asset_location.url(), {})[attr] = value

        with open(assets_policy_file, 'w') as f:
            json.dump(policy, f)
    def _construct(cls, system, contents, error_msg, location):

        if isinstance(location, dict) and "course" in location:
            location = Location(location)
        if isinstance(location, Location) and location.name is None:
            location = location.replace(
                category="error",
                # Pick a unique url_name -- the sha1 hash of the contents.
                # NOTE: We could try to pull out the url_name of the errored descriptor,
                # but url_names aren't guaranteed to be unique between descriptor types,
                # and ErrorDescriptor can wrap any type.  When the wrapped module is fixed,
                # it will be written out with the original url_name.
                name=hashlib.sha1(contents.encode("utf8")).hexdigest(),
            )

        # real metadata stays in the content, but add a display name
        model_data = {
            "error_msg": str(error_msg),
            "contents": contents,
            "display_name": "Error: " + location.url(),
            "location": location,
            "category": "error",
        }
        return cls(system, model_data)
Ejemplo n.º 23
0
    def test_cms_imported_course_walkthrough(self):
        """
        Import and walk through some common URL endpoints. This just verifies non-500 and no other
        correct behavior, so it is not a deep test
        """
        import_from_xml(modulestore('direct'), 'common/test/data/', ['simple'])
        loc = Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None])
        resp = self.client.get(reverse('course_index',
                                       kwargs={'org': loc.org,
                                               'course': loc.course,
                                               'name': loc.name}))

        self.assertEqual(200, resp.status_code)
        self.assertContains(resp, 'Chapter 2')

        # go to various pages

        # import page
        resp = self.client.get(reverse('import_course',
                                       kwargs={'org': loc.org,
                                               'course': loc.course,
                                               'name': loc.name}))
        self.assertEqual(200, resp.status_code)

        # export page
        resp = self.client.get(reverse('export_course',
                                       kwargs={'org': loc.org,
                                               'course': loc.course,
                                               'name': loc.name}))
        self.assertEqual(200, resp.status_code)

        # manage users
        resp = self.client.get(reverse('manage_users',
                                       kwargs={'location': loc.url()}))
        self.assertEqual(200, resp.status_code)

        # course info
        resp = self.client.get(reverse('course_info',
                                       kwargs={'org': loc.org,
                                               'course': loc.course,
                                               'name': loc.name}))
        self.assertEqual(200, resp.status_code)

        # settings_details
        resp = self.client.get(reverse('settings_details',
                                       kwargs={'org': loc.org,
                                               'course': loc.course,
                                               'name': loc.name}))
        self.assertEqual(200, resp.status_code)

        # settings_details
        resp = self.client.get(reverse('settings_grading',
                                       kwargs={'org': loc.org,
                                               'course': loc.course,
                                               'name': loc.name}))
        self.assertEqual(200, resp.status_code)

        # static_pages
        resp = self.client.get(reverse('static_pages',
                                       kwargs={'org': loc.org,
                                               'course': loc.course,
                                               'coursename': loc.name}))
        self.assertEqual(200, resp.status_code)

        # static_pages
        resp = self.client.get(reverse('asset_index',
                                       kwargs={'org': loc.org,
                                               'course': loc.course,
                                               'name': loc.name}))
        self.assertEqual(200, resp.status_code)

        # go look at a subsection page
        subsection_location = loc.replace(category='sequential', name='test_sequence')
        resp = self.client.get(reverse('edit_subsection',
                                       kwargs={'location': subsection_location.url()}))
        self.assertEqual(200, resp.status_code)

        # go look at the Edit page
        unit_location = loc.replace(category='vertical', name='test_vertical')
        resp = self.client.get(reverse('edit_unit',
                                       kwargs={'location': unit_location.url()}))
        self.assertEqual(200, resp.status_code)

        # delete a component
        del_loc = loc.replace(category='html', name='test_html')
        resp = self.client.post(reverse('delete_item'),
                                json.dumps({'id': del_loc.url()}), "application/json")
        self.assertEqual(200, resp.status_code)

        # delete a unit
        del_loc = loc.replace(category='vertical', name='test_vertical')
        resp = self.client.post(reverse('delete_item'),
                                json.dumps({'id': del_loc.url()}), "application/json")
        self.assertEqual(200, resp.status_code)

        # delete a unit
        del_loc = loc.replace(category='sequential', name='test_sequence')
        resp = self.client.post(reverse('delete_item'),
                                json.dumps({'id': del_loc.url()}), "application/json")
        self.assertEqual(200, resp.status_code)

        # delete a chapter
        del_loc = loc.replace(category='chapter', name='chapter_2')
        resp = self.client.post(reverse('delete_item'),
                                json.dumps({'id': del_loc.url()}), "application/json")
        self.assertEqual(200, resp.status_code)
Ejemplo n.º 24
0
def clone_course(modulestore, contentstore, source_location, dest_location, delete_original=False):
    # first check to see if the modulestore is Mongo backed
    if not isinstance(modulestore, MongoModuleStore):
        raise Exception("Expected a MongoModuleStore in the runtime. Aborting....")

    # check to see if the dest_location exists as an empty course
    # we need an empty course because the app layers manage the permissions and users
    if not modulestore.has_item(dest_location):
        raise Exception("An empty course at {0} must have already been created. Aborting...".format(dest_location))

    # verify that the dest_location really is an empty course, which means only one with an optional 'overview'
    dest_modules = modulestore.get_items([dest_location.tag, dest_location.org, dest_location.course, None, None, None])

    basically_empty = True
    for module in dest_modules:
        if module.location.category == 'course' or (module.location.category == 'about'
                                                    and module.location.name == 'overview'):
            continue

        basically_empty = False
        break

    if not basically_empty:
        raise Exception("Course at destination {0} is not an empty course. You can only clone into an empty course. Aborting...".format(dest_location))

    # check to see if the source course is actually there
    if not modulestore.has_item(source_location):
        raise Exception("Cannot find a course at {0}. Aborting".format(source_location))

    # Get all modules under this namespace which is (tag, org, course) tuple

    modules = modulestore.get_items([source_location.tag, source_location.org, source_location.course, None, None, None])

    for module in modules:
        original_loc = Location(module.location)

        if original_loc.category != 'course':
            module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
                                                       course=dest_location.course)
        else:
            # on the course module we also have to update the module name
            module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
                                                       course=dest_location.course, name=dest_location.name)

        print "Cloning module {0} to {1}....".format(original_loc, module.location)

        modulestore.update_item(module.location, module._model_data._kvs._data)

        # repoint children
        if module.has_children:
            new_children = []
            for child_loc_url in module.children:
                child_loc = Location(child_loc_url)
                child_loc = child_loc._replace(
                    tag=dest_location.tag,
                    org=dest_location.org,
                    course=dest_location.course
                )
                new_children.append(child_loc.url())

            modulestore.update_children(module.location, new_children)

        # save metadata
        modulestore.update_metadata(module.location, module._model_data._kvs._metadata)

    # now iterate through all of the assets and clone them
    # first the thumbnails
    thumbs = contentstore.get_all_content_thumbnails_for_course(source_location)
    for thumb in thumbs:
        thumb_loc = Location(thumb["_id"])
        content = contentstore.find(thumb_loc)
        content.location = content.location._replace(org=dest_location.org,
                                                     course=dest_location.course)

        print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location)

        contentstore.save(content)

    # now iterate through all of the assets, also updating the thumbnail pointer

    assets = contentstore.get_all_content_for_course(source_location)
    for asset in assets:
        asset_loc = Location(asset["_id"])
        content = contentstore.find(asset_loc)
        content.location = content.location._replace(org=dest_location.org,
                                                     course=dest_location.course)

        # be sure to update the pointer to the thumbnail
        if content.thumbnail_location is not None:
            content.thumbnail_location = content.thumbnail_location._replace(org=dest_location.org,
                                                                             course=dest_location.course)

        print "Cloning asset {0} to {1}".format(asset_loc, content.location)

        contentstore.save(content)

    return True
Ejemplo n.º 25
0
class RolesTestCase(TestCase):
    """
    Tests of student.roles
    """

    def setUp(self):
        self.course = Location('i4x://edX/toy/course/2012_Fall')
        self.anonymous_user = AnonymousUserFactory()
        self.student = UserFactory()
        self.global_staff = UserFactory(is_staff=True)
        self.course_staff = StaffFactory(course=self.course)
        self.course_instructor = InstructorFactory(course=self.course)

    def test_global_staff(self):
        self.assertFalse(GlobalStaff().has_user(self.student))
        self.assertFalse(GlobalStaff().has_user(self.course_staff))
        self.assertFalse(GlobalStaff().has_user(self.course_instructor))
        self.assertTrue(GlobalStaff().has_user(self.global_staff))

    def test_group_name_case_insensitive(self):
        uppercase_loc = "i4x://ORG/COURSE/course/NAME"
        lowercase_loc = uppercase_loc.lower()

        lowercase_group = "role_org/course/name"
        uppercase_group = lowercase_group.upper()

        lowercase_user = UserFactory(groups=lowercase_group)
        uppercase_user = UserFactory(groups=uppercase_group)

        self.assertTrue(CourseRole("role", lowercase_loc).has_user(lowercase_user))
        self.assertTrue(CourseRole("role", uppercase_loc).has_user(lowercase_user))
        self.assertTrue(CourseRole("role", lowercase_loc).has_user(uppercase_user))
        self.assertTrue(CourseRole("role", uppercase_loc).has_user(uppercase_user))

    def test_course_role(self):
        """
        Test that giving a user a course role enables access appropriately
        """
        course_locator = loc_mapper().translate_location(
            self.course.course_id, self.course, add_entry_if_missing=True
        )
        self.assertFalse(
            CourseStaffRole(course_locator).has_user(self.student),
            "Student has premature access to {}".format(unicode(course_locator))
        )
        self.assertFalse(
            CourseStaffRole(self.course).has_user(self.student),
            "Student has premature access to {}".format(self.course.url())
        )
        CourseStaffRole(course_locator).add_users(self.student)
        self.assertTrue(
            CourseStaffRole(course_locator).has_user(self.student),
            "Student doesn't have access to {}".format(unicode(course_locator))
        )
        self.assertTrue(
            CourseStaffRole(self.course).has_user(self.student),
            "Student doesn't have access to {}".format(unicode(self.course.url()))
        )
        # now try accessing something internal to the course
        vertical_locator = BlockUsageLocator(
            package_id=course_locator.package_id, branch='published', block_id='madeup'
        )
        vertical_location = self.course.replace(category='vertical', name='madeuptoo')
        self.assertTrue(
            CourseStaffRole(vertical_locator).has_user(self.student),
            "Student doesn't have access to {}".format(unicode(vertical_locator))
        )
        self.assertTrue(
            CourseStaffRole(vertical_location, course_context=self.course.course_id).has_user(self.student),
            "Student doesn't have access to {}".format(unicode(vertical_location.url()))
        )
Ejemplo n.º 26
0
def clone_course(modulestore,
                 contentstore,
                 source_location,
                 dest_location,
                 delete_original=False):
    # first check to see if the modulestore is Mongo backed
    if not isinstance(modulestore, MongoModuleStore):
        raise Exception(
            "Expected a MongoModuleStore in the runtime. Aborting....")

    # check to see if the dest_location exists as an empty course
    # we need an empty course because the app layers manage the permissions and users
    if not modulestore.has_item(dest_location):
        raise Exception(
            "An empty course at {0} must have already been created. Aborting..."
            .format(dest_location))

    # verify that the dest_location really is an empty course, which means only one with an optional 'overview'
    dest_modules = modulestore.get_items([
        dest_location.tag, dest_location.org, dest_location.course, None, None,
        None
    ])

    basically_empty = True
    for module in dest_modules:
        if module.location.category == 'course' or (
                module.location.category == 'about'
                and module.location.name == 'overview'):
            continue

        basically_empty = False
        break

    if not basically_empty:
        raise Exception(
            "Course at destination {0} is not an empty course. You can only clone into an empty course. Aborting..."
            .format(dest_location))

    # check to see if the source course is actually there
    if not modulestore.has_item(source_location):
        raise Exception(
            "Cannot find a course at {0}. Aborting".format(source_location))

    # Get all modules under this namespace which is (tag, org, course) tuple

    modules = modulestore.get_items([
        source_location.tag, source_location.org, source_location.course, None,
        None, None
    ])

    for module in modules:
        original_loc = Location(module.location)

        if original_loc.category != 'course':
            module.location = module.location._replace(
                tag=dest_location.tag,
                org=dest_location.org,
                course=dest_location.course)
        else:
            # on the course module we also have to update the module name
            module.location = module.location._replace(
                tag=dest_location.tag,
                org=dest_location.org,
                course=dest_location.course,
                name=dest_location.name)

        print "Cloning module {0} to {1}....".format(original_loc,
                                                     module.location)

        modulestore.update_item(module.location, module._model_data._kvs._data)

        # repoint children
        if module.has_children:
            new_children = []
            for child_loc_url in module.children:
                child_loc = Location(child_loc_url)
                child_loc = child_loc._replace(tag=dest_location.tag,
                                               org=dest_location.org,
                                               course=dest_location.course)
                new_children.append(child_loc.url())

            modulestore.update_children(module.location, new_children)

        # save metadata
        modulestore.update_metadata(module.location,
                                    module._model_data._kvs._metadata)

    # now iterate through all of the assets and clone them
    # first the thumbnails
    thumbs = contentstore.get_all_content_thumbnails_for_course(
        source_location)
    for thumb in thumbs:
        thumb_loc = Location(thumb["_id"])
        content = contentstore.find(thumb_loc)
        content.location = content.location._replace(
            org=dest_location.org, course=dest_location.course)

        print "Cloning thumbnail {0} to {1}".format(thumb_loc,
                                                    content.location)

        contentstore.save(content)

    # now iterate through all of the assets, also updating the thumbnail pointer

    assets = contentstore.get_all_content_for_course(source_location)
    for asset in assets:
        asset_loc = Location(asset["_id"])
        content = contentstore.find(asset_loc)
        content.location = content.location._replace(
            org=dest_location.org, course=dest_location.course)

        # be sure to update the pointer to the thumbnail
        if content.thumbnail_location is not None:
            content.thumbnail_location = content.thumbnail_location._replace(
                org=dest_location.org, course=dest_location.course)

        print "Cloning asset {0} to {1}".format(asset_loc, content.location)

        contentstore.save(content)

    return True
Ejemplo n.º 27
0
    def test_cms_imported_course_walkthrough(self):
        """
        Import and walk through some common URL endpoints. This just verifies non-500 and no other
        correct behavior, so it is not a deep test
        """
        import_from_xml(modulestore("direct"), "common/test/data/", ["simple"])
        loc = Location(["i4x", "edX", "simple", "course", "2012_Fall", None])
        resp = self.client.get(reverse("course_index", kwargs={"org": loc.org, "course": loc.course, "name": loc.name}))

        self.assertEqual(200, resp.status_code)
        self.assertContains(resp, "Chapter 2")

        # go to various pages

        # import page
        resp = self.client.get(
            reverse("import_course", kwargs={"org": loc.org, "course": loc.course, "name": loc.name})
        )
        self.assertEqual(200, resp.status_code)

        # export page
        resp = self.client.get(
            reverse("export_course", kwargs={"org": loc.org, "course": loc.course, "name": loc.name})
        )
        self.assertEqual(200, resp.status_code)

        # manage users
        resp = self.client.get(reverse("manage_users", kwargs={"location": loc.url()}))
        self.assertEqual(200, resp.status_code)

        # course info
        resp = self.client.get(reverse("course_info", kwargs={"org": loc.org, "course": loc.course, "name": loc.name}))
        self.assertEqual(200, resp.status_code)

        # settings_details
        resp = self.client.get(
            reverse("settings_details", kwargs={"org": loc.org, "course": loc.course, "name": loc.name})
        )
        self.assertEqual(200, resp.status_code)

        # settings_details
        resp = self.client.get(
            reverse("settings_grading", kwargs={"org": loc.org, "course": loc.course, "name": loc.name})
        )
        self.assertEqual(200, resp.status_code)

        # static_pages
        resp = self.client.get(
            reverse("static_pages", kwargs={"org": loc.org, "course": loc.course, "coursename": loc.name})
        )
        self.assertEqual(200, resp.status_code)

        # static_pages
        resp = self.client.get(reverse("asset_index", kwargs={"org": loc.org, "course": loc.course, "name": loc.name}))
        self.assertEqual(200, resp.status_code)

        # go look at a subsection page
        subsection_location = loc.replace(category="sequential", name="test_sequence")
        resp = self.client.get(reverse("edit_subsection", kwargs={"location": subsection_location.url()}))
        self.assertEqual(200, resp.status_code)

        # go look at the Edit page
        unit_location = loc.replace(category="vertical", name="test_vertical")
        resp = self.client.get(reverse("edit_unit", kwargs={"location": unit_location.url()}))
        self.assertEqual(200, resp.status_code)

        # delete a component
        del_loc = loc.replace(category="html", name="test_html")
        resp = self.client.post(reverse("delete_item"), json.dumps({"id": del_loc.url()}), "application/json")
        self.assertEqual(200, resp.status_code)

        # delete a unit
        del_loc = loc.replace(category="vertical", name="test_vertical")
        resp = self.client.post(reverse("delete_item"), json.dumps({"id": del_loc.url()}), "application/json")
        self.assertEqual(200, resp.status_code)

        # delete a unit
        del_loc = loc.replace(category="sequential", name="test_sequence")
        resp = self.client.post(reverse("delete_item"), json.dumps({"id": del_loc.url()}), "application/json")
        self.assertEqual(200, resp.status_code)

        # delete a chapter
        del_loc = loc.replace(category="chapter", name="chapter_2")
        resp = self.client.post(reverse("delete_item"), json.dumps({"id": del_loc.url()}), "application/json")
        self.assertEqual(200, resp.status_code)
Ejemplo n.º 28
0
    def test_encode_location(self):
        loc = Location('i4x', 'org', 'course', 'category', 'name')
        self.assertEqual(loc.url(), self.encoder.default(loc))

        loc = Location('i4x', 'org', 'course', 'category', 'name', 'version')
        self.assertEqual(loc.url(), self.encoder.default(loc))
Ejemplo n.º 29
0
    def create_item(self,
                    course_or_parent_loc,
                    category,
                    user_id=None,
                    **kwargs):
        """
        Create and return the item. If parent_loc is a specific location v a course id,
        it installs the new item as a child of the parent (if the parent_loc is a specific
        xblock reference).

        :param course_or_parent_loc: Can be a course_id (org/course/run), CourseLocator,
        Location, or BlockUsageLocator but must be what the persistence modulestore expects
        """
        # find the store for the course
        course_id = self._infer_course_id_try(course_or_parent_loc)
        if course_id is None:
            raise ItemNotFoundError(u"Cannot find modulestore for %s" %
                                    course_or_parent_loc)

        store = self._get_modulestore_for_courseid(course_id)

        location = kwargs.pop('location', None)
        # invoke its create_item
        if isinstance(store, MongoModuleStore):
            block_id = kwargs.pop('block_id',
                                  getattr(location, 'name',
                                          uuid4().hex))
            # convert parent loc if it's legit
            if isinstance(course_or_parent_loc, basestring):
                parent_loc = None
                if location is None:
                    loc_dict = Location.parse_course_id(course_id)
                    loc_dict['name'] = block_id
                    location = Location(category=category, **loc_dict)
            else:
                parent_loc = course_or_parent_loc
                # must have a legitimate location, compute if appropriate
                if location is None:
                    location = parent_loc.replace(category=category,
                                                  name=block_id)
            # do the actual creation
            xblock = store.create_and_save_xmodule(location, **kwargs)
            # don't forget to attach to parent
            if parent_loc is not None and not 'detached' in xblock._class_tags:
                parent = store.get_item(parent_loc)
                parent.children.append(location.url())
                store.update_item(parent)
        elif isinstance(store, SplitMongoModuleStore):
            if isinstance(course_or_parent_loc, basestring):  # course_id
                course_or_parent_loc = loc_mapper(
                ).translate_location_to_course_locator(
                    # hardcode draft version until we figure out how we're handling branches from app
                    course_or_parent_loc,
                    None,
                    published=False)
            elif not isinstance(course_or_parent_loc, CourseLocator):
                raise ValueError(
                    u"Cannot create a child of {} in split. Wrong repr.".
                    format(course_or_parent_loc))

            # split handles all the fields in one dict not separated by scope
            fields = kwargs.get('fields', {})
            fields.update(kwargs.pop('metadata', {}))
            fields.update(kwargs.pop('definition_data', {}))
            kwargs['fields'] = fields

            xblock = store.create_item(course_or_parent_loc, category, user_id,
                                       **kwargs)
        else:
            raise NotImplementedError(u"Cannot create an item on store %s" %
                                      store)

        return xblock
Ejemplo n.º 30
0
    def compute_metadata_inheritance_tree(self, location):
        '''
        TODO (cdodge) This method can be deleted when the 'split module store' work has been completed
        '''

        # get all collections in the course, this query should not return any leaf nodes
        # note this is a bit ugly as when we add new categories of containers, we have to add it here
        query = {'_id.org': location.org,
                 '_id.course': location.course,
                 '_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical', 'videosequence',
                                          'wrapper', 'problemset', 'conditional', 'randomize']}
                 }
        # we just want the Location, children, and inheritable metadata
        record_filter = {'_id': 1, 'definition.children': 1}

        # just get the inheritable metadata since that is all we need for the computation
        # this minimizes both data pushed over the wire
        for attr in INHERITABLE_METADATA:
            record_filter['metadata.{0}'.format(attr)] = 1

        # call out to the DB
        resultset = self.collection.find(query, record_filter)

        results_by_url = {}
        root = None

        # now go through the results and order them by the location url
        for result in resultset:
            location = Location(result['_id'])
            # We need to collate between draft and non-draft
            # i.e. draft verticals can have children which are not in non-draft versions
            location = location.replace(revision=None)
            location_url = location.url()
            if location_url in results_by_url:
                existing_children = results_by_url[location_url].get('definition', {}).get('children', [])
                additional_children = result.get('definition', {}).get('children', [])
                total_children = existing_children + additional_children
                if 'definition' not in results_by_url[location_url]:
                    results_by_url[location_url]['definition'] = {}
                results_by_url[location_url]['definition']['children'] = total_children
            results_by_url[location.url()] = result
            if location.category == 'course':
                root = location.url()

        # now traverse the tree and compute down the inherited metadata
        metadata_to_inherit = {}

        def _compute_inherited_metadata(url):
            """
            Helper method for computing inherited metadata for a specific location url
            """
            # check for presence of metadata key. Note that a given module may not yet be fully formed.
            # example: update_item -> update_children -> update_metadata sequence on new item create
            # if we get called here without update_metadata called first then 'metadata' hasn't been set
            # as we're not fully transactional at the DB layer. Same comment applies to below key name
            # check
            my_metadata = results_by_url[url].get('metadata', {})

            # go through all the children and recurse, but only if we have
            # in the result set. Remember results will not contain leaf nodes
            for child in results_by_url[url].get('definition', {}).get('children', []):
                if child in results_by_url:
                    new_child_metadata = copy.deepcopy(my_metadata)
                    new_child_metadata.update(results_by_url[child].get('metadata', {}))
                    results_by_url[child]['metadata'] = new_child_metadata
                    metadata_to_inherit[child] = new_child_metadata
                    _compute_inherited_metadata(child)
                else:
                    # this is likely a leaf node, so let's record what metadata we need to inherit
                    metadata_to_inherit[child] = my_metadata

        if root is not None:
            _compute_inherited_metadata(root)

        return metadata_to_inherit
Ejemplo n.º 31
0
class XModule(XModuleFields, HTMLSnippet, XBlock):
    ''' Implements a generic learning module.

        Subclasses must at a minimum provide a definition for get_html in order
        to be displayed to users.

        See the HTML module for a simple example.
    '''

    # The default implementation of get_icon_class returns the icon_class
    # attribute of the class
    #
    # This attribute can be overridden by subclasses, and
    # the function can also be overridden if the icon class depends on the data
    # in the module
    icon_class = 'other'


    def __init__(self, system, location, descriptor, model_data):
        '''
        Construct a new xmodule

        system: A ModuleSystem allowing access to external resources

        location: Something Location-like that identifies this xmodule

        descriptor: the XModuleDescriptor that this module is an instance of.
            TODO (vshnayder): remove the definition parameter and location--they
            can come from the descriptor.

        model_data: A dictionary-like object that maps field names to values
            for those fields.
        '''
        self._model_data = model_data
        self.system = system
        self.location = Location(location)
        self.descriptor = descriptor
        self.url_name = self.location.name
        self.category = self.location.category
        self._loaded_children = None

    @property
    def id(self):
        return self.location.url()

    @property
    def display_name_with_default(self):
        '''
        Return a display name for the module: use display_name if defined in
        metadata, otherwise convert the url name.
        '''
        name = self.display_name
        if name is None:
            name = self.url_name.replace('_', ' ')
        return name

    def get_children(self):
        '''
        Return module instances for all the children of this module.
        '''
        if self._loaded_children is None:
            child_descriptors = self.get_child_descriptors()
            children = [self.system.get_module(descriptor) for descriptor in child_descriptors]
            # get_module returns None if the current user doesn't have access
            # to the location.
            self._loaded_children = [c for c in children if c is not None]

        return self._loaded_children

    def __unicode__(self):
        return '<x_module(id={0})>'.format(self.id)

    def get_child_descriptors(self):
        '''
        Returns the descriptors of the child modules

        Overriding this changes the behavior of get_children and
        anything that uses get_children, such as get_display_items.

        This method will not instantiate the modules of the children
        unless absolutely necessary, so it is cheaper to call than get_children

        These children will be the same children returned by the
        descriptor unless descriptor.has_dynamic_children() is true.
        '''
        return self.descriptor.get_children()

    def get_child_by(self, selector):
        """
        Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise.
        """
        for child in self.get_children():
            if selector(child):
                return child
        return None

    def get_display_items(self):
        '''
        Returns a list of descendent module instances that will display
        immediately inside this module.
        '''
        items = []
        for child in self.get_children():
            items.extend(child.displayable_items())

        return items

    def displayable_items(self):
        '''
        Returns list of displayable modules contained by this module. If this
        module is visible, should return [self].
        '''
        return [self]

    def get_icon_class(self):
        '''
        Return a css class identifying this module in the context of an icon
        '''
        return self.icon_class

    ### Functions used in the LMS

    def get_score(self):
        """
        Score the student received on the problem, or None if there is no
        score.

        Returns:
          dictionary
             {'score': integer, from 0 to get_max_score(),
              'total': get_max_score()}

          NOTE (vshnayder): not sure if this was the intended return value, but
          that's what it's doing now.  I suspect that we really want it to just
          return a number.  Would need to change (at least) capa and
          modx_dispatch to match if we did that.
        """
        return None

    def max_score(self):
        ''' Maximum score. Two notes:

            * This is generic; in abstract, a problem could be 3/5 points on one
              randomization, and 5/7 on another

            * In practice, this is a Very Bad Idea, and (a) will break some code
              in place (although that code should get fixed), and (b) break some
              analytics we plan to put in place.
        '''
        return None

    def get_progress(self):
        ''' Return a progress.Progress object that represents how far the
        student has gone in this module.  Must be implemented to get correct
        progress tracking behavior in nesting modules like sequence and
        vertical.

        If this module has no notion of progress, return None.
        '''
        return None

    def handle_ajax(self, dispatch, get):
        ''' dispatch is last part of the URL.
            get is a dictionary-like object '''
        return ""

    # cdodge: added to support dynamic substitutions of
    # links for courseware assets (e.g. images). <link> is passed through from lxml.html parser
    def rewrite_content_links(self, link):
        # see if we start with our format, e.g. 'xasset:<filename>'
        if link.startswith(XASSET_SRCREF_PREFIX):
            # yes, then parse out the name
            name = link[len(XASSET_SRCREF_PREFIX):]
            loc = Location(self.location)
            # resolve the reference to our internal 'filepath' which
            link = StaticContent.compute_location_filename(loc.org, loc.course, name)

        return link
Ejemplo n.º 32
0
    def compute_metadata_inheritance_tree(self, location):
        """
        TODO (cdodge) This method can be deleted when the 'split module store' work has been completed
        """

        # get all collections in the course, this query should not return any leaf nodes
        # note this is a bit ugly as when we add new categories of containers, we have to add it here
        query = {
            "_id.org": location.org,
            "_id.course": location.course,
            "_id.category": {
                "$in": [
                    "course",
                    "chapter",
                    "sequential",
                    "vertical",
                    "wrapper",
                    "problemset",
                    "conditional",
                    "randomize",
                ]
            },
        }
        # we just want the Location, children, and inheritable metadata
        record_filter = {"_id": 1, "definition.children": 1}

        # just get the inheritable metadata since that is all we need for the computation
        # this minimizes both data pushed over the wire
        for attr in INHERITABLE_METADATA:
            record_filter["metadata.{0}".format(attr)] = 1

        # call out to the DB
        resultset = self.collection.find(query, record_filter)

        results_by_url = {}
        root = None

        # now go through the results and order them by the location url
        for result in resultset:
            location = Location(result["_id"])
            # We need to collate between draft and non-draft
            # i.e. draft verticals can have children which are not in non-draft versions
            location = location.replace(revision=None)
            location_url = location.url()
            if location_url in results_by_url:
                existing_children = results_by_url[location_url].get("definition", {}).get("children", [])
                additional_children = result.get("definition", {}).get("children", [])
                total_children = existing_children + additional_children
                if "definition" not in results_by_url[location_url]:
                    results_by_url[location_url]["definition"] = {}
                results_by_url[location_url]["definition"]["children"] = total_children
            results_by_url[location.url()] = result
            if location.category == "course":
                root = location.url()

        # now traverse the tree and compute down the inherited metadata
        metadata_to_inherit = {}

        def _compute_inherited_metadata(url):
            """
            Helper method for computing inherited metadata for a specific location url
            """
            # check for presence of metadata key. Note that a given module may not yet be fully formed.
            # example: update_item -> update_children -> update_metadata sequence on new item create
            # if we get called here without update_metadata called first then 'metadata' hasn't been set
            # as we're not fully transactional at the DB layer. Same comment applies to below key name
            # check
            my_metadata = results_by_url[url].get("metadata", {})

            # go through all the children and recurse, but only if we have
            # in the result set. Remember results will not contain leaf nodes
            for child in results_by_url[url].get("definition", {}).get("children", []):
                if child in results_by_url:
                    new_child_metadata = copy.deepcopy(my_metadata)
                    new_child_metadata.update(results_by_url[child].get("metadata", {}))
                    results_by_url[child]["metadata"] = new_child_metadata
                    metadata_to_inherit[child] = new_child_metadata
                    _compute_inherited_metadata(child)
                else:
                    # this is likely a leaf node, so let's record what metadata we need to inherit
                    metadata_to_inherit[child] = my_metadata

        if root is not None:
            _compute_inherited_metadata(root)

        return metadata_to_inherit
Ejemplo n.º 33
0
class RolesTestCase(TestCase):
    """
    Tests of student.roles
    """
    def setUp(self):
        self.course = Location('i4x://edX/toy/course/2012_Fall')
        self.anonymous_user = AnonymousUserFactory()
        self.student = UserFactory()
        self.global_staff = UserFactory(is_staff=True)
        self.course_staff = StaffFactory(course=self.course)
        self.course_instructor = InstructorFactory(course=self.course)

    def test_global_staff(self):
        self.assertFalse(GlobalStaff().has_user(self.student))
        self.assertFalse(GlobalStaff().has_user(self.course_staff))
        self.assertFalse(GlobalStaff().has_user(self.course_instructor))
        self.assertTrue(GlobalStaff().has_user(self.global_staff))

    def test_group_name_case_insensitive(self):
        uppercase_loc = "i4x://ORG/COURSE/course/NAME"
        lowercase_loc = uppercase_loc.lower()

        lowercase_group = "role_org/course/name"
        uppercase_group = lowercase_group.upper()

        lowercase_user = UserFactory(groups=lowercase_group)
        uppercase_user = UserFactory(groups=uppercase_group)

        self.assertTrue(
            CourseRole("role", lowercase_loc).has_user(lowercase_user))
        self.assertTrue(
            CourseRole("role", uppercase_loc).has_user(lowercase_user))
        self.assertTrue(
            CourseRole("role", lowercase_loc).has_user(uppercase_user))
        self.assertTrue(
            CourseRole("role", uppercase_loc).has_user(uppercase_user))

    def test_course_role(self):
        """
        Test that giving a user a course role enables access appropriately
        """
        course_locator = loc_mapper().translate_location(
            self.course.course_id, self.course, add_entry_if_missing=True)
        self.assertFalse(
            CourseStaffRole(course_locator).has_user(self.student),
            "Student has premature access to {}".format(
                unicode(course_locator)))
        self.assertFalse(
            CourseStaffRole(self.course).has_user(self.student),
            "Student has premature access to {}".format(self.course.url()))
        CourseStaffRole(course_locator).add_users(self.student)
        self.assertTrue(
            CourseStaffRole(course_locator).has_user(self.student),
            "Student doesn't have access to {}".format(
                unicode(course_locator)))
        self.assertTrue(
            CourseStaffRole(self.course).has_user(self.student),
            "Student doesn't have access to {}".format(
                unicode(self.course.url())))
        # now try accessing something internal to the course
        vertical_locator = BlockUsageLocator(
            package_id=course_locator.package_id,
            branch='published',
            block_id='madeup')
        vertical_location = self.course.replace(category='vertical',
                                                name='madeuptoo')
        self.assertTrue(
            CourseStaffRole(vertical_locator).has_user(self.student),
            "Student doesn't have access to {}".format(
                unicode(vertical_locator)))
        self.assertTrue(
            CourseStaffRole(vertical_location,
                            course_context=self.course.course_id).has_user(
                                self.student),
            "Student doesn't have access to {}".format(
                unicode(vertical_location.url())))
Ejemplo n.º 34
0
    def test_encode_location(self):
        loc = Location('i4x', 'org', 'course', 'category', 'name')
        self.assertEqual(loc.url(), self.encoder.default(loc))

        loc = Location('i4x', 'org', 'course', 'category', 'name', 'version')
        self.assertEqual(loc.url(), self.encoder.default(loc))
Ejemplo n.º 35
0
class XModule(XModuleFields, HTMLSnippet, XBlock):
    ''' Implements a generic learning module.

        Subclasses must at a minimum provide a definition for get_html in order
        to be displayed to users.

        See the HTML module for a simple example.
    '''

    # The default implementation of get_icon_class returns the icon_class
    # attribute of the class
    #
    # This attribute can be overridden by subclasses, and
    # the function can also be overridden if the icon class depends on the data
    # in the module
    icon_class = 'other'

    def __init__(self, system, location, descriptor, model_data):
        '''
        Construct a new xmodule

        system: A ModuleSystem allowing access to external resources

        location: Something Location-like that identifies this xmodule

        descriptor: the XModuleDescriptor that this module is an instance of.
            TODO (vshnayder): remove the definition parameter and location--they
            can come from the descriptor.

        model_data: A dictionary-like object that maps field names to values
            for those fields.
        '''
        self._model_data = model_data
        self.system = system
        self.location = Location(location)
        self.descriptor = descriptor
        self.url_name = self.location.name
        self.category = self.location.category
        self._loaded_children = None

    @property
    def id(self):
        return self.location.url()

    @property
    def display_name_with_default(self):
        '''
        Return a display name for the module: use display_name if defined in
        metadata, otherwise convert the url name.
        '''
        name = self.display_name
        if name is None:
            name = self.url_name.replace('_', ' ')
        return name

    def get_children(self):
        '''
        Return module instances for all the children of this module.
        '''
        if self._loaded_children is None:
            child_descriptors = self.get_child_descriptors()
            children = [
                self.system.get_module(descriptor)
                for descriptor in child_descriptors
            ]
            # get_module returns None if the current user doesn't have access
            # to the location.
            self._loaded_children = [c for c in children if c is not None]

        return self._loaded_children

    def __unicode__(self):
        return '<x_module(id={0})>'.format(self.id)

    def get_child_descriptors(self):
        '''
        Returns the descriptors of the child modules

        Overriding this changes the behavior of get_children and
        anything that uses get_children, such as get_display_items.

        This method will not instantiate the modules of the children
        unless absolutely necessary, so it is cheaper to call than get_children

        These children will be the same children returned by the
        descriptor unless descriptor.has_dynamic_children() is true.
        '''
        return self.descriptor.get_children()

    def get_child_by(self, selector):
        """
        Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise.
        """
        for child in self.get_children():
            if selector(child):
                return child
        return None

    def get_display_items(self):
        '''
        Returns a list of descendent module instances that will display
        immediately inside this module.
        '''
        items = []
        for child in self.get_children():
            items.extend(child.displayable_items())

        return items

    def displayable_items(self):
        '''
        Returns list of displayable modules contained by this module. If this
        module is visible, should return [self].
        '''
        return [self]

    def get_icon_class(self):
        '''
        Return a css class identifying this module in the context of an icon
        '''
        return self.icon_class

    ### Functions used in the LMS

    def get_score(self):
        """
        Score the student received on the problem, or None if there is no
        score.

        Returns:
          dictionary
             {'score': integer, from 0 to get_max_score(),
              'total': get_max_score()}

          NOTE (vshnayder): not sure if this was the intended return value, but
          that's what it's doing now.  I suspect that we really want it to just
          return a number.  Would need to change (at least) capa and
          modx_dispatch to match if we did that.
        """
        return None

    def max_score(self):
        ''' Maximum score. Two notes:

            * This is generic; in abstract, a problem could be 3/5 points on one
              randomization, and 5/7 on another

            * In practice, this is a Very Bad Idea, and (a) will break some code
              in place (although that code should get fixed), and (b) break some
              analytics we plan to put in place.
        '''
        return None

    def get_progress(self):
        ''' Return a progress.Progress object that represents how far the
        student has gone in this module.  Must be implemented to get correct
        progress tracking behavior in nesting modules like sequence and
        vertical.

        If this module has no notion of progress, return None.
        '''
        return None

    def handle_ajax(self, dispatch, get):
        ''' dispatch is last part of the URL.
            get is a dictionary-like object '''
        return ""

    # cdodge: added to support dynamic substitutions of
    # links for courseware assets (e.g. images). <link> is passed through from lxml.html parser
    def rewrite_content_links(self, link):
        # see if we start with our format, e.g. 'xasset:<filename>'
        if link.startswith(XASSET_SRCREF_PREFIX):
            # yes, then parse out the name
            name = link[len(XASSET_SRCREF_PREFIX):]
            loc = Location(self.location)
            # resolve the reference to our internal 'filepath' which
            link = StaticContent.compute_location_filename(
                loc.org, loc.course, name)

        return link
Ejemplo n.º 36
0
class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
    """
    An XModuleDescriptor is a specification for an element of a course. This
    could be a problem, an organizational element (a group of content), or a
    segment of video, for example.

    XModuleDescriptors are independent and agnostic to the current student state
    on a problem. They handle the editing interface used by instructors to
    create a problem, and can generate XModules (which do know about student
    state).
    """
    entry_point = "xmodule.v1"
    module_class = XModule

    # Attributes for inspection of the descriptor

    # This indicates whether the xmodule is a problem-type.
    # It should respond to max_score() and grade(). It can be graded or ungraded
    # (like a practice problem).
    has_score = False

    # A list of descriptor attributes that must be equal for the descriptors to
    # be equal
    equality_attributes = ('_model_data', 'location')

    # Name of resource directory to load templates from
    template_dir_name = "default"

    # Class level variable
    always_recalculate_grades = False
    """
    Return whether this descriptor always requires recalculation of grades, for
    example if the score can change via an extrnal service, not just when the
    student interacts with the module on the page.  A specific example is
    FoldIt, which posts grade-changing updates through a separate API.
    """

    # VS[compat].  Backwards compatibility code that can go away after
    # importing 2012 courses.
    # A set of metadata key conversions that we want to make
    metadata_translations = {
        'slug': 'url_name',
        'name': 'display_name',
    }

    # ============================= STRUCTURAL MANIPULATION ===================
    def __init__(self, system, location, model_data):
        """
        Construct a new XModuleDescriptor. The only required arguments are the
        system, used for interaction with external resources, and the
        definition, which specifies all the data needed to edit and display the
        problem (but none of the associated metadata that handles recordkeeping
        around the problem).

        This allows for maximal flexibility to add to the interface while
        preserving backwards compatibility.

        system: A DescriptorSystem for interacting with external resources

        location: Something Location-like that identifies this xmodule

        model_data: A dictionary-like object that maps field names to values
            for those fields.
        """
        self.system = system
        self.location = Location(location)
        self.url_name = self.location.name
        self.category = self.location.category
        self._model_data = model_data

        self._child_instances = None

    @property
    def id(self):
        return self.location.url()

    @property
    def display_name_with_default(self):
        '''
        Return a display name for the module: use display_name if defined in
        metadata, otherwise convert the url name.
        '''
        name = self.display_name
        if name is None:
            name = self.url_name.replace('_', ' ')
        return name

    def get_required_module_descriptors(self):
        """Returns a list of XModuleDescritpor instances upon which this module depends, but are
        not children of this module"""
        return []

    def get_children(self):
        """Returns a list of XModuleDescriptor instances for the children of
        this module"""
        if not self.has_children:
            return []

        if self._child_instances is None:
            self._child_instances = []
            for child_loc in self.children:
                try:
                    child = self.system.load_item(child_loc)
                except ItemNotFoundError:
                    log.exception('Unable to load item {loc}, skipping'.format(
                        loc=child_loc))
                    continue
                self._child_instances.append(child)

        return self._child_instances

    def get_child_by(self, selector):
        """
        Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise.
        """
        for child in self.get_children():
            if selector(child):
                return child
        return None

    def xmodule(self, system):
        """
        Returns an XModule.

        system: Module system
        """
        return self.module_class(
            system,
            self.location,
            self,
            system.xblock_model_data(self),
        )

    def has_dynamic_children(self):
        """
        Returns True if this descriptor has dynamic children for a given
        student when the module is created.

        Returns False if the children of this descriptor are the same
        children that the module will return for any student.
        """
        return False

    # ================================= JSON PARSING ===========================
    @staticmethod
    def load_from_json(json_data, system, default_class=None):
        """
        This method instantiates the correct subclass of XModuleDescriptor based
        on the contents of json_data.

        json_data must contain a 'location' element, and must be suitable to be
        passed into the subclasses `from_json` method as model_data
        """
        class_ = XModuleDescriptor.load_class(
            json_data['location']['category'], default_class)
        return class_.from_json(json_data, system)

    @classmethod
    def from_json(cls, json_data, system):
        """
        Creates an instance of this descriptor from the supplied json_data.
        This may be overridden by subclasses

        json_data: A json object with the keys 'definition' and 'metadata',
            definition: A json object with the keys 'data' and 'children'
                data: A json value
                children: A list of edX Location urls
            metadata: A json object with any keys

        This json_data is transformed to model_data using the following rules:
            1) The model data contains all of the fields from metadata
            2) The model data contains the 'children' array
            3) If 'definition.data' is a json object, model data contains all of its fields
               Otherwise, it contains the single field 'data'
            4) Any value later in this list overrides a value earlier in this list

        system: A DescriptorSystem for interacting with external resources
        """
        model_data = {}

        for key, value in json_data.get('metadata', {}).items():
            model_data[cls._translate(key)] = value

        model_data.update(json_data.get('metadata', {}))

        definition = json_data.get('definition', {})
        if 'children' in definition:
            model_data['children'] = definition['children']

        if 'data' in definition:
            if isinstance(definition['data'], dict):
                model_data.update(definition['data'])
            else:
                model_data['data'] = definition['data']

        return cls(system=system,
                   location=json_data['location'],
                   model_data=model_data)

    @classmethod
    def _translate(cls, key):
        'VS[compat]'
        return cls.metadata_translations.get(key, key)

    # ================================= XML PARSING ============================
    @staticmethod
    def load_from_xml(xml_data,
                      system,
                      org=None,
                      course=None,
                      default_class=None):
        """
        This method instantiates the correct subclass of XModuleDescriptor based
        on the contents of xml_data.

        xml_data must be a string containing valid xml

        system is an XMLParsingSystem

        org and course are optional strings that will be used in the generated
            module's url identifiers
        """
        class_ = XModuleDescriptor.load_class(
            etree.fromstring(xml_data).tag, default_class)
        # leave next line, commented out - useful for low-level debugging
        # log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (
        #        etree.fromstring(xml_data).tag,class_))

        return class_.from_xml(xml_data, system, org, course)

    @classmethod
    def from_xml(cls, xml_data, system, org=None, course=None):
        """
        Creates an instance of this descriptor from the supplied xml_data.
        This may be overridden by subclasses

        xml_data: A string of xml that will be translated into data and children
            for this module

        system is an XMLParsingSystem

        org and course are optional strings that will be used in the generated
            module's url identifiers
        """
        raise NotImplementedError(
            'Modules must implement from_xml to be parsable from xml')

    def export_to_xml(self, resource_fs):
        """
        Returns an xml string representing this module, and all modules
        underneath it.  May also write required resources out to resource_fs

        Assumes that modules have single parentage (that no module appears twice
        in the same course), and that it is thus safe to nest modules as xml
        children as appropriate.

        The returned XML should be able to be parsed back into an identical
        XModuleDescriptor using the from_xml method with the same system, org,
        and course
        """
        raise NotImplementedError(
            'Modules must implement export_to_xml to enable xml export')

    # =============================== Testing ==================================
    def get_sample_state(self):
        """
        Return a list of tuples of instance_state, shared_state. Each tuple
        defines a sample case for this module
        """
        return [('{}', '{}')]

    # =============================== BUILTIN METHODS ==========================
    def __eq__(self, other):
        eq = (self.__class__ == other.__class__ and all(
            getattr(self, attr, None) == getattr(other, attr, None)
            for attr in self.equality_attributes))

        return eq

    def __repr__(self):
        return ("{class_}({system!r}, location={location!r},"
                " model_data={model_data!r})".format(
                    class_=self.__class__.__name__,
                    system=self.system,
                    location=self.location,
                    model_data=self._model_data,
                ))

    @property
    def non_editable_metadata_fields(self):
        """
        Return the list of fields that should not be editable in Studio.

        When overriding, be sure to append to the superclasses' list.
        """
        # We are not allowing editing of xblock tag and name fields at this time (for any component).
        return [XBlock.tags, XBlock.name]

    @property
    def editable_metadata_fields(self):
        """
        Returns the metadata fields to be edited in Studio. These are fields with scope `Scope.settings`.

        Can be limited by extending `non_editable_metadata_fields`.
        """
        inherited_metadata = getattr(self, '_inherited_metadata', {})
        inheritable_metadata = getattr(self, '_inheritable_metadata', {})
        metadata_fields = {}
        for field in self.fields:

            if field.scope != Scope.settings or field in self.non_editable_metadata_fields:
                continue

            inheritable = False
            value = getattr(self, field.name)
            default_value = field.default
            explicitly_set = field.name in self._model_data
            if field.name in inheritable_metadata:
                inheritable = True
                default_value = field.from_json(
                    inheritable_metadata.get(field.name))
                if field.name in inherited_metadata:
                    explicitly_set = False

            # We support the following editors:
            # 1. A select editor for fields with a list of possible values (includes Booleans).
            # 2. Number editors for integers and floats.
            # 3. A generic string editor for anything else (editing JSON representation of the value).
            type = "Generic"
            values = [] if field.values is None else copy.deepcopy(
                field.values)
            if isinstance(values, tuple):
                values = list(values)
            if isinstance(values, list):
                if len(values) > 0:
                    type = "Select"
                for index, choice in enumerate(values):
                    json_choice = copy.deepcopy(choice)
                    if isinstance(json_choice,
                                  dict) and 'value' in json_choice:
                        json_choice['value'] = field.to_json(
                            json_choice['value'])
                    else:
                        json_choice = field.to_json(json_choice)
                    values[index] = json_choice
            elif isinstance(field, Integer):
                type = "Integer"
            elif isinstance(field, Float):
                type = "Float"
            metadata_fields[field.name] = {
                'field_name': field.name,
                'type': type,
                'display_name': field.display_name,
                'value': field.to_json(value),
                'options': values,
                'default_value': field.to_json(default_value),
                'inheritable': inheritable,
                'explicitly_set': explicitly_set,
                'help': field.help
            }

        return metadata_fields
Ejemplo n.º 37
0
    def compute_metadata_inheritance_tree(self, location):
        '''
        TODO (cdodge) This method can be deleted when the 'split module store' work has been completed
        '''

        # get all collections in the course, this query should not return any leaf nodes
        # note this is a bit ugly as when we add new categories of containers, we have to add it here
        query = {'_id.org': location.org,
                 '_id.course': location.course,
                 '_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical', 'videosequence',
                                          'wrapper', 'problemset', 'conditional', 'randomize']}
                 }
        # we just want the Location, children, and inheritable metadata
        record_filter = {'_id': 1, 'definition.children': 1}

        # just get the inheritable metadata since that is all we need for the computation
        # this minimizes both data pushed over the wire
        for field_name in InheritanceMixin.fields:
            record_filter['metadata.{0}'.format(field_name)] = 1

        # call out to the DB
        resultset = self.collection.find(query, record_filter)

        results_by_url = {}
        root = None

        # now go through the results and order them by the location url
        for result in resultset:
            location = Location(result['_id'])
            # We need to collate between draft and non-draft
            # i.e. draft verticals will have draft children but will have non-draft parents currently
            location = location.replace(revision=None)
            location_url = location.url()
            if location_url in results_by_url:
                existing_children = results_by_url[location_url].get('definition', {}).get('children', [])
                additional_children = result.get('definition', {}).get('children', [])
                total_children = existing_children + additional_children
                results_by_url[location_url].setdefault('definition', {})['children'] = total_children
            results_by_url[location.url()] = result
            if location.category == 'course':
                root = location.url()

        # now traverse the tree and compute down the inherited metadata
        metadata_to_inherit = {}

        def _compute_inherited_metadata(url):
            """
            Helper method for computing inherited metadata for a specific location url
            """
            # check for presence of metadata key. Note that a given module may not yet be fully formed.
            # example: update_item -> update_children -> update_metadata sequence on new item create
            # if we get called here without update_metadata called first then 'metadata' hasn't been set
            # as we're not fully transactional at the DB layer. Same comment applies to below key name
            # check
            my_metadata = results_by_url[url].get('metadata', {})

            # go through all the children and recurse, but only if we have
            # in the result set. Remember results will not contain leaf nodes
            for child in results_by_url[url].get('definition', {}).get('children', []):
                if child in results_by_url:
                    new_child_metadata = copy.deepcopy(my_metadata)
                    new_child_metadata.update(results_by_url[child].get('metadata', {}))
                    results_by_url[child]['metadata'] = new_child_metadata
                    metadata_to_inherit[child] = new_child_metadata
                    _compute_inherited_metadata(child)
                else:
                    # this is likely a leaf node, so let's record what metadata we need to inherit
                    metadata_to_inherit[child] = my_metadata

        if root is not None:
            _compute_inherited_metadata(root)

        return metadata_to_inherit
Ejemplo n.º 38
0
class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
    """
    An XModuleDescriptor is a specification for an element of a course. This
    could be a problem, an organizational element (a group of content), or a
    segment of video, for example.

    XModuleDescriptors are independent and agnostic to the current student state
    on a problem. They handle the editing interface used by instructors to
    create a problem, and can generate XModules (which do know about student
    state).
    """
    entry_point = "xmodule.v1"
    module_class = XModule

    # Attributes for inspection of the descriptor

    # Indicates whether the xmodule state should be
    # stored in a database (independent of shared state)
    stores_state = False

    # This indicates whether the xmodule is a problem-type.
    # It should respond to max_score() and grade(). It can be graded or ungraded
    # (like a practice problem).
    has_score = False

    # A list of descriptor attributes that must be equal for the descriptors to
    # be equal
    equality_attributes = ('_model_data', 'location')

    # Name of resource directory to load templates from
    template_dir_name = "default"

    # Class level variable
    always_recalculate_grades = False
    """
    Return whether this descriptor always requires recalculation of grades, for
    example if the score can change via an extrnal service, not just when the
    student interacts with the module on the page.  A specific example is
    FoldIt, which posts grade-changing updates through a separate API.
    """

    # VS[compat].  Backwards compatibility code that can go away after
    # importing 2012 courses.
    # A set of metadata key conversions that we want to make
    metadata_translations = {
        'slug': 'url_name',
        'name': 'display_name',
        }

    # ============================= STRUCTURAL MANIPULATION ===================
    def __init__(self,
                 system,
                 location,
                 model_data):
        """
        Construct a new XModuleDescriptor. The only required arguments are the
        system, used for interaction with external resources, and the
        definition, which specifies all the data needed to edit and display the
        problem (but none of the associated metadata that handles recordkeeping
        around the problem).

        This allows for maximal flexibility to add to the interface while
        preserving backwards compatibility.

        system: A DescriptorSystem for interacting with external resources

        location: Something Location-like that identifies this xmodule

        model_data: A dictionary-like object that maps field names to values
            for those fields.
        """
        self.system = system
        self.location = Location(location)
        self.url_name = self.location.name
        self.category = self.location.category
        self._model_data = model_data

        self._child_instances = None

    @property
    def id(self):
        return self.location.url()

    @property
    def display_name_with_default(self):
        '''
        Return a display name for the module: use display_name if defined in
        metadata, otherwise convert the url name.
        '''
        name = self.display_name
        if name is None:
            name = self.url_name.replace('_', ' ')
        return name

    def get_required_module_descriptors(self):
        """Returns a list of XModuleDescritpor instances upon which this module depends, but are
        not children of this module"""
        return []

    def get_children(self):
        """Returns a list of XModuleDescriptor instances for the children of
        this module"""
        if not self.has_children:
            return []

        if self._child_instances is None:
            self._child_instances = []
            for child_loc in self.children:
                try:
                    child = self.system.load_item(child_loc)
                except ItemNotFoundError:
                    log.exception('Unable to load item {loc}, skipping'.format(loc=child_loc))
                    continue
                self._child_instances.append(child)

        return self._child_instances

    def get_child_by(self, selector):
        """
        Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise.
        """
        for child in self.get_children():
            if selector(child):
                return child
        return None

    def xmodule(self, system):
        """
        Returns an XModule.

        system: Module system
        """
        return self.module_class(
            system,
            self.location,
            self,
            system.xblock_model_data(self),
        )

    def has_dynamic_children(self):
        """
        Returns True if this descriptor has dynamic children for a given
        student when the module is created.

        Returns False if the children of this descriptor are the same
        children that the module will return for any student.
        """
        return False


    # ================================= JSON PARSING ===========================
    @staticmethod
    def load_from_json(json_data, system, default_class=None):
        """
        This method instantiates the correct subclass of XModuleDescriptor based
        on the contents of json_data.

        json_data must contain a 'location' element, and must be suitable to be
        passed into the subclasses `from_json` method as model_data
        """
        class_ = XModuleDescriptor.load_class(
            json_data['location']['category'],
            default_class
        )
        return class_.from_json(json_data, system)

    @classmethod
    def from_json(cls, json_data, system):
        """
        Creates an instance of this descriptor from the supplied json_data.
        This may be overridden by subclasses

        json_data: A json object with the keys 'definition' and 'metadata',
            definition: A json object with the keys 'data' and 'children'
                data: A json value
                children: A list of edX Location urls
            metadata: A json object with any keys

        This json_data is transformed to model_data using the following rules:
            1) The model data contains all of the fields from metadata
            2) The model data contains the 'children' array
            3) If 'definition.data' is a json object, model data contains all of its fields
               Otherwise, it contains the single field 'data'
            4) Any value later in this list overrides a value earlier in this list

        system: A DescriptorSystem for interacting with external resources
        """
        model_data = {}

        for key, value in json_data.get('metadata', {}).items():
            model_data[cls._translate(key)] = value

        model_data.update(json_data.get('metadata', {}))

        definition = json_data.get('definition', {})
        if 'children' in definition:
            model_data['children'] = definition['children']

        if 'data' in definition:
            if isinstance(definition['data'], dict):
                model_data.update(definition['data'])
            else:
                model_data['data'] = definition['data']

        return cls(system=system, location=json_data['location'], model_data=model_data)

    @classmethod
    def _translate(cls, key):
        'VS[compat]'
        return cls.metadata_translations.get(key, key)

    # ================================= XML PARSING ============================
    @staticmethod
    def load_from_xml(xml_data,
            system,
            org=None,
            course=None,
            default_class=None):
        """
        This method instantiates the correct subclass of XModuleDescriptor based
        on the contents of xml_data.

        xml_data must be a string containing valid xml

        system is an XMLParsingSystem

        org and course are optional strings that will be used in the generated
            module's url identifiers
        """
        class_ = XModuleDescriptor.load_class(
            etree.fromstring(xml_data).tag,
            default_class
            )
        # leave next line, commented out - useful for low-level debugging
        # log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (
        #        etree.fromstring(xml_data).tag,class_))

        return class_.from_xml(xml_data, system, org, course)

    @classmethod
    def from_xml(cls, xml_data, system, org=None, course=None):
        """
        Creates an instance of this descriptor from the supplied xml_data.
        This may be overridden by subclasses

        xml_data: A string of xml that will be translated into data and children
            for this module

        system is an XMLParsingSystem

        org and course are optional strings that will be used in the generated
            module's url identifiers
        """
        raise NotImplementedError(
            'Modules must implement from_xml to be parsable from xml')

    def export_to_xml(self, resource_fs):
        """
        Returns an xml string representing this module, and all modules
        underneath it.  May also write required resources out to resource_fs

        Assumes that modules have single parentage (that no module appears twice
        in the same course), and that it is thus safe to nest modules as xml
        children as appropriate.

        The returned XML should be able to be parsed back into an identical
        XModuleDescriptor using the from_xml method with the same system, org,
        and course
        """
        raise NotImplementedError(
            'Modules must implement export_to_xml to enable xml export')

    # =============================== Testing ==================================
    def get_sample_state(self):
        """
        Return a list of tuples of instance_state, shared_state. Each tuple
        defines a sample case for this module
        """
        return [('{}', '{}')]

    # =============================== BUILTIN METHODS ==========================
    def __eq__(self, other):
        eq = (self.__class__ == other.__class__ and
                all(getattr(self, attr, None) == getattr(other, attr, None)
                    for attr in self.equality_attributes))

        return eq

    def __repr__(self):
        return ("{class_}({system!r}, location={location!r},"
                " model_data={model_data!r})".format(
            class_=self.__class__.__name__,
            system=self.system,
            location=self.location,
            model_data=self._model_data,
        ))

    @property
    def non_editable_metadata_fields(self):
        """
        Return the list of fields that should not be editable in Studio.

        When overriding, be sure to append to the superclasses' list.
        """
        # We are not allowing editing of xblock tag and name fields at this time (for any component).
        return [XBlock.tags, XBlock.name]

    @property
    def editable_metadata_fields(self):
        """
        Returns the metadata fields to be edited in Studio. These are fields with scope `Scope.settings`.

        Can be limited by extending `non_editable_metadata_fields`.
        """
        inherited_metadata = getattr(self, '_inherited_metadata', {})
        inheritable_metadata = getattr(self, '_inheritable_metadata', {})
        metadata = {}
        for field in self.fields:

            if field.scope != Scope.settings or field in self.non_editable_metadata_fields:
                continue

            inheritable = False
            value = getattr(self, field.name)
            default_value = field.default
            explicitly_set = field.name in self._model_data
            if field.name in inheritable_metadata:
                inheritable = True
                default_value = field.from_json(inheritable_metadata.get(field.name))
                if field.name in inherited_metadata:
                    explicitly_set = False

            metadata[field.name] = {'field': field,
                                    'value': value,
                                    'default_value': default_value,
                                    'inheritable': inheritable,
                                    'explicitly_set': explicitly_set }

        return metadata
Ejemplo n.º 39
0
    def compute_metadata_inheritance_tree(self, location):
        '''
        TODO (cdodge) This method can be deleted when the 'split module store' work has been completed
        '''
        # get all collections in the course, this query should not return any leaf nodes
        # note this is a bit ugly as when we add new categories of containers, we have to add it here

        block_types_with_children = set(name for name, class_ in XBlock.load_classes() if getattr(class_, 'has_children', False))
        query = {'_id.org': location.org,
                 '_id.course': location.course,
                 '_id.category': {'$in': list(block_types_with_children)}
                 }
        # we just want the Location, children, and inheritable metadata
        record_filter = {'_id': 1, 'definition.children': 1}

        # just get the inheritable metadata since that is all we need for the computation
        # this minimizes both data pushed over the wire
        for field_name in InheritanceMixin.fields:
            record_filter['metadata.{0}'.format(field_name)] = 1

        # call out to the DB
        resultset = self.collection.find(query, record_filter)

        results_by_url = {}
        root = None

        # now go through the results and order them by the location url
        for result in resultset:
            location = Location(result['_id'])
            # We need to collate between draft and non-draft
            # i.e. draft verticals will have draft children but will have non-draft parents currently
            location = location.replace(revision=None)
            location_url = location.url()
            if location_url in results_by_url:
                existing_children = results_by_url[location_url].get('definition', {}).get('children', [])
                additional_children = result.get('definition', {}).get('children', [])
                total_children = existing_children + additional_children
                results_by_url[location_url].setdefault('definition', {})['children'] = total_children
            results_by_url[location.url()] = result
            if location.category == 'course':
                root = location.url()

        # now traverse the tree and compute down the inherited metadata
        metadata_to_inherit = {}

        def _compute_inherited_metadata(url):
            """
            Helper method for computing inherited metadata for a specific location url
            """
            my_metadata = results_by_url[url].get('metadata', {})

            # go through all the children and recurse, but only if we have
            # in the result set. Remember results will not contain leaf nodes
            for child in results_by_url[url].get('definition', {}).get('children', []):
                if child in results_by_url:
                    new_child_metadata = copy.deepcopy(my_metadata)
                    new_child_metadata.update(results_by_url[child].get('metadata', {}))
                    results_by_url[child]['metadata'] = new_child_metadata
                    metadata_to_inherit[child] = new_child_metadata
                    _compute_inherited_metadata(child)
                else:
                    # this is likely a leaf node, so let's record what metadata we need to inherit
                    metadata_to_inherit[child] = my_metadata

        if root is not None:
            _compute_inherited_metadata(root)

        return metadata_to_inherit
Ejemplo n.º 40
0
    def compute_metadata_inheritance_tree(self, location):
        '''
        TODO (cdodge) This method can be deleted when the 'split module store' work has been completed
        '''

        # get all collections in the course, this query should not return any leaf nodes
        # note this is a bit ugly as when we add new categories of containers, we have to add it here
        query = {
            '_id.org': location.org,
            '_id.course': location.course,
            '_id.category': {
                '$in': [
                    'course', 'chapter', 'sequential', 'vertical',
                    'videosequence', 'wrapper', 'problemset', 'conditional',
                    'randomize'
                ]
            }
        }
        # we just want the Location, children, and inheritable metadata
        record_filter = {'_id': 1, 'definition.children': 1}

        # just get the inheritable metadata since that is all we need for the computation
        # this minimizes both data pushed over the wire
        for field_name in InheritanceMixin.fields:
            record_filter['metadata.{0}'.format(field_name)] = 1

        # call out to the DB
        resultset = self.collection.find(query, record_filter)

        results_by_url = {}
        root = None

        # now go through the results and order them by the location url
        for result in resultset:
            location = Location(result['_id'])
            # We need to collate between draft and non-draft
            # i.e. draft verticals will have draft children but will have non-draft parents currently
            location = location.replace(revision=None)
            location_url = location.url()
            if location_url in results_by_url:
                existing_children = results_by_url[location_url].get(
                    'definition', {}).get('children', [])
                additional_children = result.get('definition',
                                                 {}).get('children', [])
                total_children = existing_children + additional_children
                results_by_url[location_url].setdefault(
                    'definition', {})['children'] = total_children
            results_by_url[location.url()] = result
            if location.category == 'course':
                root = location.url()

        # now traverse the tree and compute down the inherited metadata
        metadata_to_inherit = {}

        def _compute_inherited_metadata(url):
            """
            Helper method for computing inherited metadata for a specific location url
            """
            my_metadata = results_by_url[url].get('metadata', {})

            # go through all the children and recurse, but only if we have
            # in the result set. Remember results will not contain leaf nodes
            for child in results_by_url[url].get('definition',
                                                 {}).get('children', []):
                if child in results_by_url:
                    new_child_metadata = copy.deepcopy(my_metadata)
                    new_child_metadata.update(results_by_url[child].get(
                        'metadata', {}))
                    results_by_url[child]['metadata'] = new_child_metadata
                    metadata_to_inherit[child] = new_child_metadata
                    _compute_inherited_metadata(child)
                else:
                    # this is likely a leaf node, so let's record what metadata we need to inherit
                    metadata_to_inherit[child] = my_metadata

        if root is not None:
            _compute_inherited_metadata(root)

        return metadata_to_inherit