예제 #1
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**")
                    def _import_module(module):
                        module.location = module.location._replace(revision='draft')
                        # make sure our parent has us in its list of children
                        # this is to make sure private only verticals show up in the list of children since
                        # they would have been filtered out from the non-draft store export
                        if module.location.category == 'vertical':
                            module.location = module.location._replace(revision=None)
                            sequential_url = module.xml_attributes['parent_sequential_url']
                            index = int(module.xml_attributes['index_in_children_list'])

                            seq_location = Location(sequential_url)

                            # IMPORTANT: Be sure to update the sequential in the NEW namespace
                            seq_location = seq_location._replace(org=target_location_namespace.org,
                                                                 course=target_location_namespace.course
                                                                 )
                            sequential = store.get_item(seq_location)

                            if module.location.url() not in sequential.children:
                                sequential.children.insert(index, module.location.url())
                                store.update_children(sequential.location, sequential.children)

                            del module.xml_attributes['parent_sequential_url']
                            del module.xml_attributes['index_in_children_list']

                        import_module(module, draft_store, course_data_path, static_content_store, allow_not_found=True)
                        for child in module.get_children():
                            _import_module(child)
예제 #3
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))
def remap_namespace(module, target_location_namespace):
    if target_location_namespace is None:
        return module

    # This looks a bit wonky as we need to also change the 'name' of the imported course to be what
    # the caller passed in
    if module.location.category != 'course':
        module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
                                                   course=target_location_namespace.course)
    else:
        module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
                                                   course=target_location_namespace.course, name=target_location_namespace.name)

    # then remap children pointers since they too will be re-namespaced
    if hasattr(module, 'children'):
        children_locs = module.children
        if children_locs is not None and children_locs != []:
            new_locs = []
            for child in children_locs:
                child_loc = Location(child)
                new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
                                                   course=target_location_namespace.course)

                new_locs.append(new_child_loc.url())

            module.children = new_locs

    return module
예제 #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)
        )
예제 #6
0
def clone_item(request):
    parent_location = Location(request.POST['parent_location'])
    template = Location(request.POST['template'])

    display_name = request.POST.get('display_name')

    if not has_access(request.user, parent_location):
        raise PermissionDenied()

    parent = get_modulestore(template).get_item(parent_location)
    dest_location = parent_location._replace(
        category=template.category, name=uuid4().hex)

    new_item = get_modulestore(template).clone_item(template, dest_location)

    # replace the display name with an optional parameter passed in from the
    # caller
    if display_name is not None:
        new_item.display_name = display_name

    get_modulestore(template).update_metadata(
        new_item.location.url(), own_metadata(new_item))

    if new_item.location.category not in DETACHED_CATEGORIES:
        get_modulestore(parent.location).update_children(
            parent_location, parent.children + [new_item.location.url()])

    return HttpResponse(json.dumps({'id': dest_location.url()}))
예제 #7
0
파일: draft.py 프로젝트: Mtax/MHST2013-14
    def _query_children_for_cache_children(self, items):
        # first get non-draft in a round-trip
        to_process_non_drafts = super(DraftModuleStore, self)._query_children_for_cache_children(items)

        to_process_dict = {}
        for non_draft in to_process_non_drafts:
            to_process_dict[Location(non_draft["_id"])] = non_draft

        # now query all draft content in another round-trip
        query = {
            '_id': {'$in': [namedtuple_to_son(as_draft(Location(item))) for item in items]}
        }
        to_process_drafts = list(self.collection.find(query))

        # now we have to go through all drafts and replace the non-draft
        # with the draft. This is because the semantics of the DraftStore is to
        # always return the draft - if available
        for draft in to_process_drafts:
            draft_loc = Location(draft["_id"])
            draft_as_non_draft_loc = draft_loc.replace(revision=None)

            # does non-draft exist in the collection
            # if so, replace it
            if draft_as_non_draft_loc in to_process_dict:
                to_process_dict[draft_as_non_draft_loc] = draft

        # convert the dict - which is used for look ups - back into a list
        queried_children = to_process_dict.values()

        return queried_children
예제 #8
0
def import_module_from_xml(modulestore, static_content_store, course_data_path, module, target_location_namespace=None, verbose=False):
    # remap module to the new namespace
    if target_location_namespace is not None:
        # This looks a bit wonky as we need to also change the 'name' of the imported course to be what
        # the caller passed in
        if module.location.category != 'course':
            module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
                                                       course=target_location_namespace.course)
        else:
            module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
                                                       course=target_location_namespace.course, name=target_location_namespace.name)

        # then remap children pointers since they too will be re-namespaced
        if module.has_children:
            children_locs = module.children
            new_locs = []
            for child in children_locs:
                child_loc = Location(child)
                new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
                                                   course=target_location_namespace.course)

                new_locs.append(new_child_loc.url())

            module.children = new_locs

    if hasattr(module, 'data'):
        modulestore.update_item(module.location, module.data)

    if module.has_children:
        modulestore.update_children(module.location, module.children)

    modulestore.update_metadata(module.location, own_metadata(module))
예제 #9
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,
        )
예제 #10
0
    def _construct(cls, system, contents, error_msg, location):
        location = Location(location)

        if error_msg is None:
            # this string is not marked for translation because we don't have
            # access to the user context, and this will only be seen by staff
            error_msg = 'Error not available'

        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,
            '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,
        )
예제 #11
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()
예제 #12
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"))
예제 #13
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,
        )
예제 #14
0
                    def _import_module(module):
                        module.location = module.location.replace(revision="draft")
                        # make sure our parent has us in its list of children
                        # this is to make sure private only verticals show up
                        # in the list of children since they would have been
                        # filtered out from the non-draft store export
                        if module.location.category == "vertical":
                            non_draft_location = module.location.replace(revision=None)
                            sequential_url = module.xml_attributes["parent_sequential_url"]
                            index = int(module.xml_attributes["index_in_children_list"])

                            seq_location = Location(sequential_url)

                            # IMPORTANT: Be sure to update the sequential
                            # in the NEW namespace
                            seq_location = seq_location.replace(
                                org=target_location_namespace.org, course=target_location_namespace.course
                            )
                            sequential = store.get_item(seq_location, depth=0)

                            if non_draft_location.url() not in sequential.children:
                                sequential.children.insert(index, non_draft_location.url())
                                store.update_item(sequential, "**replace_user**")

                        import_module(
                            module,
                            draft_store,
                            course_data_path,
                            static_content_store,
                            source_location_namespace,
                            target_location_namespace,
                            allow_not_found=True,
                        )
                        for child in module.get_children():
                            _import_module(child)
예제 #15
0
def remap_namespace(module, target_location_namespace):
    if target_location_namespace is None:
        return module

    # This looks a bit wonky as we need to also change the 'name' of the
    # imported course to be what the caller passed in
    if module.location.category != 'course':
        module.location = module.location._replace(
            tag=target_location_namespace.tag,
            org=target_location_namespace.org,
            course=target_location_namespace.course
        )
    else:
        original_location = module.location
        #
        # module is a course module
        #
        module.location = module.location._replace(
            tag=target_location_namespace.tag,
            org=target_location_namespace.org,
            course=target_location_namespace.course,
            name=target_location_namespace.name
        )
        # There is more re-namespacing work we have to do when
        # importing course modules

        # remap pdf_textbook urls
        for entry in module.pdf_textbooks:
            for chapter in entry.get('chapters', []):
                if StaticContent.is_c4x_path(chapter.get('url', '')):
                    chapter['url'] = StaticContent.renamespace_c4x_path(
                        chapter['url'], target_location_namespace
                    )

        # if there is a wiki_slug which is the same as the original location
        # (aka default value), then remap that so the wiki doesn't point to
        # the old Wiki.
        if module.wiki_slug == original_location.course:
            module.wiki_slug = target_location_namespace.course

        module.save()

    # then remap children pointers since they too will be re-namespaced
    if hasattr(module, 'children'):
        children_locs = module.children
        if children_locs is not None and children_locs != []:
            new_locs = []
            for child in children_locs:
                child_loc = Location(child)
                new_child_loc = child_loc._replace(
                    tag=target_location_namespace.tag,
                    org=target_location_namespace.org,
                    course=target_location_namespace.course
                )

                new_locs.append(new_child_loc.url())

            module.children = new_locs

    return module
예제 #16
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)
예제 #17
0
파일: item.py 프로젝트: haksel/edx-platform
def create_item(request):
    parent_location = Location(request.POST["parent_location"])
    category = request.POST["category"]

    display_name = request.POST.get("display_name")

    if not has_access(request.user, parent_location):
        raise PermissionDenied()

    parent = get_modulestore(category).get_item(parent_location)
    dest_location = parent_location.replace(category=category, name=uuid4().hex)

    # get the metadata, display_name, and definition from the request
    metadata = {}
    data = None
    template_id = request.POST.get("boilerplate")
    if template_id is not None:
        clz = XModuleDescriptor.load_class(category)
        if clz is not None:
            template = clz.get_template(template_id)
            if template is not None:
                metadata = template.get("metadata", {})
                data = template.get("data")

    if display_name is not None:
        metadata["display_name"] = display_name

    get_modulestore(category).create_and_save_xmodule(
        dest_location, definition_data=data, metadata=metadata, system=parent.system
    )

    if category not in DETACHED_CATEGORIES:
        get_modulestore(parent.location).update_children(parent_location, parent.children + [dest_location.url()])

    return JsonResponse({"id": dest_location.url()})
예제 #18
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
예제 #19
0
def create_new_course(request):

    if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff:
        raise PermissionDenied()

    # This logic is repeated in xmodule/modulestore/tests/factories.py
    # so if you change anything here, you need to also change it there.
    # TODO: write a test that creates two courses, one with the factory and
    # the other with this method, then compare them to make sure they are
    # equivalent.
    template = Location(request.POST['template'])
    org = request.POST.get('org')
    number = request.POST.get('number')
    display_name = request.POST.get('display_name')

    try:
        dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
    except InvalidLocationError as error:
        return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" +
                                        display_name + "'.\n\n" + error.message}))

    # see if the course already exists
    existing_course = None
    try:
        existing_course = modulestore('direct').get_item(dest_location)
    except ItemNotFoundError:
        pass

    if existing_course is not None:
        return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with this name.'}))

    course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None]
    courses = modulestore().get_items(course_search_location)

    if len(courses) > 0:
        return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with the same organization and course number.'}))

    new_course = modulestore('direct').clone_item(template, dest_location)

    # clone a default 'about' module as well

    about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview'])
    dest_about_location = dest_location._replace(category='about', name='overview')
    modulestore('direct').clone_item(about_template_location, dest_about_location)

    if display_name is not None:
        new_course.display_name = display_name

    # set a default start date to now
    new_course.start = datetime.datetime.now(UTC())

    initialize_course_tabs(new_course)

    create_all_course_groups(request.user, new_course.location)

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

    return HttpResponse(json.dumps({'id': new_course.location.url()}))
    def test_post_course_update(self):
        """
        Test that a user can successfully post on course updates and handouts of a course
        whose location in not in loc_mapper
        """
        # create a course via the view handler
        course_location = Location(['i4x', 'Org_1', 'Course_1', 'course', 'Run_1'])
        course_locator = loc_mapper().translate_location(
            course_location.course_id, course_location, False, True
        )
        self.client.ajax_post(
            course_locator.url_reverse('course'),
            {
                'org': course_location.org,
                'number': course_location.course,
                'display_name': 'test course',
                'run': course_location.name,
            }
        )

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

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

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

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

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

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

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

        payload = json.loads(resp.content)
        self.assertHTMLEqual(payload['data'], content)
예제 #21
0
def create_new_course(request):
    """
    Create a new course
    """
    if not is_user_in_creator_group(request.user):
        raise PermissionDenied()

    org = request.POST.get("org")
    number = request.POST.get("number")
    display_name = request.POST.get("display_name")

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

    # see if the course already exists
    existing_course = None
    try:
        existing_course = modulestore("direct").get_item(dest_location)
    except ItemNotFoundError:
        pass
    if existing_course is not None:
        return JsonResponse({"ErrMsg": "There is already a course defined with this name."})

    course_search_location = ["i4x", dest_location.org, dest_location.course, "course", None]
    courses = modulestore().get_items(course_search_location)
    if len(courses) > 0:
        return JsonResponse(
            {"ErrMsg": "There is already a course defined with the same organization and course number."}
        )

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

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

    initialize_course_tabs(new_course)

    create_all_course_groups(request.user, new_course.location)

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

    return JsonResponse({"id": new_course.location.url()})
예제 #22
0
def import_module_from_xml(modulestore, static_content_store, course_data_path, module, target_location_namespace=None, verbose=False):
    # remap module to the new namespace
    if target_location_namespace is not None:
        # This looks a bit wonky as we need to also change the 'name' of the imported course to be what
        # the caller passed in
        if module.location.category != 'course':
            module.location = module.location._replace(
                tag=target_location_namespace.tag, org=target_location_namespace.org,
                course=target_location_namespace.course)
        else:
            module.location = module.location._replace(
                tag=target_location_namespace.tag, org=target_location_namespace.org,
                course=target_location_namespace.course, name=target_location_namespace.name)

        # then remap children pointers since they too will be re-namespaced
        if module.has_children:
            children_locs = module.children
            new_locs = []
            for child in children_locs:
                child_loc = Location(child)
                new_child_loc = child_loc._replace(
                    tag=target_location_namespace.tag, org=target_location_namespace.org,
                    course=target_location_namespace.course)

                new_locs.append(new_child_loc.url())

            module.children = new_locs

    if hasattr(module, 'data'):
        # cdodge: now go through any link references to '/static/' and make sure we've imported
        # it as a StaticContent asset
        try:
            remap_dict = {}

            # use the rewrite_links as a utility means to enumerate through all links
            # in the module data. We use that to load that reference into our asset store
            # IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
            # do the rewrites natively in that code.
            # For example, what I'm seeing is <img src='foo.jpg' />   ->   <img src='bar.jpg'>
            # Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
            # no good, so we have to do this kludge
            if isinstance(module.data, str) or isinstance(module.data, unicode):   # some module 'data' fields are non strings which blows up the link traversal code
                lxml_rewrite_links(module.data, lambda link: verify_content_links(
                    module, course_data_path, static_content_store, link, remap_dict))

                for key in remap_dict.keys():
                    module.data = module.data.replace(key, remap_dict[key])

        except Exception:
            logging.exception(
                "failed to rewrite links on {0}. Continuing...".format(module.location))

        modulestore.update_item(module.location, module.data)

    if module.has_children:
        modulestore.update_children(module.location, module.children)

    modulestore.update_metadata(module.location, own_metadata(module))
예제 #23
0
    def load_item(self, location):
        """
        Return an XModule instance for the specified location
        """
        location = Location(location)
        json_data = self.module_data.get(location)
        if json_data is None:
            module = self.modulestore.get_item(location)
            if module is not None:
                # update our own cache after going to the DB to get cache miss
                self.module_data.update(module.runtime.module_data)
            return module
        else:
            # load the module and apply the inherited metadata
            try:
                category = json_data['location']['category']
                class_ = XModuleDescriptor.load_class(
                    category,
                    self.default_class
                )
                definition = json_data.get('definition', {})
                metadata = json_data.get('metadata', {})
                for old_name, new_name in getattr(class_, 'metadata_translations', {}).items():
                    if old_name in metadata:
                        metadata[new_name] = metadata[old_name]
                        del metadata[old_name]

                kvs = MongoKeyValueStore(
                    definition.get('data', {}),
                    definition.get('children', []),
                    metadata,
                )

                field_data = DbModel(kvs)
                scope_ids = ScopeIds(None, category, location, location)
                module = self.construct_xblock_from_class(class_, field_data, scope_ids)
                if self.cached_metadata is not None:
                    # parent container pointers don't differentiate between draft and non-draft
                    # so when we do the lookup, we should do so with a non-draft location
                    non_draft_loc = location.replace(revision=None)

                    # Convert the serialized fields values in self.cached_metadata
                    # to python values
                    metadata_to_inherit = self.cached_metadata.get(non_draft_loc.url(), {})
                    inherit_metadata(module, metadata_to_inherit)
                # decache any computed pending field settings
                module.save()
                return module
            except:
                log.warning("Failed to load descriptor", exc_info=True)
                return ErrorDescriptor.from_json(
                    json_data,
                    self,
                    json_data['location'],
                    error_msg=exc_info_to_str(sys.exc_info())
                )
예제 #24
0
    def load_item(self, location):
        """
        Return an XModule instance for the specified location
        """
        location = Location(location)
        json_data = self.module_data.get(location)
        if json_data is None:
            module = self.modulestore.get_item(location)
            if module is not None:
                # update our own cache after going to the DB to get cache miss
                self.module_data.update(module.system.module_data)
            return module
        else:
            # load the module and apply the inherited metadata
            try:
                category = json_data['location']['category']
                class_ = XModuleDescriptor.load_class(
                    category,
                    self.default_class
                )
                definition = json_data.get('definition', {})
                metadata = json_data.get('metadata', {})
                for old_name, new_name in class_.metadata_translations.items():
                    if old_name in metadata:
                        metadata[new_name] = metadata[old_name]
                        del metadata[old_name]

                kvs = MongoKeyValueStore(
                    definition.get('data', {}),
                    definition.get('children', []),
                    metadata,
                    location,
                    category
                )

                model_data = DbModel(kvs, class_, None, MongoUsage(self.course_id, location))
                model_data['category'] = category
                model_data['location'] = location
                module = class_(self, model_data)
                if self.cached_metadata is not None:
                    # parent container pointers don't differentiate between draft and non-draft
                    # so when we do the lookup, we should do so with a non-draft location
                    non_draft_loc = location.replace(revision=None)
                    metadata_to_inherit = self.cached_metadata.get(non_draft_loc.url(), {})
                    inherit_metadata(module, metadata_to_inherit)
                return module
            except:
                log.warning("Failed to load descriptor", exc_info=True)
                return ErrorDescriptor.from_json(
                    json_data,
                    self,
                    json_data['location'],
                    error_msg=exc_info_to_str(sys.exc_info())
                )
 def test_parse_course_id(self):
     """
     Test the parse_course_id class method
     """
     source_string = "myorg/mycourse/myrun"
     parsed = Location.parse_course_id(source_string)
     self.assertEqual(parsed["org"], "myorg")
     self.assertEqual(parsed["course"], "mycourse")
     self.assertEqual(parsed["name"], "myrun")
     with self.assertRaises(ValueError):
         Location.parse_course_id("notlegit.id/foo")
예제 #26
0
    def _infer_course_id_try(self, location):
        """
        Create, Update, Delete operations don't require a fully-specified course_id, but
        there's no complete & sound general way to compute the course_id except via the
        proper modulestore. This method attempts several sound but not complete methods.
        :param location: an old style Location
        """
        if isinstance(location, CourseLocator):
            return location.package_id
        elif isinstance(location, basestring):
            try:
                location = Location(location)
            except InvalidLocationError:
                # try to parse as a course_id
                try:
                    Location.parse_course_id(location)
                    # it's already a course_id
                    return location
                except ValueError:
                    # cannot interpret the location
                    return None

        # location is a Location at this point
        if location.category == 'course':  # easiest case
            return location.course_id
        # try finding in loc_mapper
        try:
            # see if the loc mapper knows the course id (requires double translation)
            locator = loc_mapper().translate_location_to_course_locator(None, location)
            location = loc_mapper().translate_locator_to_location(locator, get_course=True)
            return location.course_id
        except ItemNotFoundError:
            pass
        # expensive query against all location-based modulestores to look for location.
        for store in self.modulestores.itervalues():
            if isinstance(location, store.reference_type):
                try:
                    xblock = store.get_item(location)
                    course_id = self._get_course_id_from_block(xblock, store)
                    if course_id is not None:
                        return course_id
                except NotImplementedError:
                    blocks = store.get_items(location)
                    if len(blocks) == 1:
                        block = blocks[0]
                        try:
                            return block.course_id
                        except UndefinedContext:
                            pass
                except ItemNotFoundError:
                    pass
        # if we get here, it must be in a Locator based store, but we won't be able to find
        # it.
        return None
    def fetch(cls, course_location):
        """
        Fetch the course details for the given course from persistence and return a CourseDetails model.
        """
        if not isinstance(course_location, Location):
            course_location = Location(course_location)

        course = cls(course_location)

        descriptor = get_modulestore(course_location).get_item(course_location)

        course.start_date = descriptor.start
        course.end_date = descriptor.end
        course.enrollment_start = descriptor.enrollment_start
        course.enrollment_end = descriptor.enrollment_end
        course.course_image_name = descriptor.course_image
        course.course_image_asset_path = course_image_url(descriptor)

        temploc = course_location.replace(category='about', name='syllabus')
        try:
            course.syllabus = get_modulestore(temploc).get_item(temploc).data
        except ItemNotFoundError:
            pass

        temploc = temploc.replace(name='overview')
        try:
            course.overview = get_modulestore(temploc).get_item(temploc).data
        except ItemNotFoundError:
            pass


        temploc = temploc.replace(name='tags')
        try:

            course.tags = get_modulestore(temploc).get_item(temploc).data
        except ItemNotFoundError:

            pass

        temploc = temploc.replace(name='effort')
        try:
            course.effort = get_modulestore(temploc).get_item(temploc).data
        except ItemNotFoundError:
            pass

        temploc = temploc.replace(name='video')
        try:
            raw_video = get_modulestore(temploc).get_item(temploc).data
            course.intro_video = CourseDetails.parse_video_tag(raw_video)
        except ItemNotFoundError:
            pass

        return course
예제 #28
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))
예제 #29
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)
예제 #30
0
    def get_items(self, location, course_id=None, depth=0, qualifiers=None):
        """
        Returns a list of XModuleDescriptor instances for the items
        that match location. Any element of location that is None is treated
        as a wildcard that matches any value. NOTE: don't use this to look for courses
        as the course_id is required. Use get_courses.

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

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

        store = self._get_modulestore_for_courseid(course_id or getattr(location, 'package_id'))
        # translate won't work w/ missing fields so work around it
        if store.reference_type == Location:
            if not self.use_locations:
                if getattr(location, 'block_id', False):
                    location = self._incoming_reference_adaptor(store, course_id, location)
                else:
                    # get the course's location
                    location = loc_mapper().translate_locator_to_location(location, get_course=True)
                    # now remove the unknowns
                    location = location.replace(
                        category=qualifiers.get('category', None),
                        name=None
                    )
        else:
            if self.use_locations:
                if not isinstance(location, Location):
                    location = Location(location)
                try:
                    location.ensure_fully_specified()
                    location = loc_mapper().translate_location(
                        course_id, location, location.revision == 'published', True
                    )
                except InsufficientSpecificationError:
                    # construct the Locator by hand
                    if location.category is not None and qualifiers.get('category', False):
                        qualifiers['category'] = location.category
                    location = loc_mapper().translate_location_to_course_locator(
                        course_id, location, location.revision == 'published'
                    )
        xblocks = store.get_items(location, course_id, depth, qualifiers)
        xblocks = [self._outgoing_xblock_adaptor(store, course_id, xblock) for xblock in xblocks]
        return xblocks
예제 #31
0
class TestOrphan(unittest.TestCase):
    """
    Test the orphan finding code
    """

    # Snippet of what would be in the django settings envs file
    db_config = {
        'host': 'localhost',
        'db': 'test_xmodule',
    }

    modulestore_options = {
        'default_class': 'xmodule.raw_module.RawDescriptor',
        'fs_root': '',
        'render_template': mock.Mock(return_value=""),
        'xblock_mixins': (InheritanceMixin, )
    }

    split_course_id = 'test_org.test_course.runid'

    def setUp(self):
        self.db_config['collection'] = 'modulestore{0}'.format(
            uuid.uuid4().hex)

        self.userid = random.getrandbits(32)
        super(TestOrphan, self).setUp()
        self.split_mongo = SplitMongoModuleStore(self.db_config,
                                                 **self.modulestore_options)
        self.addCleanup(self.tear_down_split)
        self.old_mongo = MongoModuleStore(self.db_config,
                                          **self.modulestore_options)
        self.addCleanup(self.tear_down_mongo)
        self.course_location = None
        self._create_course()

    def tear_down_split(self):
        """
        Remove the test collections, close the db connection
        """
        split_db = self.split_mongo.db
        split_db.drop_collection(split_db.course_index)
        split_db.drop_collection(split_db.structures)
        split_db.drop_collection(split_db.definitions)
        split_db.connection.close()

    def tear_down_mongo(self):
        """
        Remove the test collections, close the db connection
        """
        split_db = self.split_mongo.db
        # old_mongo doesn't give a db attr, but all of the dbs are the same
        split_db.drop_collection(self.old_mongo.collection)

    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)

    def _create_course(self):
        """
        * some detached items
        * some attached children
        * some orphans
        """
        date_proxy = Date()
        metadata = {
            'start': date_proxy.to_json(datetime.datetime(2000, 3, 13, 4)),
            'display_name': 'Migration test course',
        }
        data = {'wiki_slug': 'test_course_slug'}
        fields = metadata.copy()
        fields.update(data)
        # split requires the course to be created separately from creating items
        self.split_mongo.create_course('test_org',
                                       'my course',
                                       self.userid,
                                       self.split_course_id,
                                       fields=fields,
                                       root_usage_id='runid')
        self.course_location = Location('i4x', 'test_org', 'test_course',
                                        'course', 'runid')
        self.old_mongo.create_and_save_xmodule(self.course_location, data,
                                               metadata)
        runtime = self.old_mongo.get_item(self.course_location).runtime

        self._create_item('chapter', 'Chapter1', {},
                          {'display_name': 'Chapter 1'}, 'course', 'runid',
                          runtime)
        self._create_item('chapter', 'Chapter2', {},
                          {'display_name': 'Chapter 2'}, 'course', 'runid',
                          runtime)
        self._create_item('chapter', 'OrphanChapter', {},
                          {'display_name': 'Orphan Chapter'}, None, None,
                          runtime)
        self._create_item('vertical', 'Vert1', {},
                          {'display_name': 'Vertical 1'}, 'chapter',
                          'Chapter1', runtime)
        self._create_item('vertical', 'OrphanVert', {},
                          {'display_name': 'Orphan Vertical'}, None, None,
                          runtime)
        self._create_item('html', 'Html1', "<p>Goodbye</p>",
                          {'display_name': 'Parented Html'}, 'vertical',
                          'Vert1', runtime)
        self._create_item('html', 'OrphanHtml', "<p>Hello</p>",
                          {'display_name': 'Orphan html'}, None, None, runtime)
        self._create_item('static_tab', 'staticuno', "<p>tab</p>",
                          {'display_name': 'Tab uno'}, None, None, runtime)
        self._create_item('about', 'overview', "<p>overview</p>", {}, None,
                          None, runtime)
        self._create_item('course_info', 'updates',
                          "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {},
                          None, None, runtime)

    def test_mongo_orphan(self):
        """
        Test that old mongo finds the orphans
        """
        orphans = self.old_mongo.get_orphans(
            self.course_location, ['static_tab', 'about', 'course_info'], None)
        self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans))
        location = self.course_location.replace(category='chapter',
                                                name='OrphanChapter')
        self.assertIn(location.url(), orphans)
        location = self.course_location.replace(category='vertical',
                                                name='OrphanVert')
        self.assertIn(location.url(), orphans)
        location = self.course_location.replace(category='html',
                                                name='OrphanHtml')
        self.assertIn(location.url(), orphans)

    def test_split_orphan(self):
        """
        Test that old mongo finds the orphans
        """
        orphans = self.split_mongo.get_orphans(
            self.split_course_id, ['static_tab', 'about', 'course_info'],
            'draft')
        self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans))
        location = BlockUsageLocator(course_id=self.split_course_id,
                                     branch='draft',
                                     usage_id='OrphanChapter')
        self.assertIn(location, orphans)
        location = BlockUsageLocator(course_id=self.split_course_id,
                                     branch='draft',
                                     usage_id='OrphanVert')
        self.assertIn(location, orphans)
        location = BlockUsageLocator(course_id=self.split_course_id,
                                     branch='draft',
                                     usage_id='OrphanHtml')
        self.assertIn(location, orphans)
예제 #32
0
 def get_about_page_link(self):
     """ create mock course and return the about page link """
     location = Location('i4x', 'mitX', '101', 'course', 'test')
     return utils.get_lms_link_for_about_page(location)
 def test_delete_block(self):
     """
     test delete_block_location_translator(location, old_course_id=None)
     """
     org = 'foo_org'
     course = 'bar_course'
     new_style_course_id = '{}.geek_dept.{}.baz_run'.format(org, course)
     loc_mapper().create_map_entry(Location('i4x', org, course, 'course',
                                            'baz_run'),
                                   new_style_course_id,
                                   block_map={
                                       'abc123': {
                                           'problem': 'problem2'
                                       },
                                       '48f23a10395384929234': {
                                           'chapter': 'chapter48f'
                                       },
                                       '1': {
                                           'chapter': 'chapter1',
                                           'problem': 'problem1'
                                       },
                                   })
     new_style_course_id2 = '{}.geek_dept.{}.delta_run'.format(org, course)
     loc_mapper().create_map_entry(Location('i4x', org, course, 'course',
                                            'delta_run'),
                                   new_style_course_id2,
                                   block_map={
                                       'abc123': {
                                           'problem': 'problem3'
                                       },
                                       '48f23a10395384929234': {
                                           'chapter': 'chapter48b'
                                       },
                                       '1': {
                                           'chapter': 'chapter2',
                                           'problem': 'problem2'
                                       },
                                   })
     location = Location('i4x', org, course, 'problem', '1')
     # delete from all courses
     loc_mapper().delete_block_location_translator(location)
     self.assertIsNone(loc_mapper().translate_locator_to_location(
         BlockUsageLocator(course_id=new_style_course_id,
                           usage_id='problem1')))
     self.assertIsNone(loc_mapper().translate_locator_to_location(
         BlockUsageLocator(course_id=new_style_course_id2,
                           usage_id='problem2')))
     # delete from one course
     location = location.replace(name='abc123')
     loc_mapper().delete_block_location_translator(
         location, '{}/{}/{}'.format(org, course, 'baz_run'))
     with self.assertRaises(ItemNotFoundError):
         loc_mapper().translate_location('{}/{}/{}'.format(
             org, course, 'baz_run'),
                                         location,
                                         add_entry_if_missing=False)
     locator = loc_mapper().translate_location('{}/{}/{}'.format(
         org, course, 'delta_run'),
                                               location,
                                               add_entry_if_missing=False)
     self.assertEqual(locator.usage_id, 'problem3')
예제 #34
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)
예제 #35
0
    def test_draft_metadata(self):
        '''
        This verifies a bug we had where inherited metadata was getting written to the
        module as 'own-metadata' when publishing. Also verifies the metadata inheritance is
        properly computed
        '''
        store = modulestore('direct')
        draft_store = modulestore('draft')
        import_from_xml(store, 'common/test/data/', ['simple'])

        course = draft_store.get_item(Location(
            ['i4x', 'edX', 'simple', 'course', '2012_Fall', None]),
                                      depth=None)
        html_module = draft_store.get_item(
            ['i4x', 'edX', 'simple', 'html', 'test_html', None])

        self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod)
        self.assertNotIn('graceperiod', own_metadata(html_module))

        draft_store.clone_item(html_module.location, html_module.location)

        # refetch to check metadata
        html_module = draft_store.get_item(
            ['i4x', 'edX', 'simple', 'html', 'test_html', None])

        self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod)
        self.assertNotIn('graceperiod', own_metadata(html_module))

        # publish module
        draft_store.publish(html_module.location, 0)

        # refetch to check metadata
        html_module = draft_store.get_item(
            ['i4x', 'edX', 'simple', 'html', 'test_html', None])

        self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod)
        self.assertNotIn('graceperiod', own_metadata(html_module))

        # put back in draft and change metadata and see if it's now marked as 'own_metadata'
        draft_store.clone_item(html_module.location, html_module.location)
        html_module = draft_store.get_item(
            ['i4x', 'edX', 'simple', 'html', 'test_html', None])

        new_graceperiod = timedelta(**{'hours': 1})

        self.assertNotIn('graceperiod', own_metadata(html_module))
        html_module.lms.graceperiod = new_graceperiod
        self.assertIn('graceperiod', own_metadata(html_module))
        self.assertEqual(html_module.lms.graceperiod, new_graceperiod)

        draft_store.update_metadata(html_module.location,
                                    own_metadata(html_module))

        # read back to make sure it reads as 'own-metadata'
        html_module = draft_store.get_item(
            ['i4x', 'edX', 'simple', 'html', 'test_html', None])

        self.assertIn('graceperiod', own_metadata(html_module))
        self.assertEqual(html_module.lms.graceperiod, new_graceperiod)

        # republish
        draft_store.publish(html_module.location, 0)

        # and re-read and verify 'own-metadata'
        draft_store.clone_item(html_module.location, html_module.location)
        html_module = draft_store.get_item(
            ['i4x', 'edX', 'simple', 'html', 'test_html', None])

        self.assertIn('graceperiod', own_metadata(html_module))
        self.assertEqual(html_module.lms.graceperiod, new_graceperiod)
예제 #36
0
class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore):
    """
    Test if student is able to reset the problem
    """
    problem_location = Location([
        "i4x", "edX", "open_ended", "combinedopenended",
        "SampleQuestion1Attempt"
    ])
    answer = "blah blah"
    assessment = [0, 1]
    hint = "blah"

    def setUp(self):
        self.test_system = get_test_system()
        self.test_system.xqueue['interface'] = Mock(send_to_queue=Mock(
            side_effect=[1, "queued"]))
        self.setup_modulestore(COURSE)

    def test_reset_fail(self):
        """
       Test the flow of the module if we complete the self assessment step and then reset
       Since the problem only allows one attempt, should fail.
       @return:
       """
        assessment = [0, 1]
        module = self.get_module_from_location(self.problem_location, COURSE)

        #Simulate a student saving an answer
        module.handle_ajax("save_answer", {"student_answer": self.answer})
        status = module.handle_ajax("get_status", {})
        self.assertTrue(isinstance(status, basestring))

        #Mock a student submitting an assessment
        assessment_dict = MockQueryDict()
        assessment_dict.update({
            'assessment': sum(assessment),
            'score_list[]': assessment
        })
        module.handle_ajax("save_assessment", assessment_dict)
        task_one_json = json.loads(module.task_states[0])
        self.assertEqual(
            json.loads(task_one_json['child_history'][0]['post_assessment']),
            assessment)
        status = module.handle_ajax("get_status", {})
        self.assertTrue(isinstance(status, basestring))

        #Move to the next step in the problem
        module.handle_ajax("next_problem", {})
        self.assertEqual(module.current_task_number, 0)

        html = module.get_html()
        self.assertTrue(isinstance(html, basestring))

        #Module should now be done
        rubric = module.handle_ajax("get_combined_rubric", {})
        self.assertTrue(isinstance(rubric, basestring))
        self.assertEqual(module.state, "done")

        #Try to reset, should fail because only 1 attempt is allowed
        reset_data = json.loads(module.handle_ajax("reset", {}))
        self.assertEqual(reset_data['success'], False)
예제 #37
0
class OpenEndedModuleTest(unittest.TestCase):
    """
    Test the open ended module class
    """
    location = Location(
        ["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"])

    metadata = json.dumps({'attempts': '10'})
    prompt = etree.XML("<prompt>This is a question prompt</prompt>")
    rubric = etree.XML('''<rubric>
        <category>
        <description>Response Quality</description>
        <option>The response is not a satisfactory answer to the question.  It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option>
        </category>
         </rubric>''')
    max_score = 4

    static_data = {
        'max_attempts': 20,
        'prompt': prompt,
        'rubric': rubric,
        'max_score': max_score,
        'display_name': 'Name',
        'accept_file_upload': False,
        'close_date': None,
        's3_interface': test_util_open_ended.S3_INTERFACE,
        'open_ended_grading_interface':
        test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
        'skip_basic_checks': False,
    }

    oeparam = etree.XML('''
      <openendedparam>
            <initial_display>Enter essay here.</initial_display>
            <answer_display>This is the answer.</answer_display>
            <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
        </openendedparam>
    ''')
    definition = {'oeparam': oeparam}
    descriptor = Mock()

    def setUp(self):
        self.test_system = get_test_system()

        self.test_system.location = self.location
        self.mock_xqueue = MagicMock()
        self.mock_xqueue.send_to_queue.return_value = (None, "Message")

        def constructed_callback(dispatch="score_update"):
            return dispatch

        self.test_system.xqueue = {
            'interface': self.mock_xqueue,
            'construct_callback': constructed_callback,
            'default_queuename': 'testqueue',
            'waittime': 1
        }
        self.openendedmodule = OpenEndedModule(self.test_system, self.location,
                                               self.definition,
                                               self.descriptor,
                                               self.static_data, self.metadata)

    def test_message_post(self):
        get = {
            'feedback': 'feedback text',
            'submission_id': '1',
            'grader_id': '1',
            'score': 3
        }
        qtime = datetime.strftime(datetime.now(UTC),
                                  xqueue_interface.dateformat)
        student_info = {
            'anonymous_student_id': self.test_system.anonymous_student_id,
            'submission_time': qtime
        }
        contents = {
            'feedback': get['feedback'],
            'submission_id': int(get['submission_id']),
            'grader_id': int(get['grader_id']),
            'score': get['score'],
            'student_info': json.dumps(student_info)
        }

        result = self.openendedmodule.message_post(get, self.test_system)
        self.assertTrue(result['success'])
        # make sure it's actually sending something we want to the queue
        self.mock_xqueue.send_to_queue.assert_called_with(
            body=json.dumps(contents), header=ANY)

        state = json.loads(self.openendedmodule.get_instance_state())
        self.assertIsNotNone(state['child_state'], OpenEndedModule.DONE)

    def test_send_to_grader(self):
        submission = "This is a student submission"
        qtime = datetime.strftime(datetime.now(UTC),
                                  xqueue_interface.dateformat)
        student_info = {
            'anonymous_student_id': self.test_system.anonymous_student_id,
            'submission_time': qtime
        }
        contents = self.openendedmodule.payload.copy()
        contents.update({
            'student_info': json.dumps(student_info),
            'student_response': submission,
            'max_score': self.max_score
        })
        result = self.openendedmodule.send_to_grader(submission,
                                                     self.test_system)
        self.assertTrue(result)
        self.mock_xqueue.send_to_queue.assert_called_with(
            body=json.dumps(contents), header=ANY)

    def update_score_single(self):
        self.openendedmodule.new_history_entry("New Entry")
        score_msg = {
            'correct': True,
            'score': 4,
            'msg': 'Grader Message',
            'feedback': "Grader Feedback"
        }
        get = {'queuekey': "abcd", 'xqueue_body': score_msg}
        self.openendedmodule.update_score(get, self.test_system)

    def update_score_single(self):
        self.openendedmodule.new_history_entry("New Entry")
        feedback = {"success": True, "feedback": "Grader Feedback"}
        score_msg = {
            'correct': True,
            'score': 4,
            'msg': 'Grader Message',
            'feedback': json.dumps(feedback),
            'grader_type': 'IN',
            'grader_id': '1',
            'submission_id': '1',
            'success': True,
            'rubric_scores': [0],
            'rubric_scores_complete': True,
            'rubric_xml': etree.tostring(self.rubric)
        }
        get = {'queuekey': "abcd", 'xqueue_body': json.dumps(score_msg)}
        self.openendedmodule.update_score(get, self.test_system)

    def test_latest_post_assessment(self):
        self.update_score_single()
        assessment = self.openendedmodule.latest_post_assessment(
            self.test_system)
        self.assertFalse(assessment == '')
        # check for errors
        self.assertFalse('errors' in assessment)

    def test_update_score(self):
        self.update_score_single()
        score = self.openendedmodule.latest_score()
        self.assertEqual(score, 4)
예제 #38
0
class CombinedOpenEndedModuleTest(unittest.TestCase):
    """
    Unit tests for the combined open ended xmodule
    """
    location = Location(
        ["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"])
    definition_template = """
                    <combinedopenended attempts="10000">
                    {rubric}
                    {prompt}
                    <task>
                    {task1}
                    </task>
                    <task>
                    {task2}
                    </task>
                    </combinedopenended>
                    """
    prompt = "<prompt>This is a question prompt</prompt>"
    rubric = '''<rubric><rubric>
        <category>
        <description>Response Quality</description>
        <option>The response is not a satisfactory answer to the question.  It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option>
        <option>Second option</option>
        </category>
         </rubric></rubric>'''
    max_score = 1

    metadata = {'attempts': '10', 'max_score': max_score}

    static_data = {
        'max_attempts': 20,
        'prompt': prompt,
        'rubric': rubric,
        'max_score': max_score,
        'display_name': 'Name',
        'accept_file_upload': False,
        'close_date': "",
        's3_interface': test_util_open_ended.S3_INTERFACE,
        'open_ended_grading_interface':
        test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
        'skip_basic_checks': False,
        'graded': True,
    }

    oeparam = etree.XML('''
      <openendedparam>
            <initial_display>Enter essay here.</initial_display>
            <answer_display>This is the answer.</answer_display>
            <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
        </openendedparam>
    ''')

    task_xml1 = '''
                <selfassessment>
                    <hintprompt>
                        What hint about this problem would you give to someone?
                    </hintprompt>
                    <submitmessage>
                        Save Succcesful.  Thanks for participating!
                    </submitmessage>
                </selfassessment>
            '''
    task_xml2 = '''
    <openended min_score_to_attempt="1" max_score_to_attempt="1">
            <openendedparam>
                    <initial_display>Enter essay here.</initial_display>
                    <answer_display>This is the answer.</answer_display>
                    <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
           </openendedparam>
    </openended>'''
    definition = {
        'prompt': etree.XML(prompt),
        'rubric': etree.XML(rubric),
        'task_xml': [task_xml1, task_xml2]
    }
    full_definition = definition_template.format(prompt=prompt,
                                                 rubric=rubric,
                                                 task1=task_xml1,
                                                 task2=task_xml2)
    descriptor = Mock(data=full_definition)
    test_system = get_test_system()
    test_system.open_ended_grading_interface = None
    combinedoe_container = CombinedOpenEndedModule(
        descriptor=descriptor,
        runtime=test_system,
        field_data=DictFieldData({
            'data': full_definition,
            'weight': '1',
        }),
        scope_ids=ScopeIds(None, None, None, None),
    )

    def setUp(self):
        self.combinedoe = CombinedOpenEndedV1Module(
            self.test_system,
            self.location,
            self.definition,
            self.descriptor,
            static_data=self.static_data,
            metadata=self.metadata,
            instance_state=self.static_data)

    def test_get_tag_name(self):
        """
        Test to see if the xml tag name is correct
        """
        name = self.combinedoe.get_tag_name("<t>Tag</t>")
        self.assertEqual(name, "t")

    def test_get_last_response(self):
        """
        See if we can parse the last response
        """
        response_dict = self.combinedoe.get_last_response(0)
        self.assertEqual(response_dict['type'], "selfassessment")
        self.assertEqual(response_dict['max_score'], self.max_score)
        self.assertEqual(response_dict['state'],
                         CombinedOpenEndedV1Module.INITIAL)

    def test_update_task_states(self):
        """
        See if we can update the task states properly
        """
        changed = self.combinedoe.update_task_states()
        self.assertFalse(changed)

        current_task = self.combinedoe.current_task
        current_task.change_state(CombinedOpenEndedV1Module.DONE)
        changed = self.combinedoe.update_task_states()

        self.assertTrue(changed)

    def test_get_max_score(self):
        """
        Try to get the max score of the problem
        """
        self.combinedoe.update_task_states()
        self.combinedoe.state = "done"
        self.combinedoe.is_scored = True
        max_score = self.combinedoe.max_score()
        self.assertEqual(max_score, 1)

    def test_container_get_max_score(self):
        """
        See if we can get the max score from the actual xmodule
        """
        #The progress view requires that this function be exposed
        max_score = self.combinedoe_container.max_score()
        self.assertEqual(max_score, None)

    def test_container_get_progress(self):
        """
        See if we can get the progress from the actual xmodule
        """
        progress = self.combinedoe_container.max_score()
        self.assertEqual(progress, None)

    def test_get_progress(self):
        """
        Test if we can get the correct progress from the combined open ended class
        """
        self.combinedoe.update_task_states()
        self.combinedoe.state = "done"
        self.combinedoe.is_scored = True
        progress = self.combinedoe.get_progress()
        self.assertIsInstance(progress, Progress)

        # progress._a is the score of the xmodule, which is 0 right now.
        self.assertEqual(progress._a, 0)

        # progress._b is the max_score (which is 1), divided by the weight (which is 1).
        self.assertEqual(progress._b, 1)

    def test_container_weight(self):
        """
        Check the problem weight in the container
        """
        weight = self.combinedoe_container.weight
        self.assertEqual(weight, 1)

    def test_container_child_weight(self):
        """
        Test the class to see if it picks up the right weight
        """
        weight = self.combinedoe_container.child_module.weight
        self.assertEqual(weight, 1)

    def test_get_score(self):
        """
        See if scoring works
        """
        score_dict = self.combinedoe.get_score()
        self.assertEqual(score_dict['score'], 0)
        self.assertEqual(score_dict['total'], 1)

    def test_alternate_orderings(self):
        """
        Try multiple ordering of definitions to see if the problem renders different steps correctly.
        """
        t1 = self.task_xml1
        t2 = self.task_xml2
        xml_to_test = [[t1], [t2], [t1, t1], [t1, t2], [t2, t2], [t2, t1],
                       [t1, t2, t1]]
        for xml in xml_to_test:
            definition = {
                'prompt': etree.XML(self.prompt),
                'rubric': etree.XML(self.rubric),
                'task_xml': xml
            }
            descriptor = Mock(data=definition)
            combinedoe = CombinedOpenEndedV1Module(
                self.test_system,
                self.location,
                definition,
                descriptor,
                static_data=self.static_data,
                metadata=self.metadata,
                instance_state=self.static_data)

            changed = combinedoe.update_task_states()
            self.assertFalse(changed)

            combinedoe = CombinedOpenEndedV1Module(
                self.test_system,
                self.location,
                definition,
                descriptor,
                static_data=self.static_data,
                metadata=self.metadata,
                instance_state={'task_states': TEST_STATE_SA})

            combinedoe = CombinedOpenEndedV1Module(
                self.test_system,
                self.location,
                definition,
                descriptor,
                static_data=self.static_data,
                metadata=self.metadata,
                instance_state={'task_states': TEST_STATE_SA_IN})

    def test_get_score_realistic(self):
        """
        Try to parse the correct score from a json instance state
        """
        instance_state = json.loads(MOCK_INSTANCE_STATE)
        rubric = """
        <rubric>
            <rubric>
                <category>
                    <description>Response Quality</description>
                    <option>The response is not a satisfactory answer to the question.  It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option>
                    <option>The response is a marginal answer to the question.  It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option>
                    <option>The response is a proficient answer to the question.  It is generally correct, although it may contain minor inaccuracies.  There is limited evidence of higher-order thinking.</option>
                    <option>The response is correct, complete, and contains evidence of higher-order thinking.</option>
                </category>
            </rubric>
        </rubric>
        """
        definition = {
            'prompt': etree.XML(self.prompt),
            'rubric': etree.XML(rubric),
            'task_xml': [self.task_xml1, self.task_xml2]
        }
        descriptor = Mock(data=definition)
        combinedoe = CombinedOpenEndedV1Module(self.test_system,
                                               self.location,
                                               definition,
                                               descriptor,
                                               static_data=self.static_data,
                                               metadata=self.metadata,
                                               instance_state=instance_state)
        score_dict = combinedoe.get_score()
        self.assertEqual(score_dict['score'], 15.0)
        self.assertEqual(score_dict['total'], 15.0)

    def generate_oe_module(self, task_state, task_number, task_xml):
        """
        Return a combined open ended module with the specified parameters
        """
        definition = {
            'prompt': etree.XML(self.prompt),
            'rubric': etree.XML(self.rubric),
            'task_xml': task_xml
        }
        descriptor = Mock(data=definition)
        instance_state = {'task_states': task_state, 'graded': True}
        if task_number is not None:
            instance_state.update({'current_task_number': task_number})
        combinedoe = CombinedOpenEndedV1Module(self.test_system,
                                               self.location,
                                               definition,
                                               descriptor,
                                               static_data=self.static_data,
                                               metadata=self.metadata,
                                               instance_state=instance_state)
        return combinedoe

    def ai_state_reset(self, task_state, task_number=None):
        """
        See if state is properly reset
        """
        combinedoe = self.generate_oe_module(task_state, task_number,
                                             [self.task_xml2])
        html = combinedoe.get_html()
        self.assertIsInstance(html, basestring)

        score = combinedoe.get_score()
        if combinedoe.is_scored:
            self.assertEqual(score['score'], 0)
        else:
            self.assertEqual(score['score'], None)

    def ai_state_success(self,
                         task_state,
                         task_number=None,
                         iscore=2,
                         tasks=None):
        """
        See if state stays the same
        """
        if tasks is None:
            tasks = [self.task_xml1, self.task_xml2]
        combinedoe = self.generate_oe_module(task_state, task_number, tasks)
        html = combinedoe.get_html()
        self.assertIsInstance(html, basestring)
        score = combinedoe.get_score()
        self.assertEqual(int(score['score']), iscore)

    def test_ai_state_reset(self):
        self.ai_state_reset(TEST_STATE_AI)

    def test_ai_state2_reset(self):
        self.ai_state_reset(TEST_STATE_AI2)

    def test_ai_invalid_state(self):
        self.ai_state_reset(TEST_STATE_AI2_INVALID)

    def test_ai_state_rest_task_number(self):
        self.ai_state_reset(TEST_STATE_AI, task_number=2)
        self.ai_state_reset(TEST_STATE_AI, task_number=5)
        self.ai_state_reset(TEST_STATE_AI, task_number=1)
        self.ai_state_reset(TEST_STATE_AI, task_number=0)

    def test_ai_state_success(self):
        self.ai_state_success(TEST_STATE_AI)

    def test_state_single(self):
        self.ai_state_success(TEST_STATE_SINGLE, iscore=12)

    def test_state_pe_single(self):
        self.ai_state_success(TEST_STATE_PE_SINGLE,
                              iscore=0,
                              tasks=[self.task_xml2])
예제 #39
0
class OpenEndedChildTest(unittest.TestCase):
    """
    Test the open ended child class
    """
    location = Location(
        ["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"])

    metadata = json.dumps({'attempts': '10'})
    prompt = etree.XML("<prompt>This is a question prompt</prompt>")
    rubric = '''<rubric><rubric>
        <category>
        <description>Response Quality</description>
        <option>The response is not a satisfactory answer to the question.  It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option>
        <option>Second option</option>
        </category>
         </rubric></rubric>'''
    max_score = 1

    static_data = {
        'max_attempts': 20,
        'prompt': prompt,
        'rubric': rubric,
        'max_score': max_score,
        'display_name': 'Name',
        'accept_file_upload': False,
        'close_date': None,
        's3_interface': "",
        'open_ended_grading_interface': {},
        'skip_basic_checks': False,
        'control': {
            'required_peer_grading': 1,
            'peer_grader_count': 1,
            'min_to_calibrate': 3,
            'max_to_calibrate': 6,
            'peer_grade_finished_submissions_when_none_pending': False,
        }
    }
    definition = Mock()
    descriptor = Mock()

    def setUp(self):
        self.test_system = get_test_system()
        self.test_system.open_ended_grading_interface = None
        self.openendedchild = OpenEndedChild(self.test_system, self.location,
                                             self.definition, self.descriptor,
                                             self.static_data, self.metadata)

    def test_latest_answer_empty(self):
        answer = self.openendedchild.latest_answer()
        self.assertEqual(answer, "")

    def test_latest_score_empty(self):
        answer = self.openendedchild.latest_score()
        self.assertEqual(answer, None)

    def test_latest_post_assessment_empty(self):
        answer = self.openendedchild.latest_post_assessment(self.test_system)
        self.assertEqual(answer, "")

    def test_new_history_entry(self):
        new_answer = "New Answer"
        self.openendedchild.new_history_entry(new_answer)
        answer = self.openendedchild.latest_answer()
        self.assertEqual(answer, new_answer)

        new_answer = "Newer Answer"
        self.openendedchild.new_history_entry(new_answer)
        answer = self.openendedchild.latest_answer()
        self.assertEqual(new_answer, answer)

    def test_record_latest_score(self):
        new_answer = "New Answer"
        self.openendedchild.new_history_entry(new_answer)
        new_score = 3
        self.openendedchild.record_latest_score(new_score)
        score = self.openendedchild.latest_score()
        self.assertEqual(score, 3)

        new_score = 4
        self.openendedchild.new_history_entry(new_answer)
        self.openendedchild.record_latest_score(new_score)
        score = self.openendedchild.latest_score()
        self.assertEqual(score, 4)

    def test_record_latest_post_assessment(self):
        new_answer = "New Answer"
        self.openendedchild.new_history_entry(new_answer)

        post_assessment = "Post assessment"
        self.openendedchild.record_latest_post_assessment(post_assessment)
        self.assertEqual(
            post_assessment,
            self.openendedchild.latest_post_assessment(self.test_system))

    def test_get_score(self):
        new_answer = "New Answer"
        self.openendedchild.new_history_entry(new_answer)

        score = self.openendedchild.get_score()
        self.assertEqual(score['score'], 0)
        self.assertEqual(score['total'], self.static_data['max_score'])

        new_score = 4
        self.openendedchild.new_history_entry(new_answer)
        self.openendedchild.record_latest_score(new_score)
        score = self.openendedchild.get_score()
        self.assertEqual(score['score'], new_score)
        self.assertEqual(score['total'], self.static_data['max_score'])

    def test_reset(self):
        self.openendedchild.reset(self.test_system)
        state = json.loads(self.openendedchild.get_instance_state())
        self.assertEqual(state['child_state'], OpenEndedChild.INITIAL)

    def test_is_last_response_correct(self):
        new_answer = "New Answer"
        self.openendedchild.new_history_entry(new_answer)
        self.openendedchild.record_latest_score(self.static_data['max_score'])
        self.assertEqual(self.openendedchild.is_last_response_correct(),
                         'correct')

        self.openendedchild.new_history_entry(new_answer)
        self.openendedchild.record_latest_score(0)
        self.assertEqual(self.openendedchild.is_last_response_correct(),
                         'incorrect')
예제 #40
0
class SelfAssessmentTest(unittest.TestCase):
    rubric = '''<rubric><rubric>
        <category>
        <description>Response Quality</description>
        <option>The response is not a satisfactory answer to the question.  It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option>
        </category>
         </rubric></rubric>'''

    prompt = etree.XML("<prompt>This is sample prompt text.</prompt>")
    definition = {
        'rubric': rubric,
        'prompt': prompt,
        'submitmessage': 'Shall we submit now?',
        'hintprompt': 'Consider this...',
    }

    location = Location(
        ["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"])

    descriptor = Mock()

    def setUp(self):
        state = json.dumps({
            'student_answers': ["Answer 1", "answer 2", "answer 3"],
            'scores': [0, 1],
            'hints': ['o hai'],
            'state':
            SelfAssessmentModule.INITIAL,
            'attempts':
            2
        })

        static_data = {
            'max_attempts': 10,
            'rubric': etree.XML(self.rubric),
            'prompt': self.prompt,
            'max_score': 1,
            'display_name': "Name",
            'accept_file_upload': False,
            'close_date': None,
            's3_interface': test_util_open_ended.S3_INTERFACE,
            'open_ended_grading_interface':
            test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
            'skip_basic_checks': False,
            'control': {
                'required_peer_grading': 1,
                'peer_grader_count': 1,
                'min_to_calibrate': 3,
                'max_to_calibrate': 6,
            }
        }

        self.module = SelfAssessmentModule(get_test_system(), self.location,
                                           self.definition, self.descriptor,
                                           static_data)

    def test_get_html(self):
        html = self.module.get_html(self.module.system)
        self.assertTrue("This is sample prompt text" in html)

    def test_self_assessment_flow(self):
        responses = {'assessment': '0', 'score_list[]': ['0', '0']}

        def get_fake_item(name):
            return responses[name]

        def get_data_for_location(self, location, student):
            return {
                'count_graded': 0,
                'count_required': 0,
                'student_sub_count': 0,
            }

        mock_query_dict = MagicMock()
        mock_query_dict.__getitem__.side_effect = get_fake_item
        mock_query_dict.getlist = get_fake_item

        self.module.peer_gs.get_data_for_location = get_data_for_location

        self.assertEqual(self.module.get_score()['score'], 0)

        self.module.save_answer({'student_answer': "I am an answer"},
                                self.module.system)
        self.assertEqual(self.module.child_state, self.module.ASSESSING)

        self.module.save_assessment(mock_query_dict, self.module.system)
        self.assertEqual(self.module.child_state, self.module.DONE)

        d = self.module.reset({})
        self.assertTrue(d['success'])
        self.assertEqual(self.module.child_state, self.module.INITIAL)

        # if we now assess as right, skip the REQUEST_HINT state
        self.module.save_answer({'student_answer': 'answer 4'},
                                self.module.system)
        responses['assessment'] = '1'
        self.module.save_assessment(mock_query_dict, self.module.system)
        self.assertEqual(self.module.child_state, self.module.DONE)
예제 #41
0
class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
    """
    Test the student flow in the combined open ended xmodule
    """
    problem_location = Location(
        ["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"])
    answer = "blah blah"
    assessment = [0, 1]
    hint = "blah"

    def setUp(self):
        self.test_system = get_test_system()
        self.test_system.open_ended_grading_interface = None
        self.test_system.xqueue['interface'] = Mock(send_to_queue=Mock(
            side_effect=[1, "queued"]))
        self.setup_modulestore(COURSE)

    def test_open_ended_load_and_save(self):
        """
        See if we can load the module and save an answer
        @return:
        """
        # Load the module
        module = self.get_module_from_location(self.problem_location, COURSE)

        # Try saving an answer
        module.handle_ajax("save_answer", {"student_answer": self.answer})
        # Save our modifications to the underlying KeyValueStore so they can be persisted
        module.save()
        task_one_json = json.loads(module.task_states[0])
        self.assertEqual(task_one_json['child_history'][0]['answer'],
                         self.answer)

        module = self.get_module_from_location(self.problem_location, COURSE)
        task_one_json = json.loads(module.task_states[0])
        self.assertEqual(task_one_json['child_history'][0]['answer'],
                         self.answer)

    def test_open_ended_flow_reset(self):
        """
        Test the flow of the module if we complete the self assessment step and then reset
        @return:
        """
        assessment = [0, 1]
        module = self.get_module_from_location(self.problem_location, COURSE)

        #Simulate a student saving an answer
        html = module.handle_ajax("get_html", {})
        module.handle_ajax(
            "save_answer", {
                "student_answer": self.answer,
                "can_upload_files": False,
                "student_file": None
            })
        html = module.handle_ajax("get_html", {})

        #Mock a student submitting an assessment
        assessment_dict = MockQueryDict()
        assessment_dict.update({
            'assessment': sum(assessment),
            'score_list[]': assessment
        })
        module.handle_ajax("save_assessment", assessment_dict)
        task_one_json = json.loads(module.task_states[0])
        self.assertEqual(
            json.loads(task_one_json['child_history'][0]['post_assessment']),
            assessment)
        rubric = module.handle_ajax("get_combined_rubric", {})

        #Move to the next step in the problem
        module.handle_ajax("next_problem", {})
        self.assertEqual(module.current_task_number, 0)

        html = module.get_html()
        self.assertTrue(isinstance(html, basestring))

        rubric = module.handle_ajax("get_combined_rubric", {})
        self.assertTrue(isinstance(rubric, basestring))
        self.assertEqual(module.state, "assessing")
        module.handle_ajax("reset", {})
        self.assertEqual(module.current_task_number, 0)

    def test_open_ended_flow_correct(self):
        """
        Test a two step problem where the student first goes through the self assessment step, and then the
        open ended step.
        @return:
        """
        assessment = [1, 1]
        #Load the module
        module = self.get_module_from_location(self.problem_location, COURSE)

        #Simulate a student saving an answer
        module.handle_ajax("save_answer", {"student_answer": self.answer})
        status = module.handle_ajax("get_status", {})
        self.assertTrue(isinstance(status, basestring))

        #Mock a student submitting an assessment
        assessment_dict = MockQueryDict()
        assessment_dict.update({
            'assessment': sum(assessment),
            'score_list[]': assessment
        })
        module.handle_ajax("save_assessment", assessment_dict)
        task_one_json = json.loads(module.task_states[0])
        self.assertEqual(
            json.loads(task_one_json['child_history'][0]['post_assessment']),
            assessment)

        #Move to the next step in the problem
        try:
            module.handle_ajax("next_problem", {})
        except GradingServiceError:
            #This error is okay.  We don't have a grading service to connect to!
            pass
        self.assertEqual(module.current_task_number, 1)
        try:
            module.get_html()
        except GradingServiceError:
            #This error is okay.  We don't have a grading service to connect to!
            pass

        #Try to get the rubric from the module
        module.handle_ajax("get_combined_rubric", {})

        #Make a fake reply from the queue
        queue_reply = {
            'queuekey':
            "",
            'xqueue_body':
            json.dumps({
                'score':
                0,
                'feedback':
                json.dumps({
                    "spelling":
                    "Spelling: Ok.",
                    "grammar":
                    "Grammar: Ok.",
                    "markup-text":
                    " all of us can think of a book that we hope none of our children or any other children have taken off the shelf . but if i have the right to remove that book from the shelf that work i abhor then you also have exactly the same right and so does everyone else . and then we <bg>have no books left</bg> on the shelf for any of us . <bs>katherine</bs> <bs>paterson</bs> , author write a persuasive essay to a newspaper reflecting your vies on censorship <bg>in libraries . do</bg> you believe that certain materials , such as books , music , movies , magazines , <bg>etc . , should be</bg> removed from the shelves if they are found <bg>offensive ? support your</bg> position with convincing arguments from your own experience , observations <bg>, and or reading .</bg> "
                }),
                'grader_type':
                "ML",
                'success':
                True,
                'grader_id':
                1,
                'submission_id':
                1,
                'rubric_xml':
                "<rubric><category><description>Writing Applications</description><score>0</score><option points='0'> The essay loses focus, has little information or supporting details, and the organization makes it difficult to follow.</option><option points='1'> The essay presents a mostly unified theme, includes sufficient information to convey the theme, and is generally organized well.</option></category><category><description> Language Conventions </description><score>0</score><option points='0'> The essay demonstrates a reasonable command of proper spelling and grammar. </option><option points='1'> The essay demonstrates superior command of proper spelling and grammar.</option></category></rubric>",
                'rubric_scores_complete':
                True,
            })
        }

        module.handle_ajax("check_for_score", {})

        #Update the module with the fake queue reply
        module.handle_ajax("score_update", queue_reply)
        self.assertFalse(module.ready_to_reset)
        self.assertEqual(module.current_task_number, 1)

        #Get html and other data client will request
        module.get_html()

        module.handle_ajax("skip_post_assessment", {})

        #Get all results
        module.handle_ajax("get_combined_rubric", {})

        #reset the problem
        module.handle_ajax("reset", {})
        self.assertEqual(module.state, "initial")
    def test_translate_locator(self):
        """
        tests translate_locator_to_location(BlockUsageLocator)
        """
        # lookup for non-existent course
        org = 'foo_org'
        course = 'bar_course'
        new_style_course_id = '{}.geek_dept.{}.baz_run'.format(org, course)
        prob_locator = BlockUsageLocator(course_id=new_style_course_id,
                                         usage_id='problem2')
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator)
        self.assertIsNone(prob_location, 'found entry in empty map table')

        loc_mapper().create_map_entry(Location('i4x', org, course, 'course',
                                               'baz_run'),
                                      new_style_course_id,
                                      block_map={
                                          'abc123': {
                                              'problem': 'problem2'
                                          },
                                          '48f23a10395384929234': {
                                              'chapter': 'chapter48f'
                                          }
                                      })
        # only one course matches
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator)
        # default branch
        self.assertEqual(
            prob_location,
            Location('i4x', org, course, 'problem', 'abc123', None))
        # explicit branch
        prob_locator = BlockUsageLocator(prob_locator, branch='draft')
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator)
        self.assertEqual(
            prob_location,
            Location('i4x', org, course, 'problem', 'abc123', 'draft'))
        prob_locator = BlockUsageLocator(course_id=new_style_course_id,
                                         usage_id='problem2',
                                         branch='production')
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator)
        self.assertEqual(
            prob_location,
            Location('i4x', org, course, 'problem', 'abc123', None))
        # same for chapter except chapter cannot be draft in old system
        chap_locator = BlockUsageLocator(course_id=new_style_course_id,
                                         usage_id='chapter48f')
        chap_location = loc_mapper().translate_locator_to_location(
            chap_locator)
        self.assertEqual(
            chap_location,
            Location('i4x', org, course, 'chapter', '48f23a10395384929234'))
        # explicit branch
        chap_locator = BlockUsageLocator(chap_locator, branch='draft')
        chap_location = loc_mapper().translate_locator_to_location(
            chap_locator)
        self.assertEqual(
            chap_location,
            Location('i4x', org, course, 'chapter', '48f23a10395384929234'))
        chap_locator = BlockUsageLocator(course_id=new_style_course_id,
                                         usage_id='chapter48f',
                                         branch='production')
        chap_location = loc_mapper().translate_locator_to_location(
            chap_locator)
        self.assertEqual(
            chap_location,
            Location('i4x', org, course, 'chapter', '48f23a10395384929234'))

        # look for non-existent problem
        prob_locator2 = BlockUsageLocator(course_id=new_style_course_id,
                                          branch='draft',
                                          usage_id='problem3')
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator2)
        self.assertIsNone(prob_location, 'Found non-existent problem')

        # add a distractor course
        new_style_course_id = '{}.geek_dept.{}.{}'.format(
            org, course, 'delta_run')
        loc_mapper().create_map_entry(
            Location('i4x', org, course, 'course', 'delta_run'),
            new_style_course_id,
            block_map={'abc123': {
                'problem': 'problem3'
            }})
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator)
        self.assertEqual(
            prob_location,
            Location('i4x', org, course, 'problem', 'abc123', None))

        # add a default course pointing to the delta_run
        loc_mapper().create_map_entry(
            Location('i4x', org, course, 'problem', '789abc123efg456'),
            new_style_course_id,
            block_map={'abc123': {
                'problem': 'problem3'
            }})
        # now query delta (2 entries point to it)
        prob_locator = BlockUsageLocator(course_id=new_style_course_id,
                                         branch='production',
                                         usage_id='problem3')
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator)
        self.assertEqual(prob_location,
                         Location('i4x', org, course, 'problem', 'abc123'))
    def test_add_block(self):
        """
        Test add_block_location_translator(location, old_course_id=None, usage_id=None)
        """
        # call w/ no matching courses
        org = 'foo_org'
        course = 'bar_course'
        old_style_course_id = '{}/{}/{}'.format(org, course, 'baz_run')
        problem_name = 'abc123abc123abc123abc123abc123f9'
        location = Location('i4x', org, course, 'problem', problem_name)
        with self.assertRaises(ItemNotFoundError):
            loc_mapper().add_block_location_translator(location)
        with self.assertRaises(ItemNotFoundError):
            loc_mapper().add_block_location_translator(location,
                                                       old_style_course_id)

        # w/ one matching course
        new_style_course_id = '{}.{}.{}'.format(org, course, 'baz_run')
        loc_mapper().create_map_entry(
            Location('i4x', org, course, 'course', 'baz_run'),
            new_style_course_id,
        )
        new_usage_id = loc_mapper().add_block_location_translator(location)
        self.assertEqual(new_usage_id, 'problemabc')
        # look it up
        translated_loc = loc_mapper().translate_location(
            old_style_course_id, location, add_entry_if_missing=False)
        self.assertEqual(translated_loc.course_id, new_style_course_id)
        self.assertEqual(translated_loc.usage_id, new_usage_id)

        # w/ one distractor which has one entry already
        new_style_course_id = '{}.geek_dept.{}.{}'.format(
            org, course, 'delta_run')
        loc_mapper().create_map_entry(
            Location('i4x', org, course, 'course', 'delta_run'),
            new_style_course_id,
            block_map={'48f23a10395384929234': {
                'chapter': 'chapter48f'
            }})
        # try adding the one added before
        new_usage_id2 = loc_mapper().add_block_location_translator(location)
        self.assertEqual(new_usage_id, new_usage_id2)
        # it should be in the distractor now
        new_location = loc_mapper().translate_locator_to_location(
            BlockUsageLocator(course_id=new_style_course_id,
                              usage_id=new_usage_id2))
        self.assertEqual(new_location, location)
        # add one close to the existing chapter (cause name collision)
        location = Location('i4x', org, course, 'chapter',
                            '48f23a103953849292341234567890ab')
        new_usage_id = loc_mapper().add_block_location_translator(location)
        self.assertRegexpMatches(new_usage_id, r'^chapter48f\d')
        # retrievable from both courses
        new_location = loc_mapper().translate_locator_to_location(
            BlockUsageLocator(course_id=new_style_course_id,
                              usage_id=new_usage_id))
        self.assertEqual(new_location, location)
        new_location = loc_mapper().translate_locator_to_location(
            BlockUsageLocator(course_id='{}.{}.{}'.format(
                org, course, 'baz_run'),
                              usage_id=new_usage_id))
        self.assertEqual(new_location, location)

        # provoke duplicate item errors
        location = location.replace(name='44f23a103953849292341234567890ab')
        with self.assertRaises(DuplicateItemError):
            loc_mapper().add_block_location_translator(location,
                                                       usage_id=new_usage_id)
        new_usage_id = loc_mapper().add_block_location_translator(
            location, old_course_id=old_style_course_id)
        other_course_old_style = '{}/{}/{}'.format(org, course, 'delta_run')
        new_usage_id2 = loc_mapper().add_block_location_translator(
            location,
            old_course_id=other_course_old_style,
            usage_id='{}b'.format(new_usage_id))
        with self.assertRaises(DuplicateItemError):
            loc_mapper().add_block_location_translator(location)
 def test_update_block(self):
     """
     test update_block_location_translator(location, usage_id, old_course_id=None)
     """
     org = 'foo_org'
     course = 'bar_course'
     new_style_course_id = '{}.geek_dept.{}.baz_run'.format(org, course)
     loc_mapper().create_map_entry(Location('i4x', org, course, 'course',
                                            'baz_run'),
                                   new_style_course_id,
                                   block_map={
                                       'abc123': {
                                           'problem': 'problem2'
                                       },
                                       '48f23a10395384929234': {
                                           'chapter': 'chapter48f'
                                       },
                                       '1': {
                                           'chapter': 'chapter1',
                                           'problem': 'problem1'
                                       },
                                   })
     new_style_course_id2 = '{}.geek_dept.{}.delta_run'.format(org, course)
     loc_mapper().create_map_entry(Location('i4x', org, course, 'course',
                                            'delta_run'),
                                   new_style_course_id2,
                                   block_map={
                                       'abc123': {
                                           'problem': 'problem3'
                                       },
                                       '48f23a10395384929234': {
                                           'chapter': 'chapter48b'
                                       },
                                       '1': {
                                           'chapter': 'chapter2',
                                           'problem': 'problem2'
                                       },
                                   })
     location = Location('i4x', org, course, 'problem', '1')
     # change in all courses to same value
     loc_mapper().update_block_location_translator(location, 'problem1')
     trans_loc = loc_mapper().translate_locator_to_location(
         BlockUsageLocator(course_id=new_style_course_id,
                           usage_id='problem1'))
     self.assertEqual(trans_loc, location)
     trans_loc = loc_mapper().translate_locator_to_location(
         BlockUsageLocator(course_id=new_style_course_id2,
                           usage_id='problem1'))
     self.assertEqual(trans_loc, location)
     # try to change to overwrite used usage_id
     location = Location('i4x', org, course, 'chapter',
                         '48f23a10395384929234')
     with self.assertRaises(DuplicateItemError):
         loc_mapper().update_block_location_translator(location, 'chapter2')
     # just change the one course
     loc_mapper().update_block_location_translator(
         location, 'chapter2', '{}/{}/{}'.format(org, course, 'baz_run'))
     trans_loc = loc_mapper().translate_locator_to_location(
         BlockUsageLocator(course_id=new_style_course_id,
                           usage_id='chapter2'))
     self.assertEqual(trans_loc.name, '48f23a10395384929234')
     # but this still points to the old
     trans_loc = loc_mapper().translate_locator_to_location(
         BlockUsageLocator(course_id=new_style_course_id2,
                           usage_id='chapter2'))
     self.assertEqual(trans_loc.name, '1')
예제 #45
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
예제 #46
0
class OpenEndedModuleTest(unittest.TestCase):
    """
    Test the open ended module class
    """
    location = Location(
        ["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"])

    metadata = json.dumps({'attempts': '10'})
    prompt = etree.XML("<prompt>This is a question prompt</prompt>")
    rubric = etree.XML('''<rubric>
        <category>
        <description>Response Quality</description>
        <option>The response is not a satisfactory answer to the question.  It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option>
        </category>
         </rubric>''')
    max_score = 4

    static_data = {
        'max_attempts': 20,
        'prompt': prompt,
        'rubric': rubric,
        'max_score': max_score,
        'display_name': 'Name',
        'accept_file_upload': False,
        'close_date': None,
        's3_interface': test_util_open_ended.S3_INTERFACE,
        'open_ended_grading_interface':
        test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
        'skip_basic_checks': False,
        'control': {
            'required_peer_grading': 1,
            'peer_grader_count': 1,
            'min_to_calibrate': 3,
            'max_to_calibrate': 6,
            'peer_grade_finished_submissions_when_none_pending': False,
        }
    }

    oeparam = etree.XML('''
      <openendedparam>
            <initial_display>Enter essay here.</initial_display>
            <answer_display>This is the answer.</answer_display>
            <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
        </openendedparam>
    ''')
    definition = {'oeparam': oeparam}
    descriptor = Mock()

    def setUp(self):
        self.test_system = get_test_system()
        self.test_system.open_ended_grading_interface = None
        self.test_system.location = self.location
        self.mock_xqueue = MagicMock()
        self.mock_xqueue.send_to_queue.return_value = (None, "Message")

        def constructed_callback(dispatch="score_update"):
            return dispatch

        self.test_system.xqueue = {
            'interface': self.mock_xqueue,
            'construct_callback': constructed_callback,
            'default_queuename': 'testqueue',
            'waittime': 1
        }
        self.openendedmodule = OpenEndedModule(self.test_system, self.location,
                                               self.definition,
                                               self.descriptor,
                                               self.static_data, self.metadata)

    def test_message_post(self):
        get = {
            'feedback': 'feedback text',
            'submission_id': '1',
            'grader_id': '1',
            'score': 3
        }
        qtime = datetime.strftime(datetime.now(UTC),
                                  xqueue_interface.dateformat)
        student_info = {
            'anonymous_student_id': self.test_system.anonymous_student_id,
            'submission_time': qtime
        }
        contents = {
            'feedback': get['feedback'],
            'submission_id': int(get['submission_id']),
            'grader_id': int(get['grader_id']),
            'score': get['score'],
            'student_info': json.dumps(student_info)
        }

        result = self.openendedmodule.message_post(get, self.test_system)
        self.assertTrue(result['success'])
        # make sure it's actually sending something we want to the queue
        self.mock_xqueue.send_to_queue.assert_called_with(
            body=json.dumps(contents), header=ANY)

        state = json.loads(self.openendedmodule.get_instance_state())
        self.assertIsNotNone(state['child_state'], OpenEndedModule.DONE)

    def test_send_to_grader(self):
        submission = "This is a student submission"
        qtime = datetime.strftime(datetime.now(UTC),
                                  xqueue_interface.dateformat)
        student_info = {
            'anonymous_student_id': self.test_system.anonymous_student_id,
            'submission_time': qtime
        }
        contents = self.openendedmodule.payload.copy()
        contents.update({
            'student_info': json.dumps(student_info),
            'student_response': submission,
            'max_score': self.max_score
        })
        result = self.openendedmodule.send_to_grader(submission,
                                                     self.test_system)
        self.assertTrue(result)
        self.mock_xqueue.send_to_queue.assert_called_with(
            body=json.dumps(contents), header=ANY)

    def update_score_single(self):
        self.openendedmodule.new_history_entry("New Entry")
        score_msg = {
            'correct': True,
            'score': 4,
            'msg': 'Grader Message',
            'feedback': "Grader Feedback"
        }
        get = {'queuekey': "abcd", 'xqueue_body': score_msg}
        self.openendedmodule.update_score(get, self.test_system)

    def update_score_single(self):
        self.openendedmodule.new_history_entry("New Entry")
        feedback = {"success": True, "feedback": "Grader Feedback"}
        score_msg = {
            'correct': True,
            'score': 4,
            'msg': 'Grader Message',
            'feedback': json.dumps(feedback),
            'grader_type': 'IN',
            'grader_id': '1',
            'submission_id': '1',
            'success': True,
            'rubric_scores': [0],
            'rubric_scores_complete': True,
            'rubric_xml': etree.tostring(self.rubric)
        }
        get = {'queuekey': "abcd", 'xqueue_body': json.dumps(score_msg)}
        self.openendedmodule.update_score(get, self.test_system)

    def update_score_multiple(self):
        self.openendedmodule.new_history_entry("New Entry")
        feedback = {"success": True, "feedback": "Grader Feedback"}
        score_msg = {
            'correct':
            True,
            'score': [0, 1],
            'msg':
            'Grader Message',
            'feedback': [json.dumps(feedback),
                         json.dumps(feedback)],
            'grader_type':
            'PE',
            'grader_id': ['1', '2'],
            'submission_id':
            '1',
            'success':
            True,
            'rubric_scores': [[0], [0]],
            'rubric_scores_complete': [True, True],
            'rubric_xml':
            [etree.tostring(self.rubric),
             etree.tostring(self.rubric)]
        }
        get = {'queuekey': "abcd", 'xqueue_body': json.dumps(score_msg)}
        self.openendedmodule.update_score(get, self.test_system)

    def test_latest_post_assessment(self):
        self.update_score_single()
        assessment = self.openendedmodule.latest_post_assessment(
            self.test_system)
        self.assertFalse(assessment == '')
        # check for errors
        self.assertFalse('errors' in assessment)

    def test_update_score_single(self):
        self.update_score_single()
        score = self.openendedmodule.latest_score()
        self.assertEqual(score, 4)

    def test_update_score_multiple(self):
        """
        Tests that a score of [0, 1] gets aggregated to 1.  A change in behavior added by @jbau
        """
        self.update_score_multiple()
        score = self.openendedmodule.latest_score()
        self.assertEquals(score, 1)

    def test_open_ended_display(self):
        """
        Test storing answer with the open ended module.
        """

        # Create a module with no state yet.  Important that this start off as a blank slate.
        test_module = OpenEndedModule(self.test_system, self.location,
                                      self.definition, self.descriptor,
                                      self.static_data, self.metadata)

        saved_response = "Saved response."
        submitted_response = "Submitted response."

        # Initially, there will be no stored answer.
        self.assertEqual(test_module.stored_answer, None)
        # And the initial answer to display will be an empty string.
        self.assertEqual(test_module.get_display_answer(), "")

        # Now, store an answer in the module.
        test_module.handle_ajax("store_answer",
                                {'student_answer': saved_response},
                                get_test_system())
        # The stored answer should now equal our response.
        self.assertEqual(test_module.stored_answer, saved_response)
        self.assertEqual(test_module.get_display_answer(), saved_response)

        # Mock out the send_to_grader function so it doesn't try to connect to the xqueue.
        test_module.send_to_grader = Mock(return_value=True)
        # Submit a student response to the question.
        test_module.handle_ajax(
            "save_answer", {
                "student_answer": submitted_response,
                "can_upload_files": False,
                "student_file": None
            }, get_test_system())
        # Submitting an answer should clear the stored answer.
        self.assertEqual(test_module.stored_answer, None)
        # Confirm that the answer is stored properly.
        self.assertEqual(test_module.latest_answer(), submitted_response)
예제 #47
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_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
예제 #48
0
def create_new_course(request):
    """
    Create a new course.

    Returns the URL for the course overview page.
    """
    if not is_user_in_creator_group(request.user):
        raise PermissionDenied()

    org = request.json.get('org')
    number = request.json.get('number')
    display_name = request.json.get('display_name')
    run = request.json.get('run')

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

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

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

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

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

    initialize_course_tabs(new_course)

    new_location = loc_mapper().translate_location(
        new_course.location.course_id, new_course.location, False, True)
    create_all_course_groups(request.user, new_location)

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

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

    return JsonResponse({'url': new_location.url_reverse("course/", "")})
예제 #49
0
class CombinedOpenEndedModuleTest(unittest.TestCase):
    """
    Unit tests for the combined open ended xmodule
    """
    location = Location(
        ["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"])
    definition_template = """
                    <combinedopenended attempts="10000">
                    {rubric}
                    {prompt}
                    <task>
                    {task1}
                    </task>
                    <task>
                    {task2}
                    </task>
                    </combinedopenended>
                    """
    prompt = "<prompt>This is a question prompt</prompt>"
    rubric = '''<rubric><rubric>
        <category>
        <description>Response Quality</description>
        <option>The response is not a satisfactory answer to the question.  It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option>
        <option>Second option</option>
        </category>
         </rubric></rubric>'''
    max_score = 1

    metadata = {'attempts': '10', 'max_score': max_score}

    static_data = {
        'max_attempts': 20,
        'prompt': prompt,
        'rubric': rubric,
        'max_score': max_score,
        'display_name': 'Name',
        'accept_file_upload': False,
        'close_date': "",
        's3_interface': test_util_open_ended.S3_INTERFACE,
        'open_ended_grading_interface':
        test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
        'skip_basic_checks': False,
        'graded': True,
    }

    oeparam = etree.XML('''
      <openendedparam>
            <initial_display>Enter essay here.</initial_display>
            <answer_display>This is the answer.</answer_display>
            <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
        </openendedparam>
    ''')

    task_xml1 = '''
                <selfassessment>
                    <hintprompt>
                        What hint about this problem would you give to someone?
                    </hintprompt>
                    <submitmessage>
                        Save Succcesful.  Thanks for participating!
                    </submitmessage>
                </selfassessment>
            '''
    task_xml2 = '''
    <openended min_score_to_attempt="1" max_score_to_attempt="1">
            <openendedparam>
                    <initial_display>Enter essay here.</initial_display>
                    <answer_display>This is the answer.</answer_display>
                    <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
           </openendedparam>
    </openended>'''
    definition = {
        'prompt': etree.XML(prompt),
        'rubric': etree.XML(rubric),
        'task_xml': [task_xml1, task_xml2]
    }
    full_definition = definition_template.format(prompt=prompt,
                                                 rubric=rubric,
                                                 task1=task_xml1,
                                                 task2=task_xml2)
    descriptor = Mock(data=full_definition)
    test_system = get_test_system()
    combinedoe_container = CombinedOpenEndedModule(test_system,
                                                   descriptor,
                                                   model_data={
                                                       'data': full_definition,
                                                       'weight': '1',
                                                       'location': location
                                                   })

    def setUp(self):
        # TODO: this constructor call is definitely wrong, but neither branch
        # of the merge matches the module constructor.  Someone (Vik?) should fix this.
        self.combinedoe = CombinedOpenEndedV1Module(
            self.test_system,
            self.location,
            self.definition,
            self.descriptor,
            static_data=self.static_data,
            metadata=self.metadata,
            instance_state=self.static_data)

    def test_get_tag_name(self):
        name = self.combinedoe.get_tag_name("<t>Tag</t>")
        self.assertEqual(name, "t")

    def test_get_last_response(self):
        response_dict = self.combinedoe.get_last_response(0)
        self.assertEqual(response_dict['type'], "selfassessment")
        self.assertEqual(response_dict['max_score'], self.max_score)
        self.assertEqual(response_dict['state'],
                         CombinedOpenEndedV1Module.INITIAL)

    def test_update_task_states(self):
        changed = self.combinedoe.update_task_states()
        self.assertFalse(changed)

        current_task = self.combinedoe.current_task
        current_task.change_state(CombinedOpenEndedV1Module.DONE)
        changed = self.combinedoe.update_task_states()

        self.assertTrue(changed)

    def test_get_max_score(self):
        self.combinedoe.update_task_states()
        self.combinedoe.state = "done"
        self.combinedoe.is_scored = True
        max_score = self.combinedoe.max_score()
        self.assertEqual(max_score, 1)

    def test_container_get_max_score(self):
        #The progress view requires that this function be exposed
        max_score = self.combinedoe_container.max_score()
        self.assertEqual(max_score, None)

    def test_container_weight(self):
        weight = self.combinedoe_container.weight
        self.assertEqual(weight, 1)

    def test_container_child_weight(self):
        weight = self.combinedoe_container.child_module.weight
        self.assertEqual(weight, 1)

    def test_get_score(self):
        score_dict = self.combinedoe.get_score()
        self.assertEqual(score_dict['score'], 0)
        self.assertEqual(score_dict['total'], 1)

    def test_alternate_orderings(self):
        t1 = self.task_xml1
        t2 = self.task_xml2
        xml_to_test = [[t1], [t2], [t1, t1], [t1, t2], [t2, t2], [t2, t1],
                       [t1, t2, t1]]
        for xml in xml_to_test:
            definition = {
                'prompt': etree.XML(self.prompt),
                'rubric': etree.XML(self.rubric),
                'task_xml': xml
            }
            descriptor = Mock(data=definition)
            combinedoe = CombinedOpenEndedV1Module(
                self.test_system,
                self.location,
                definition,
                descriptor,
                static_data=self.static_data,
                metadata=self.metadata,
                instance_state=self.static_data)

            changed = combinedoe.update_task_states()
            self.assertFalse(changed)

    def test_get_score_realistic(self):
        instance_state = r"""{"ready_to_reset": false, "skip_spelling_checks": true, "current_task_number": 1, "weight": 5.0, "graceperiod": "1 day 12 hours 59 minutes 59 seconds", "graded": "True", "task_states": ["{\"child_created\": false, \"child_attempts\": 4, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the <bg>table below . starting mass</bg> g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups <bg>procedure , describe what additional</bg> information you would need in order to replicate the <bs>expe</bs>\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question.  It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question.  It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question.  It is generally correct, although it may contain minor inaccuracies.  There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"\", \"post_assessment\": \"[3]\", \"score\": 3}], \"max_score\": 3, \"child_state\": \"done\"}", "{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the <bg>table below . starting mass</bg> g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups <bg>procedure , describe what additional</bg> information you would need in order to replicate the <bs>expe</bs>\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question.  It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question.  It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question.  It is generally correct, although it may contain minor inaccuracies.  There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"{\\\"submission_id\\\": 3098, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the <bg>table below . starting mass</bg> g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3235, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question.  It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question.  It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question.  It is generally correct, although it may contain minor inaccuracies.  There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"{\\\"submission_id\\\": 3099, \\\"score\\\": 3, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"to replicate the experiment , the procedure would require <bg>more detail . one</bg> piece of information <bg>that is omitted is the</bg> amount of vinegar used in the experiment . it is also important to know what temperature the experiment was kept at during the hours . finally , the procedure needs to include details about the experiment , for example if the whole sample must be submerged .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3237, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>3</score><option points='0'>The response is not a satisfactory answer to the question.  It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question.  It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question.  It is generally correct, although it may contain minor inaccuracies.  There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"{\\\"submission_id\\\": 3100, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"e the mass of four different samples . pour vinegar in <bg>each of four separate</bg> , but identical , containers . place a sample of one material into one container and label . repeat with remaining samples , placing a single sample into a single container . after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . \\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3239, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question.  It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question.  It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question.  It is generally correct, although it may contain minor inaccuracies.  There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}, {\"answer\": \"\", \"post_assessment\": \"{\\\"submission_id\\\": 3101, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"invalid essay .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3241, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question.  It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question.  It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question.  It is generally correct, although it may contain minor inaccuracies.  There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}], \"max_score\": 3, \"child_state\": \"done\"}"], "attempts": "10000", "student_attempts": 0, "due": null, "state": "done", "accept_file_upload": false, "display_name": "Science Question -- Machine Assessed"}"""
        instance_state = json.loads(instance_state)
        rubric = """
        <rubric>
            <rubric>
                <category>
                    <description>Response Quality</description>
                    <option>The response is not a satisfactory answer to the question.  It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option>
                    <option>The response is a marginal answer to the question.  It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option>
                    <option>The response is a proficient answer to the question.  It is generally correct, although it may contain minor inaccuracies.  There is limited evidence of higher-order thinking.</option>
                    <option>The response is correct, complete, and contains evidence of higher-order thinking.</option>
                </category>
            </rubric>
        </rubric>
        """
        definition = {
            'prompt': etree.XML(self.prompt),
            'rubric': etree.XML(rubric),
            'task_xml': [self.task_xml1, self.task_xml2]
        }
        descriptor = Mock(data=definition)
        combinedoe = CombinedOpenEndedV1Module(self.test_system,
                                               self.location,
                                               definition,
                                               descriptor,
                                               static_data=self.static_data,
                                               metadata=self.metadata,
                                               instance_state=instance_state)
        score_dict = combinedoe.get_score()
        self.assertEqual(score_dict['score'], 15.0)
        self.assertEqual(score_dict['total'], 15.0)
예제 #50
0
 def id_to_location(course_id):
     '''Convert the given course_id (org/course/name) to a location object.
     Throws ValueError if course_id is of the wrong format.
     '''
     org, course, name = course_id.split('/')
     return Location('i4x', org, course, 'course', name)
예제 #51
0
def get_course_id(location):
    """
    Returns the course_id from a given the location tuple.
    """
    # TODO: These will need to be changed to point to the particular instance of this problem in the particular course
    return modulestore().get_containing_courses(Location(location))[0].id
예제 #52
0
def modx_dispatch(request, dispatch, location, course_id):
    ''' Generic view for extensions. This is where AJAX calls go.

    Arguments:

      - request -- the django request.
      - dispatch -- the command string to pass through to the module's handle_ajax call
           (e.g. 'problem_reset').  If this string contains '?', only pass
           through the part before the first '?'.
      - location -- the module location. Used to look up the XModule instance
      - course_id -- defines the course context for this request.

    Raises PermissionDenied if the user is not logged in. Raises Http404 if
    the location and course_id do not identify a valid module, the module is
    not accessible by the user, or the module raises NotFoundError. If the
    module raises any other error, it will escape this function.
    '''
    # ''' (fix emacs broken parsing)

    # Check parameters and fail fast if there's a problem
    if not Location.is_valid(location):
        raise Http404("Invalid location")

    if not request.user.is_authenticated():
        raise PermissionDenied

    # Get the submitted data
    data = request.POST.copy()

    # Get and check submitted files
    files = request.FILES or {}
    error_msg = _check_files_limits(files)
    if error_msg:
        return HttpResponse(json.dumps({'success': error_msg}))
    for key in files:  # Merge files into to data dictionary
        data[key] = files.getlist(key)

    try:
        descriptor = modulestore().get_instance(course_id, location)
    except ItemNotFoundError:
        log.warn(
            "Invalid location for course id {course_id}: {location}".format(
                course_id=course_id, location=location))
        raise Http404

    field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
        course_id, request.user, descriptor)

    instance = get_module(request.user,
                          request,
                          location,
                          field_data_cache,
                          course_id,
                          grade_bucket_type='ajax')
    if instance is None:
        # Either permissions just changed, or someone is trying to be clever
        # and load something they shouldn't have access to.
        log.debug("No module {0} for user {1}--access denied?".format(
            location, request.user))
        raise Http404

    # Let the module handle the AJAX
    try:
        ajax_return = instance.handle_ajax(dispatch, data)
        # Save any fields that have changed to the underlying KeyValueStore
        instance.save()

    # If we can't find the module, respond with a 404
    except NotFoundError:
        log.exception("Module indicating to user that request doesn't exist")
        raise Http404

    # For XModule-specific errors, we log the error and respond with an error message
    except ProcessingError as err:
        log.warning("Module encountered an error while processing AJAX call",
                    exc_info=True)
        return JsonResponse(object={'success': err.args[0]}, status=200)

    # If any other error occurred, re-raise it to trigger a 500 response
    except:
        log.exception("error processing ajax call")
        raise

    # Return whatever the module wanted to return to the client/caller
    return HttpResponse(ajax_return)
예제 #53
0
    def test_export_course(self):
        module_store = modulestore('direct')
        draft_store = modulestore('draft')
        content_store = contentstore()

        import_from_xml(module_store, 'common/test/data/', ['full'])
        location = CourseDescriptor.id_to_location(
            'edX/full/6.002_Spring_2012')

        # get a vertical (and components in it) to put into 'draft'
        vertical = module_store.get_item(Location(
            ['i4x', 'edX', 'full', 'vertical', 'vertical_66', None]),
                                         depth=1)

        draft_store.clone_item(vertical.location, vertical.location)

        # We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case.
        draft_store.clone_item(
            vertical.location,
            Location(
                ['i4x', 'edX', 'full', 'vertical', 'no_references', 'draft']))

        for child in vertical.get_children():
            draft_store.clone_item(child.location, child.location)

        root_dir = path(mkdtemp_clean())

        # now create a private vertical
        private_vertical = draft_store.clone_item(
            vertical.location,
            Location(
                ['i4x', 'edX', 'full', 'vertical', 'a_private_vertical',
                 None]))

        # add private to list of children
        sequential = module_store.get_item(
            Location([
                'i4x', 'edX', 'full', 'sequential',
                'Administrivia_and_Circuit_Elements', None
            ]))
        private_location_no_draft = private_vertical.location.replace(
            revision=None)
        module_store.update_children(
            sequential.location,
            sequential.children + [private_location_no_draft.url()])

        # read back the sequential, to make sure we have a pointer to
        sequential = module_store.get_item(
            Location([
                'i4x', 'edX', 'full', 'sequential',
                'Administrivia_and_Circuit_Elements', None
            ]))

        self.assertIn(private_location_no_draft.url(), sequential.children)

        print 'Exporting to tempdir = {0}'.format(root_dir)

        # export out to a tempdir
        export_to_xml(module_store,
                      content_store,
                      location,
                      root_dir,
                      'test_export',
                      draft_modulestore=draft_store)

        # check for static tabs
        self.verify_content_existence(module_store, root_dir, location, 'tabs',
                                      'static_tab', '.html')

        # check for custom_tags
        self.verify_content_existence(module_store, root_dir, location, 'info',
                                      'course_info', '.html')

        # check for custom_tags
        self.verify_content_existence(module_store, root_dir, location,
                                      'custom_tags', 'custom_tag_template')

        # check for graiding_policy.json
        filesystem = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
        self.assertTrue(filesystem.exists('grading_policy.json'))

        course = module_store.get_item(location)
        # compare what's on disk compared to what we have in our course
        with filesystem.open('grading_policy.json', 'r') as grading_policy:
            on_disk = loads(grading_policy.read())
            self.assertEqual(on_disk, course.grading_policy)

        #check for policy.json
        self.assertTrue(filesystem.exists('policy.json'))

        # compare what's on disk to what we have in the course module
        with filesystem.open('policy.json', 'r') as course_policy:
            on_disk = loads(course_policy.read())
            self.assertIn('course/6.002_Spring_2012', on_disk)
            self.assertEqual(on_disk['course/6.002_Spring_2012'],
                             own_metadata(course))

        # remove old course
        delete_course(module_store, content_store, location)

        # reimport
        import_from_xml(module_store,
                        root_dir, ['test_export'],
                        draft_store=draft_store)

        items = module_store.get_items(
            Location(['i4x', 'edX', 'full', 'vertical', None]))
        self.assertGreater(len(items), 0)
        for descriptor in items:
            # don't try to look at private verticals. Right now we're running
            # the service in non-draft aware
            if getattr(descriptor, 'is_draft', False):
                print "Checking {0}....".format(descriptor.location.url())
                resp = self.client.get(
                    reverse('edit_unit',
                            kwargs={'location': descriptor.location.url()}))
                self.assertEqual(resp.status_code, 200)

        # verify that we have the content in the draft store as well
        vertical = draft_store.get_item(Location(
            ['i4x', 'edX', 'full', 'vertical', 'vertical_66', None]),
                                        depth=1)

        self.assertTrue(getattr(vertical, 'is_draft', False))
        for child in vertical.get_children():
            self.assertTrue(getattr(child, 'is_draft', False))

        # make sure that we don't have a sequential that is in draft mode
        sequential = draft_store.get_item(
            Location([
                'i4x', 'edX', 'full', 'sequential',
                'Administrivia_and_Circuit_Elements', None
            ]))

        self.assertFalse(getattr(sequential, 'is_draft', False))

        # verify that we have the private vertical
        test_private_vertical = draft_store.get_item(
            Location(['i4x', 'edX', 'full', 'vertical', 'vertical_66', None]))

        self.assertTrue(getattr(test_private_vertical, 'is_draft', False))

        # make sure the textbook survived the export/import
        course = module_store.get_item(
            Location(
                ['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))

        self.assertGreater(len(course.textbooks), 0)

        shutil.rmtree(root_dir)
예제 #54
0
    def test_get_items_with_course_items(self):
        store = modulestore()

        # fix was to allow get_items() to take the course_id parameter
        store.get_items(Location(None, None, 'vertical', None, None),
                        course_id='abc', depth=0)
예제 #55
0
 def test_jumpto_invalid_location(self):
     location = Location('i4x', 'edX', 'toy', 'NoSuchPlace', None)
     jumpto_url = '{0}/{1}/jump_to/{2}'.format('/courses', self.course_name,
                                               location)
     response = self.client.get(jumpto_url)
     self.assertEqual(response.status_code, 404)
예제 #56
0
    def _add_draft_modules_to_course(self, new_course_id, old_course_id,
                                     old_course_loc, user_id):
        """
        update each draft. Create any which don't exist in published and attach to their parents.
        """
        # each true update below will trigger a new version of the structure. We may want to just have one new version
        # but that's for a later date.
        new_draft_course_loc = CourseLocator(course_id=new_course_id,
                                             branch='draft')
        # to prevent race conditions of grandchilden being added before their parents and thus having no parent to
        # add to
        awaiting_adoption = {}
        for module in self.draft_modulestore.get_items(
                old_course_loc.replace(category=None,
                                       name=None,
                                       revision=draft.DRAFT), old_course_id):
            if getattr(module, 'is_draft', False):
                new_locator = self.loc_mapper.translate_location(
                    old_course_id,
                    module.location,
                    False,
                    add_entry_if_missing=True)
                if self.split_modulestore.has_item(new_course_id, new_locator):
                    # was in 'direct' so draft is a new version
                    split_module = self.split_modulestore.get_item(new_locator)
                    # need to remove any no-longer-explicitly-set values and add/update any now set values.
                    for name, field in split_module.fields.iteritems():
                        if field.is_set_on(split_module) and not module.fields[
                                name].is_set_on(module):
                            field.delete_from(split_module)
                    for name, field in module.fields.iteritems():
                        # draft children will insert themselves and the others are here already; so, don't do it 2x
                        if name != 'children' and field.is_set_on(module):
                            field.write_to(split_module,
                                           field.read_from(module))

                    _new_module = self.split_modulestore.update_item(
                        split_module, user_id)
                else:
                    # only a draft version (aka, 'private'). parent needs updated too.
                    # create a new course version just in case the current head is also the prod head
                    _new_module = self.split_modulestore.create_item(
                        new_draft_course_loc,
                        module.category,
                        user_id,
                        usage_id=new_locator.usage_id,
                        fields=self._get_json_fields_translate_children(
                            module, old_course_id, True))
                    awaiting_adoption[module.location] = new_locator.usage_id
        for draft_location, new_usage_id in awaiting_adoption.iteritems():
            for parent_loc in self.draft_modulestore.get_parent_locations(
                    draft_location, old_course_id):
                old_parent = self.draft_modulestore.get_item(parent_loc)
                new_parent = self.split_modulestore.get_item(
                    self.loc_mapper.translate_location(old_course_id,
                                                       old_parent.location,
                                                       False))
                # this only occurs if the parent was also awaiting adoption
                if new_usage_id in new_parent.children:
                    break
                # find index for module: new_parent may be missing quite a few of old_parent's children
                new_parent_cursor = 0
                draft_location = draft_location.url()  # need as string
                for old_child_loc in old_parent.children:
                    if old_child_loc == draft_location:
                        break
                    sibling_loc = self.loc_mapper.translate_location(
                        old_course_id, Location(old_child_loc), False)
                    # sibling may move cursor
                    for idx in range(new_parent_cursor,
                                     len(new_parent.children)):
                        if new_parent.children[idx] == sibling_loc.usage_id:
                            new_parent_cursor = idx + 1
                            break
                new_parent.children.insert(new_parent_cursor, new_usage_id)
                new_parent = self.split_modulestore.update_item(
                    new_parent, user_id)
    def test_translate_location_read_only(self):
        """
        Test the variants of translate_location which don't create entries, just decode
        """
        # lookup before there are any maps
        org = 'foo_org'
        course = 'bar_course'
        old_style_course_id = '{}/{}/{}'.format(org, course, 'baz_run')
        with self.assertRaises(ItemNotFoundError):
            _ = loc_mapper().translate_location(old_style_course_id,
                                                Location(
                                                    'i4x', org, course,
                                                    'problem', 'abc123'),
                                                add_entry_if_missing=False)

        new_style_course_id = '{}.geek_dept.{}.baz_run'.format(org, course)
        block_map = {'abc123': {'problem': 'problem2'}}
        loc_mapper().create_map_entry(Location('i4x', org, course, 'course',
                                               'baz_run'),
                                      new_style_course_id,
                                      block_map=block_map)
        # only one course matches
        prob_locator = loc_mapper().translate_location(
            old_style_course_id,
            Location('i4x', org, course, 'problem', 'abc123'),
            add_entry_if_missing=False)
        self.assertEqual(prob_locator.course_id, new_style_course_id)
        self.assertEqual(prob_locator.branch, 'published')
        self.assertEqual(prob_locator.usage_id, 'problem2')
        # look for w/ only the Location (works b/c there's only one possible course match)
        prob_locator = loc_mapper().translate_location(
            None,
            Location('i4x', org, course, 'problem', 'abc123'),
            add_entry_if_missing=False)
        self.assertEqual(prob_locator.course_id, new_style_course_id)
        # look for non-existent problem
        with self.assertRaises(ItemNotFoundError):
            prob_locator = loc_mapper().translate_location(
                None,
                Location('i4x', org, course, 'problem', '1def23'),
                add_entry_if_missing=False)

        # add a distractor course
        block_map = {'abc123': {'problem': 'problem3'}}
        loc_mapper().create_map_entry(Location('i4x', org, course, 'course',
                                               'delta_run'),
                                      '{}.geek_dept.{}.{}'.format(
                                          org, course, 'delta_run'),
                                      block_map=block_map)
        prob_locator = loc_mapper().translate_location(
            old_style_course_id,
            Location('i4x', org, course, 'problem', 'abc123'),
            add_entry_if_missing=False)
        self.assertEqual(prob_locator.course_id, new_style_course_id)
        self.assertEqual(prob_locator.usage_id, 'problem2')
        # look for w/ only the Location (not unique; so, just verify it returns something)
        prob_locator = loc_mapper().translate_location(
            None,
            Location('i4x', org, course, 'problem', 'abc123'),
            add_entry_if_missing=False)
        self.assertIsNotNone(prob_locator, "couldn't find ambiguous location")

        # add a default course pointing to the delta_run
        loc_mapper().create_map_entry(Location('i4x', org, course, 'problem',
                                               '789abc123efg456'),
                                      '{}.geek_dept.{}.{}'.format(
                                          org, course, 'delta_run'),
                                      block_map=block_map)
        # now the ambiguous query should return delta
        prob_locator = loc_mapper().translate_location(
            None,
            Location('i4x', org, course, 'problem', 'abc123'),
            add_entry_if_missing=False)
        self.assertEqual(prob_locator.course_id,
                         '{}.geek_dept.{}.{}'.format(org, course, 'delta_run'))
        self.assertEqual(prob_locator.usage_id, 'problem3')

        # get the draft one (I'm sorry this is getting long)
        prob_locator = loc_mapper().translate_location(
            None,
            Location('i4x', org, course, 'problem', 'abc123'),
            published=False,
            add_entry_if_missing=False)
        self.assertEqual(prob_locator.course_id,
                         '{}.geek_dept.{}.{}'.format(org, course, 'delta_run'))
        self.assertEqual(prob_locator.usage_id, 'problem3')
        self.assertEqual(prob_locator.branch, 'draft')
예제 #58
0
def export_to_xml(modulestore,
                  contentstore,
                  course_location,
                  root_dir,
                  course_dir,
                  draft_modulestore=None):
    """
    Export all modules from `modulestore` and content from `contentstore` as xml to `root_dir`.

    `modulestore`: A `ModuleStore` object that is the source of the modules to export
    `contentstore`: A `ContentStore` object that is the source of the content to export, can be None
    `course_location`: The `Location` of the `CourseModuleDescriptor` to export
    `root_dir`: The directory to write the exported xml to
    `course_dir`: The name of the directory inside `root_dir` to write the course content to
    `draft_modulestore`: An optional `DraftModuleStore` that contains draft content, which will be exported
        alongside the public content in the course.
    """

    course_id = course_location.course_id
    course = modulestore.get_course(course_id)

    fs = OSFS(root_dir)
    export_fs = fs.makeopendir(course_dir)

    xml = course.export_to_xml(export_fs)
    with export_fs.open('course.xml', 'w') as course_xml:
        course_xml.write(xml)

    # export the static assets
    policies_dir = export_fs.makeopendir('policies')
    if contentstore:
        contentstore.export_all_for_course(
            course_location,
            root_dir + '/' + course_dir + '/static/',
            root_dir + '/' + course_dir + '/policies/assets.json',
        )

    # export the static tabs
    export_extra_content(export_fs, modulestore, course_id, course_location,
                         'static_tab', 'tabs', '.html')

    # export the custom tags
    export_extra_content(export_fs, modulestore, course_id, course_location,
                         'custom_tag_template', 'custom_tags')

    # export the course updates
    export_extra_content(export_fs, modulestore, course_id, course_location,
                         'course_info', 'info', '.html')

    # export the 'about' data (e.g. overview, etc.)
    export_extra_content(export_fs, modulestore, course_id, course_location,
                         'about', 'about', '.html')

    # export the grading policy
    course_run_policy_dir = policies_dir.makeopendir(course.location.name)
    with course_run_policy_dir.open('grading_policy.json',
                                    'w') as grading_policy:
        grading_policy.write(dumps(course.grading_policy, cls=EdxJSONEncoder))

    # export all of the course metadata in policy.json
    with course_run_policy_dir.open('policy.json', 'w') as course_policy:
        policy = {'course/' + course.location.name: own_metadata(course)}
        course_policy.write(dumps(policy, cls=EdxJSONEncoder))

    # export draft content
    # NOTE: this code assumes that verticals are the top most draftable container
    # should we change the application, then this assumption will no longer
    # be valid
    if draft_modulestore is not None:
        draft_verticals = draft_modulestore.get_items([
            None, course_location.org, course_location.course, 'vertical',
            None, 'draft'
        ])
        if len(draft_verticals) > 0:
            draft_course_dir = export_fs.makeopendir('drafts')
            for draft_vertical in draft_verticals:
                parent_locs = draft_modulestore.get_parent_locations(
                    draft_vertical.location, course.location.course_id)
                # Don't try to export orphaned items.
                if len(parent_locs) > 0:
                    logging.debug('parent_locs = {0}'.format(parent_locs))
                    draft_vertical.xml_attributes[
                        'parent_sequential_url'] = Location(
                            parent_locs[0]).url()
                    sequential = modulestore.get_item(Location(parent_locs[0]))
                    index = sequential.children.index(
                        draft_vertical.location.url())
                    draft_vertical.xml_attributes[
                        'index_in_children_list'] = str(index)
                    draft_vertical.export_to_xml(draft_course_dir)
예제 #59
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))
예제 #60
0
 def setUp(self):
     self.location = Location("edX", 'course', 'run', "video",
                              "SampleProblem1", None)