Ejemplo n.º 1
0
def static_tab(request, course_id, tab_slug):
    """
    Display the courses tab with the given name.

    Assumes the course_id is in a valid format.
    """
    try:
        course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
    except InvalidKeyError:
        raise Http404

    course = get_course_with_access(request.user, 'load', course_key)

    tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug)
    if tab is None:
        raise Http404

    contents = get_static_tab_contents(
        request,
        course,
        tab
    )
    if contents is None:
        raise Http404

    return render_to_response('courseware/static_tab.html', {
        'course': course,
        'tab': tab,
        'tab_contents': contents,
    })
Ejemplo n.º 2
0
    def update_item(self, xblock, user_id=None, allow_not_found=False, force=False):
        """
        Update the persisted version of xblock to reflect its current values.

        xblock: which xblock to persist
        user_id: who made the change (ignored for now by this modulestore)
        allow_not_found: whether to create a new object if one didn't already exist or give an error
        force: force is meaningless for this modulestore
        """
        try:
            definition_data = self._convert_reference_fields_to_strings(xblock, xblock.get_explicitly_set_fields_by_scope())
            payload = {
                'definition.data': definition_data,
                'metadata': self._convert_reference_fields_to_strings(xblock, own_metadata(xblock)),
            }
            if xblock.has_children:
                children = self._convert_reference_fields_to_strings(xblock, {'children': xblock.children})
                payload.update({'definition.children': children['children']})
            self._update_single_item(xblock.scope_ids.usage_id, payload)
            # for static tabs, their containing course also records their display name
            if xblock.scope_ids.block_type == 'static_tab':
                course = self._get_course_for_item(xblock.scope_ids.usage_id)
                # find the course's reference to this tab and update the name.
                static_tab = CourseTabList.get_tab_by_slug(course.tabs, xblock.scope_ids.usage_id.name)
                # only update if changed
                if static_tab and static_tab['name'] != xblock.display_name:
                    static_tab['name'] = xblock.display_name
                    self.update_item(course, user_id)

            # recompute (and update) the metadata inheritance tree which is cached
            self.refresh_cached_metadata_inheritance_tree(xblock.scope_ids.usage_id.course_key, xblock.runtime)
            # fire signal that we've written to DB
        except ItemNotFoundError:
            if not allow_not_found:
                raise
Ejemplo n.º 3
0
def static_tab(request, course_id, tab_slug):
    """
    Display the courses tab with the given name.

    Assumes the course_id is in a valid format.
    """
    course = get_course_with_access(request.user, course_id, 'load')

    tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug)
    if tab is None:
        raise Http404

    contents = get_static_tab_contents(
        request,
        course,
        tab
    )
    if contents is None:
        raise Http404

    return render_to_response('courseware/static_tab.html', {
        'course': course,
        'tab': tab,
        'tab_contents': contents,
    })
Ejemplo n.º 4
0
def static_tab(request, course_id, tab_slug):
    """
    Display the courses tab with the given name.

    Assumes the course_id is in a valid format.
    """

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)

    course = get_course_with_access(request.user, 'load', course_key)

    tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug)
    if tab is None:
        raise Http404

    contents = get_static_tab_contents(
        request,
        course,
        tab
    )
    if contents is None:
        raise Http404

    return render_to_response('courseware/static_tab.html', {
        'course': course,
        'tab': tab,
        'tab_contents': contents,
    })
Ejemplo n.º 5
0
    def update_item(self, xblock, user_id=None, allow_not_found=False, force=False):
        """
        Update the persisted version of xblock to reflect its current values.

        xblock: which xblock to persist
        user_id: who made the change (ignored for now by this modulestore)
        allow_not_found: whether to create a new object if one didn't already exist or give an error
        force: force is meaningless for this modulestore
        """
        try:
            definition_data = self._convert_reference_fields_to_strings(xblock, xblock.get_explicitly_set_fields_by_scope())
            payload = {
                'definition.data': definition_data,
                'metadata': self._convert_reference_fields_to_strings(xblock, own_metadata(xblock)),
            }
            if xblock.has_children:
                children = self._convert_reference_fields_to_strings(xblock, {'children': xblock.children})
                payload.update({'definition.children': children['children']})
            self._update_single_item(xblock.scope_ids.usage_id, payload)
            # for static tabs, their containing course also records their display name
            if xblock.scope_ids.block_type == 'static_tab':
                course = self._get_course_for_item(xblock.scope_ids.usage_id)
                # find the course's reference to this tab and update the name.
                static_tab = CourseTabList.get_tab_by_slug(course.tabs, xblock.scope_ids.usage_id.name)
                # only update if changed
                if static_tab and static_tab['name'] != xblock.display_name:
                    static_tab['name'] = xblock.display_name
                    self.update_item(course, user_id)

            # recompute (and update) the metadata inheritance tree which is cached
            self.refresh_cached_metadata_inheritance_tree(xblock.scope_ids.usage_id.course_key, xblock.runtime)
            # fire signal that we've written to DB
        except ItemNotFoundError:
            if not allow_not_found:
                raise
Ejemplo n.º 6
0
    def test_get_static_tab_contents(self):
        course = get_course_by_id(self.toy_course_key)
        request = get_request_for_user(UserFactory.create())
        tab = CourseTabList.get_tab_by_slug(course.tabs, 'resources')

        # Test render works okay
        tab_content = get_static_tab_contents(request, course, tab)
        self.assertIn(self.toy_course_key.to_deprecated_string(), tab_content)
        self.assertIn('static_tab', tab_content)

        # Test when render raises an exception
        with patch('courseware.views.get_module') as mock_module_render:
            mock_module_render.return_value = MagicMock(render=Mock(
                side_effect=Exception('Render failed!')))
            static_tab = get_static_tab_contents(request, course, tab)
            self.assertIn("this module is temporarily unavailable", static_tab)
Ejemplo n.º 7
0
    def test_get_static_tab_contents(self):
        course = get_course_by_id('edX/toy/2012_Fall')
        request = get_request_for_user(UserFactory.create())
        tab = CourseTabList.get_tab_by_slug(course, 'resources')

        # Test render works okay
        tab_content = get_static_tab_contents(request, course, tab)
        self.assertIn('edX/toy/2012_Fall', tab_content)
        self.assertIn('static_tab', tab_content)

        # Test when render raises an exception
        with patch('courseware.views.get_module') as mock_module_render:
            mock_module_render.return_value = MagicMock(
                render=Mock(side_effect=Exception('Render failed!'))
            )
            static_tab = get_static_tab_contents(request, course, tab)
            self.assertIn("this module is temporarily unavailable", static_tab)
Ejemplo n.º 8
0
def static_tab(request, course_id, tab_slug):
    """
    Display the courses tab with the given name.

    Assumes the course_id is in a valid format.
    """
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
    course = get_course_with_access(request.user, "load", course_key)

    tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug)
    if tab is None:
        raise Http404

    contents = get_static_tab_contents(request, course, tab)
    if contents is None:
        raise Http404

    return render_to_response("courseware/static_tab.html", {"course": course, "tab": tab, "tab_contents": contents})
Ejemplo n.º 9
0
    def update_item(self, xblock, user=None, allow_not_found=False):
        """
        Update the persisted version of xblock to reflect its current values.

        location: Something that can be passed to Location
        data: A nested dictionary of problem data
        """
        try:
            definition_data = xblock.get_explicitly_set_fields_by_scope()
            payload = {
                'definition.data': definition_data,
                'metadata': own_metadata(xblock),
            }
            if xblock.has_children:
                # convert all to urls
                xblock.children = [
                    child.url() if isinstance(child, Location) else child
                    for child in xblock.children
                ]
                payload.update({'definition.children': xblock.children})
            self._update_single_item(xblock.location, payload)
            # for static tabs, their containing course also records their display name
            if xblock.category == 'static_tab':
                course = self._get_course_for_item(xblock.location)
                # find the course's reference to this tab and update the name.
                static_tab = CourseTabList.get_tab_by_slug(
                    course.tabs, xblock.location.name)
                # only update if changed
                if static_tab and static_tab['name'] != xblock.display_name:
                    static_tab['name'] = xblock.display_name
                    self.update_item(course, user)

            # recompute (and update) the metadata inheritance tree which is cached
            # was conditional on children or metadata having changed before dhm made one update to rule them all
            self.refresh_cached_metadata_inheritance_tree(xblock.location)
            # fire signal that we've written to DB
            self.fire_updated_modulestore_signal(
                get_course_id_no_run(xblock.location), xblock.location)
        except ItemNotFoundError:
            if not allow_not_found:
                raise
Ejemplo n.º 10
0
    def update_item(self, xblock, user=None, allow_not_found=False):
        """
        Update the persisted version of xblock to reflect its current values.

        location: Something that can be passed to Location
        data: A nested dictionary of problem data
        """
        try:
            definition_data = xblock.get_explicitly_set_fields_by_scope()
            payload = {
                'definition.data': definition_data,
                'metadata': own_metadata(xblock),
            }
            if xblock.has_children:
                # convert all to urls
                xblock.children = [child.url() if isinstance(child, Location) else child
                                   for child in xblock.children]
                payload.update({'definition.children': xblock.children})
            self._update_single_item(xblock.location, payload)
            # for static tabs, their containing course also records their display name
            if xblock.category == 'static_tab':
                course = self._get_course_for_item(xblock.location)
                # find the course's reference to this tab and update the name.
                static_tab = CourseTabList.get_tab_by_slug(course, xblock.location.name)
                # only update if changed
                if static_tab and static_tab['name'] != xblock.display_name:
                    static_tab['name'] = xblock.display_name
                    self.update_item(course, user)

            # recompute (and update) the metadata inheritance tree which is cached
            # was conditional on children or metadata having changed before dhm made one update to rule them all
            self.refresh_cached_metadata_inheritance_tree(xblock.location)
            # fire signal that we've written to DB
            self.fire_updated_modulestore_signal(get_course_id_no_run(xblock.location), xblock.location)
        except ItemNotFoundError:
            if not allow_not_found:
                raise
Ejemplo n.º 11
0
def _save_item(user,
               usage_key,
               data=None,
               children=None,
               metadata=None,
               nullout=None,
               grader_type=None,
               publish=None):
    """
    Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
    nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
    to default).
    """
    store = modulestore()

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

    old_metadata = own_metadata(existing_item)
    old_content = existing_item.get_explicitly_set_fields_by_scope(
        Scope.content)

    if publish:
        if publish == 'make_private':
            try:
                store.unpublish(existing_item.location, user.id),
            except ItemNotFoundError:
                pass
        elif publish == 'create_draft':
            try:
                store.convert_to_draft(existing_item.location, user.id)
            except DuplicateItemError:
                pass

    if data:
        # TODO Allow any scope.content fields not just "data" (exactly like the get below this)
        existing_item.data = data
    else:
        data = old_content['data'] if 'data' in old_content else None

    if children is not None:
        children_usage_keys = []
        for child in children:
            child_usage_key = UsageKey.from_string(child)
            child_usage_key = child_usage_key.replace(course_key=modulestore(
            ).fill_in_run(child_usage_key.course_key))
            children_usage_keys.append(child_usage_key)
        existing_item.children = children_usage_keys

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

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

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

    if callable(getattr(existing_item, "editor_saved", None)):
        existing_item.editor_saved(user, old_metadata, old_content)

    # commit to datastore
    store.update_item(existing_item, user.id)

    # for static tabs, their containing course also records their display name
    if usage_key.category == 'static_tab':
        course = store.get_course(usage_key.course_key)
        # find the course's reference to this tab and update the name.
        static_tab = CourseTabList.get_tab_by_slug(course.tabs, usage_key.name)
        # only update if changed
        if static_tab and static_tab['name'] != existing_item.display_name:
            static_tab['name'] = existing_item.display_name
            store.update_item(course, user.id)

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

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

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

    # Note that children aren't being returned until we have a use case.
    return JsonResponse(result)
Ejemplo n.º 12
0
def _save_xblock(user, xblock, data=None, children=None, metadata=None, nullout=None,
                 grader_type=None, publish=None):
    """
    Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
    nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
    to default).
    """
    store = modulestore()

    # Don't allow updating an xblock and discarding changes in a single operation (unsupported by UI).
    if publish == "discard_changes":
        store.revert_to_published(xblock.location, user.id)
        # Returning the same sort of result that we do for other save operations. In the future,
        # we may want to return the full XBlockInfo.
        return JsonResponse({'id': unicode(xblock.location)})

    old_metadata = own_metadata(xblock)
    old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content)

    if data:
        # TODO Allow any scope.content fields not just "data" (exactly like the get below this)
        xblock.data = data
    else:
        data = old_content['data'] if 'data' in old_content else None

    if children is not None:
        children_usage_keys = []
        for child in children:
            child_usage_key = usage_key_with_run(child)
            children_usage_keys.append(child_usage_key)
        xblock.children = children_usage_keys

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

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

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

    if callable(getattr(xblock, "editor_saved", None)):
        xblock.editor_saved(user, old_metadata, old_content)

    # commit to datastore
    store.update_item(xblock, user.id)

    # for static tabs, their containing course also records their display name
    if xblock.location.category == 'static_tab':
        course = store.get_course(xblock.location.course_key)
        # find the course's reference to this tab and update the name.
        static_tab = CourseTabList.get_tab_by_slug(course.tabs, xblock.location.name)
        # only update if changed
        if static_tab and static_tab['name'] != xblock.display_name:
            static_tab['name'] = xblock.display_name
            store.update_item(course, user.id)

    result = {
        'id': unicode(xblock.location),
        'data': data,
        'metadata': own_metadata(xblock)
    }

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

    # If publish is set to 'republish' and this item is not in direct only categories and has previously been published,
    # then this item should be republished. This is used by staff locking to ensure that changing the draft
    # value of the staff lock will also update the published version, but only at the unit level.
    if publish == 'republish' and xblock.category not in DIRECT_ONLY_CATEGORIES:
        if modulestore().has_published_version(xblock):
            publish = 'make_public'

    # Make public after updating the xblock, in case the caller asked for both an update and a publish.
    # Used by Bok Choy tests and by republishing of staff locks.
    if publish == 'make_public':
        modulestore().publish(xblock.location, user.id)

    # Note that children aren't being returned until we have a use case.
    return JsonResponse(result, encoder=EdxJSONEncoder)
Ejemplo n.º 13
0
def _save_xblock(user,
                 xblock,
                 data=None,
                 children=None,
                 metadata=None,
                 nullout=None,
                 grader_type=None,
                 publish=None):
    """
    Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
    nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
    to default).
    """
    store = modulestore()

    # Don't allow updating an xblock and discarding changes in a single operation (unsupported by UI).
    if publish == "discard_changes":
        store.revert_to_published(xblock.location, user.id)
        # Returning the same sort of result that we do for other save operations. In the future,
        # we may want to return the full XBlockInfo.
        return JsonResponse({'id': unicode(xblock.location)})

    old_metadata = own_metadata(xblock)
    old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content)

    if data:
        # TODO Allow any scope.content fields not just "data" (exactly like the get below this)
        xblock.data = data
    else:
        data = old_content['data'] if 'data' in old_content else None

    if children is not None:
        children_usage_keys = []
        for child in children:
            child_usage_key = usage_key_with_run(child)
            children_usage_keys.append(child_usage_key)
        xblock.children = children_usage_keys

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

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

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

    if callable(getattr(xblock, "editor_saved", None)):
        xblock.editor_saved(user, old_metadata, old_content)

    # commit to datastore
    store.update_item(xblock, user.id)

    # for static tabs, their containing course also records their display name
    if xblock.location.category == 'static_tab':
        course = store.get_course(xblock.location.course_key)
        # find the course's reference to this tab and update the name.
        static_tab = CourseTabList.get_tab_by_slug(course.tabs,
                                                   xblock.location.name)
        # only update if changed
        if static_tab and static_tab['name'] != xblock.display_name:
            static_tab['name'] = xblock.display_name
            store.update_item(course, user.id)

    result = {
        'id': unicode(xblock.location),
        'data': data,
        'metadata': own_metadata(xblock)
    }

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

    # If publish is set to 'republish' and this item is not in direct only categories and has previously been published,
    # then this item should be republished. This is used by staff locking to ensure that changing the draft
    # value of the staff lock will also update the published version, but only at the unit level.
    if publish == 'republish' and xblock.category not in DIRECT_ONLY_CATEGORIES:
        if modulestore().has_published_version(xblock):
            publish = 'make_public'

    # Make public after updating the xblock, in case the caller asked for both an update and a publish.
    # Used by Bok Choy tests and by republishing of staff locks.
    if publish == 'make_public':
        modulestore().publish(xblock.location, user.id)

    # Note that children aren't being returned until we have a use case.
    return JsonResponse(result, encoder=EdxJSONEncoder)
Ejemplo n.º 14
0
def _save_xblock(user,
                 xblock,
                 data=None,
                 children_strings=None,
                 metadata=None,
                 nullout=None,
                 grader_type=None,
                 publish=None):
    """
    Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
    nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
    to default).
    """
    store = modulestore()
    # Perform all xblock changes within a (single-versioned) transaction
    with store.bulk_operations(xblock.location.course_key):

        # Don't allow updating an xblock and discarding changes in a single operation (unsupported by UI).
        if publish == "discard_changes":
            store.revert_to_published(xblock.location, user.id)
            # Returning the same sort of result that we do for other save operations. In the future,
            # we may want to return the full XBlockInfo.
            return JsonResponse({'id': unicode(xblock.location)})

        old_metadata = own_metadata(xblock)
        old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content)

        if data:
            # TODO Allow any scope.content fields not just "data" (exactly like the get below this)
            xblock.data = data
        else:
            data = old_content['data'] if 'data' in old_content else None

        if children_strings is not None:
            children = []
            for child_string in children_strings:
                children.append(usage_key_with_run(child_string))

            # if new children have been added, remove them from their old parents
            new_children = set(children) - set(xblock.children)
            for new_child in new_children:
                old_parent_location = store.get_parent_location(new_child)
                if old_parent_location:
                    old_parent = store.get_item(old_parent_location)
                    old_parent.children.remove(new_child)
                    old_parent = _update_with_callback(old_parent, user)
                else:
                    # the Studio UI currently doesn't present orphaned children, so assume this is an error
                    return JsonResponse(
                        {
                            "error":
                            "Invalid data, possibly caused by concurrent authors."
                        }, 400)

            # make sure there are no old children that became orphans
            # In a single-author (no-conflict) scenario, all children in the persisted list on the server should be
            # present in the updated list.  If there are any children that have been dropped as part of this update,
            # then that would be an error.
            #
            # We can be even more restrictive in a multi-author (conflict), by returning an error whenever
            # len(old_children) > 0. However, that conflict can still be "merged" if the dropped child had been
            # re-parented. Hence, the check for the parent in the any statement below.
            #
            # Note that this multi-author conflict error should not occur in modulestores (such as Split) that support
            # atomic write transactions.  In Split, if there was another author who moved one of the "old_children"
            # into another parent, then that child would have been deleted from this parent on the server. However,
            # this is error could occur in modulestores (such as Draft) that do not support atomic write-transactions
            old_children = set(xblock.children) - set(children)
            if any(
                    store.get_parent_location(old_child) == xblock.location
                    for old_child in old_children):
                # since children are moved as part of a single transaction, orphans should not be created
                return JsonResponse(
                    {
                        "error":
                        "Invalid data, possibly caused by concurrent authors."
                    }, 400)

            # set the children on the xblock
            xblock.children = children

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

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

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

        # update the xblock and call any xblock callbacks
        xblock = _update_with_callback(xblock, user, old_metadata, old_content)

        # for static tabs, their containing course also records their display name
        if xblock.location.category == 'static_tab':
            course = store.get_course(xblock.location.course_key)
            # find the course's reference to this tab and update the name.
            static_tab = CourseTabList.get_tab_by_slug(course.tabs,
                                                       xblock.location.name)
            # only update if changed
            if static_tab and static_tab['name'] != xblock.display_name:
                static_tab['name'] = xblock.display_name
                store.update_item(course, user.id)

        result = {
            'id': unicode(xblock.location),
            'data': data,
            'metadata': own_metadata(xblock)
        }

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

        # If publish is set to 'republish' and this item is not in direct only categories and has previously been published,
        # then this item should be republished. This is used by staff locking to ensure that changing the draft
        # value of the staff lock will also update the published version, but only at the unit level.
        if publish == 'republish' and xblock.category not in DIRECT_ONLY_CATEGORIES:
            if modulestore().has_published_version(xblock):
                publish = 'make_public'

        # Make public after updating the xblock, in case the caller asked for both an update and a publish.
        # Used by Bok Choy tests and by republishing of staff locks.
        if publish == 'make_public':
            modulestore().publish(xblock.location, user.id)

        # Note that children aren't being returned until we have a use case.
        return JsonResponse(result, encoder=EdxJSONEncoder)
Ejemplo n.º 15
0
    def _load_extra_content(self, system, course_descriptor, category,
                            content_path, course_dir):
        """
        Import fields data content from files
        """
        for filepath in glob.glob(content_path / '*'):
            if not os.path.isfile(filepath):
                continue

            if filepath.endswith('~'):  # skip *~ files
                continue

            with open(filepath) as f:
                try:
                    if filepath.find('.json') != -1:
                        # json file with json data content
                        slug, loc, data_content = self._import_field_content(
                            course_descriptor, category, filepath)
                        if data_content is None:
                            continue
                        else:
                            try:
                                # get and update data field in xblock runtime
                                module = system.load_item(loc)
                                for key, value in data_content.items():
                                    setattr(module, key, value)
                                module.save()
                            except ItemNotFoundError:
                                module = None
                                data_content['location'] = loc
                                data_content['category'] = category
                    else:
                        slug = os.path.splitext(os.path.basename(filepath))[0]
                        loc = course_descriptor.scope_ids.usage_id.replace(
                            category=category, name=slug)
                        # html file with html data content
                        html = f.read()
                        try:
                            module = system.load_item(loc)
                            module.data = html
                            module.save()
                        except ItemNotFoundError:
                            module = None
                            data_content = {
                                'data': html,
                                'location': loc,
                                'category': category
                            }

                    if module is None:
                        module = system.construct_xblock(
                            category,
                            # We're loading a descriptor, so student_id is meaningless
                            # We also don't have separate notions of definition and usage ids yet,
                            # so we use the location for both
                            ScopeIds(None, category, loc, loc),
                            DictFieldData(data_content),
                        )
                        # VS[compat]:
                        # Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)  # lint-amnesty, pylint: disable=line-too-long
                        # from the course policy
                        if category == "static_tab":
                            tab = CourseTabList.get_tab_by_slug(
                                tab_list=course_descriptor.tabs, url_slug=slug)
                            if tab:
                                module.display_name = tab.name
                                module.course_staff_only = tab.course_staff_only
                        module.data_dir = course_dir
                        module.save()

                        self.modules[course_descriptor.id][
                            module.scope_ids.usage_id] = module
                except Exception as exc:  # pylint: disable=broad-except
                    logging.exception(
                        "Failed to load %s. Skipping... \
                            Exception: %s", filepath, str(exc))
                    system.error_tracker("ERROR: " + str(exc))
Ejemplo n.º 16
0
def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout=None,
               grader_type=None, publish=None):
    """
    Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
    nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
    to default).
    """
    store = modulestore()

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

    old_metadata = own_metadata(existing_item)
    old_content = existing_item.get_explicitly_set_fields_by_scope(Scope.content)

    if publish:
        if publish == 'make_private':
            try:
                store.unpublish(existing_item.location, user.id),
            except ItemNotFoundError:
                pass
        elif publish == 'create_draft':
            try:
                store.convert_to_draft(existing_item.location, user.id)
            except DuplicateItemError:
                pass


    if data:
        # TODO Allow any scope.content fields not just "data" (exactly like the get below this)
        existing_item.data = data
    else:
        data = old_content['data'] if 'data' in old_content else None

    if children is not None:
        children_usage_keys = []
        for child in children:
            child_usage_key = UsageKey.from_string(child)
            child_usage_key = child_usage_key.replace(course_key=modulestore().fill_in_run(child_usage_key.course_key))
            children_usage_keys.append(child_usage_key)
        existing_item.children = children_usage_keys

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

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

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

    if callable(getattr(existing_item, "editor_saved", None)):
        existing_item.editor_saved(user, old_metadata, old_content)

    # commit to datastore
    store.update_item(existing_item, user.id)

    # for static tabs, their containing course also records their display name
    if usage_key.category == 'static_tab':
        course = store.get_course(usage_key.course_key)
        # find the course's reference to this tab and update the name.
        static_tab = CourseTabList.get_tab_by_slug(course.tabs, usage_key.name)
        # only update if changed
        if static_tab and static_tab['name'] != existing_item.display_name:
            static_tab['name'] = existing_item.display_name
            store.update_item(course, user.id)

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

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

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

    # Note that children aren't being returned until we have a use case.
    return JsonResponse(result)
Ejemplo n.º 17
0
    def _load_extra_content(self, system, course_descriptor, category, content_path, course_dir):
        """
        Import fields data content from files
        """
        for filepath in glob.glob(content_path / "*"):
            if not os.path.isfile(filepath):
                continue

            if filepath.endswith("~"):  # skip *~ files
                continue

            with open(filepath) as f:
                try:
                    if filepath.find(".json") != -1:
                        # json file with json data content
                        slug, loc, data_content = self._import_field_content(course_descriptor, category, filepath)
                        if data_content is None:
                            continue
                        else:
                            try:
                                # get and update data field in xblock runtime
                                module = system.load_item(loc)
                                for key, value in data_content.iteritems():
                                    setattr(module, key, value)
                                module.save()
                            except ItemNotFoundError:
                                module = None
                                data_content["location"] = loc
                                data_content["category"] = category
                    else:
                        slug = os.path.splitext(os.path.basename(filepath))[0]
                        loc = course_descriptor.scope_ids.usage_id.replace(category=category, name=slug)
                        # html file with html data content
                        html = f.read().decode("utf-8")
                        try:
                            module = system.load_item(loc)
                            module.data = html
                            module.save()
                        except ItemNotFoundError:
                            module = None
                            data_content = {"data": html, "location": loc, "category": category}

                    if module is None:
                        module = system.construct_xblock(
                            category,
                            # We're loading a descriptor, so student_id is meaningless
                            # We also don't have separate notions of definition and usage ids yet,
                            # so we use the location for both
                            ScopeIds(None, category, loc, loc),
                            DictFieldData(data_content),
                        )
                        # VS[compat]:
                        # Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
                        # from the course policy
                        if category == "static_tab":
                            tab = CourseTabList.get_tab_by_slug(tab_list=course_descriptor.tabs, url_slug=slug)
                            if tab:
                                module.display_name = tab.name
                        module.data_dir = course_dir
                        module.save()

                        self.modules[course_descriptor.id][module.scope_ids.usage_id] = module
                except Exception as exc:  # pylint: disable=broad-except
                    logging.exception(
                        "Failed to load %s. Skipping... \
                            Exception: %s",
                        filepath,
                        unicode(exc),
                    )
                    system.error_tracker("ERROR: " + unicode(exc))
Ejemplo n.º 18
0
def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None, nullout=None,
                 grader_type=None, publish=None):
    """
    Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
    nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
    to default).
    """
    store = modulestore()
    # Perform all xblock changes within a (single-versioned) transaction
    with store.bulk_operations(xblock.location.course_key):

        # Don't allow updating an xblock and discarding changes in a single operation (unsupported by UI).
        if publish == "discard_changes":
            store.revert_to_published(xblock.location, user.id)
            # Returning the same sort of result that we do for other save operations. In the future,
            # we may want to return the full XBlockInfo.
            return JsonResponse({'id': unicode(xblock.location)})

        old_metadata = own_metadata(xblock)
        old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content)

        if data:
            # TODO Allow any scope.content fields not just "data" (exactly like the get below this)
            xblock.data = data
        else:
            data = old_content['data'] if 'data' in old_content else None

        if children_strings is not None:
            children = []
            for child_string in children_strings:
                children.append(usage_key_with_run(child_string))

            # if new children have been added, remove them from their old parents
            new_children = set(children) - set(xblock.children)
            for new_child in new_children:
                old_parent_location = store.get_parent_location(new_child)
                if old_parent_location:
                    old_parent = store.get_item(old_parent_location)
                    old_parent.children.remove(new_child)
                    old_parent = _update_with_callback(old_parent, user)
                else:
                    # the Studio UI currently doesn't present orphaned children, so assume this is an error
                    return JsonResponse({"error": "Invalid data, possibly caused by concurrent authors."}, 400)

            # make sure there are no old children that became orphans
            # In a single-author (no-conflict) scenario, all children in the persisted list on the server should be
            # present in the updated list.  If there are any children that have been dropped as part of this update,
            # then that would be an error.
            #
            # We can be even more restrictive in a multi-author (conflict), by returning an error whenever
            # len(old_children) > 0. However, that conflict can still be "merged" if the dropped child had been
            # re-parented. Hence, the check for the parent in the any statement below.
            #
            # Note that this multi-author conflict error should not occur in modulestores (such as Split) that support
            # atomic write transactions.  In Split, if there was another author who moved one of the "old_children"
            # into another parent, then that child would have been deleted from this parent on the server. However,
            # this is error could occur in modulestores (such as Draft) that do not support atomic write-transactions
            old_children = set(xblock.children) - set(children)
            if any(
                    store.get_parent_location(old_child) == xblock.location
                    for old_child in old_children
            ):
                # since children are moved as part of a single transaction, orphans should not be created
                return JsonResponse({"error": "Invalid data, possibly caused by concurrent authors."}, 400)

            # set the children on the xblock
            xblock.children = children

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

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

                    if value is None:
                        field.delete_from(xblock)
                    else:
                        try:
                            value = field.from_json(value)
                        except ValueError as verr:
                            reason = _("Invalid data")
                            if verr.message:
                                reason = _("Invalid data ({details})").format(details=verr.message)
                            return JsonResponse({"error": reason}, 400)

                        field.write_to(xblock, value)

        # update the xblock and call any xblock callbacks
        xblock = _update_with_callback(xblock, user, old_metadata, old_content)

        # for static tabs, their containing course also records their display name
        if xblock.location.category == 'static_tab':
            course = store.get_course(xblock.location.course_key)
            # find the course's reference to this tab and update the name.
            static_tab = CourseTabList.get_tab_by_slug(course.tabs, xblock.location.name)
            # only update if changed
            if static_tab and static_tab['name'] != xblock.display_name:
                static_tab['name'] = xblock.display_name
                store.update_item(course, user.id)

        result = {
            'id': unicode(xblock.location),
            'data': data,
            'metadata': own_metadata(xblock)
        }

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

        # If publish is set to 'republish' and this item is not in direct only categories and has previously been published,
        # then this item should be republished. This is used by staff locking to ensure that changing the draft
        # value of the staff lock will also update the published version, but only at the unit level.
        if publish == 'republish' and xblock.category not in DIRECT_ONLY_CATEGORIES:
            if modulestore().has_published_version(xblock):
                publish = 'make_public'

        # Make public after updating the xblock, in case the caller asked for both an update and a publish.
        # Used by Bok Choy tests and by republishing of staff locks.
        if publish == 'make_public':
            modulestore().publish(xblock.location, user.id)

        # Note that children aren't being returned until we have a use case.
        return JsonResponse(result, encoder=EdxJSONEncoder)