Skip to content

backend view to return tree for course outline and any given block.#14204

Merged
mushtaqak merged 1 commit intomushtaq/move-componentfrom
mushtaq/move-course-tree-nodes
Jan 12, 2017
Merged

backend view to return tree for course outline and any given block.#14204
mushtaqak merged 1 commit intomushtaq/move-componentfrom
mushtaq/move-course-tree-nodes

Conversation

@mushtaqak
Copy link
Contributor

@mushtaqak mushtaqak commented Dec 22, 2016

Description

As a part of Move Epic, this PR adds a functionality to return concise course outline data and ancestor info (parents) of the XBlock .

Below is the sample concise course outline we can get :

{
    'category': 'course',
    'display_name': 'Run 23',
    'id': 'i4x://org.23/course_23/course/Run_23',
    'child_info': {
        'category': 'chapter',
        'display_name': 'Section',
        'children': [{
            'category': 'chapter',
            'display_name': 'Week 1',
            'id': 'i4x://org.23/course_23/chapter/Week_1',
            'child_info': {
                'category': 'sequential',
                'display_name': 'Subsection',
                'children': [{
                    'category': 'sequential',
                    'display_name': 'Lesson 1',
                    'id': 'i4x://org.23/course_23/sequential/Lesson_1',
                    'child_info': {
                        'category': 'vertical',
                        'display_name': 'Unit',
                        'children': [{
                            'category': 'vertical',
                            'display_name': 'Subsection 1',
                            'id': 'i4x://org.23/course_23/vertical/Subsection_1',
                            'child_info': {
                                'children': [{
                                    'category': 'video',
                                    'display_name': 'My Video',
                                    'id': 'i4x://org.23/course_23/video/My_Video'
                                }]
                            }
                        }]
                    }
                }]
            }
        }]
    }
}

Below is the sample ancestor info we can get :

{
    'ancestors': [{
        'category': 'vertical',
        'display_name': 'vertical1',
        'id': 'i4x://org.0/course_0/vertical/4df65ef9c6334c4e93458150bf592349'
    }, {
        'category': 'sequential',
        'display_name': 'seq1',
        'id': 'i4x://org.0/course_0/sequential/fbb1be44e2774d34adf141263c55a13d'
    }, {
        'category': 'chapter',
        'display_name': 'chapter1',
        'id': 'i4x://org.0/course_0/chapter/29604e92469e410fb2431e5c37380d15'
    }, {
        'category': 'course',
        'display_name': 'Run 0',
        'id': 'i4x://org.0/course_0/course/Run_0'
    }]
}

TNL-6061

Testing

  • Unit

Reviewers

If you've been tagged for review, please check your corresponding box once you've given the 👍 or approve review.

Post-review

  • Squash commits

@mushtaqak mushtaqak force-pushed the mushtaq/move-course-tree-nodes branch from c50ed61 to a1f03ed Compare December 22, 2016 16:36
@mushtaqak
Copy link
Contributor Author

@cpennington Do you have some time to review this ?

@mushtaqak mushtaqak changed the title xblock summary data of any given xblock backend view to return tree for course outline and any given block. Dec 26, 2016
@mushtaqak mushtaqak force-pushed the mushtaq/move-course-tree-nodes branch from 92b82f4 to fda38d1 Compare December 27, 2016 07:09
Copy link
Contributor

@muhammad-ammar muhammad-ammar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mushtaqak This is looking great. I left comments about couple of minor changes. I am done with first pass of the review. Let me know once you addressed the feedback.



@ddt.ddt
class TestMoveItem(ItemTest):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mushtaqak I think CourseOutlineTreeTest or TestCourseOutlineTree would be more appropriate because we not moving anything right now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should go in TestCourseOutline which already exists and which covers a lot of the kinds of tests that are needed. I think the course outline should take a concise=true parameter or something to give a more performant response for the move modal.

@ddt.ddt
class TestMoveItem(ItemTest):
"""
Tests for move item.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests for course outline tree.?

"""
def setUp(self):
"""
Creates the test course structure and a few components to 'move'.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about to build course outline tree instead of to move?

boilerplate='multiplechoice.yaml')
self.problem_usage_key = self.response_usage_key(resp)

resp = self.create_xblock(parent_usage_key=self.vert_usage_key, display_name='html1', category='html')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about giving contextual names to above variables instead of resp? For example

chapter1 = self.create_xblock(parent_usage_key=self.usage_key, display_name='chapter1', category='chapter')

for child in xblock_children:
child_info = get_xblock_outline(child)
self.assert_xblock_info(child, child_info)
self.assert_xblock_child_info(child, child_info)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if we can add docstrings for positional arguments in all of the above util methods as per edX coding guideline.

def get_xblock_outline(xblock):
"""
Returns the complete outline of of an xblock.
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add docstring with sample dict structure that will be returned.

for usage_key in (self.problem_usage_key, self.vert_usage_key, self.seq_usage_key):
xblock = self.get_item_from_modulestore(usage_key)
url = reverse('contentstore.views.xblock_handler', kwargs={'usage_key_string': unicode(usage_key)})
url = url + '?course_tree=' + str(fetch_course_tree) if fetch_course_tree else url
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we need
if fetch_course_tree else url
why not just
url = url + '?course_tree={}'.format(etch_course_tree)?

"""
for usage_key in (self.problem_usage_key, self.vert_usage_key, self.seq_usage_key):
xblock = self.get_item_from_modulestore(usage_key)
url = reverse('contentstore.views.xblock_handler', kwargs={'usage_key_string': unicode(usage_key)})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove kwargs

@ddt.data(1, 0)
def test_get_course_tree(self, fetch_course_tree):
"""
Test that we get course tree.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add docstring for fetch_course_tree

self.assert_xblock_info(xblock, response['xblock_info'])
self.assert_course_outline(response['course_outline'])
else:
self.assertNotIn('xblock_info', response)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What else is included in the response? Can we just check if response is {}?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't get the empty response but we get _get_module_info of the block.

@mushtaqak
Copy link
Contributor Author

@andy-armstrong Do you have some time to review this PR. Thanks :)

@andy-armstrong
Copy link
Contributor

andy-armstrong commented Dec 28, 2016

@mushtaqak I implemented a very similar API two years ago when we were first going to implement moving of XBlocks. Can you look at this and see if you can reuse it, and if not then remove my work since I don't believe it was ever used (we moved onto other projects).

https://github.com/edx/edx-platform/pull/2216

@mushtaqak
Copy link
Contributor Author

@andy-armstrong sure, I can think we can use/re-use some of the code like _xmodule_json and _course_json methods that I have created too that have kind of similar logic :)

@andy-armstrong
Copy link
Contributor

@mushtaqak Just to confirm, you are planning to use the same course tree as used in the main course outline, right? We built it so that it could be reused in situations like this. For this to work, your API should return the data in the same JSON structure

@mushtaqak
Copy link
Contributor Author

@andy-armstrong The methods in PR that you mentioned above, had been reworked in this commit f061bbc and now it is create_xblock_info which calculates a lot of data and returns a complex json.

However, we just need the basic data display name, id/location, category and children.

@andy-armstrong
Copy link
Contributor

Thanks for the update, @mushtaqak. I recommend that you refactor the code so that the API can take a parameter to control how much data is returned. I don't think it makes sense to have multiple REST APIs to return course tree information.

@mushtaqak
Copy link
Contributor Author

@andy-armstrong Instead of reusing create_xblock_info, We have decided to not use the #2216, but to go on with this new approach i.e to write new util methods that will be fast and would not need unnecessary information.

If we are using the create_xblock_info we would still need to do all the work that we have done and this method would not make much sense.

@mushtaqak
Copy link
Contributor Author

@muhammad-ammar Feedback addressed.

Arguments:
xblock (Xblock): An XBlock whose summary is to be made.
include_children (bool): If True, includes child XBlocks information.
child_info (List): A list of info of children of the XBlock
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mushtaqak this is not an argument. we only have two arguments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@muhammad-ammar addressed this

@mushtaqak
Copy link
Contributor Author

@andy-armstrong May you please have a look :) Thanks

Copy link
Contributor

@andy-armstrong andy-armstrong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mushtaqak after reviewing this PR, I'm more of the opinion that adding a course outline to the XBlock API is too confusing when we already have a course outline API. IMO you could use the existing API as is and the only issue would be that it returns unnecessary information. It could then be an optimization to allow the client to request a more concise version. This was always the intention of the course outline API, although I can see that it has become bloated as new features have been added.

If you disagree, it may be more efficient to have a quick meeting to discuss the options here. Let me know if you want to do this.

GET
json: returns representation of the xblock (locator id, data, and metadata).
if ?fields=graderType, it returns the graderType for the unit instead of the above.
if ?course_tree=1, it returns the a course_outline summary and XBlock summary.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like 0/1 for boolean parameters. Our standard convention is true/false, such as here:

https://github.com/edx/edx-platform/blob/master/common/djangoapps/student/views.py#L2038

Interestingly, this isn't documented in our current API best practices, so I added a comment about it on @efagin's OEP: openedx/openedx-proposals#36

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, I'd prefer that we not add a new boolean parameter each time we think of a new format. I'd rather you have a more generic option such as format=course_tree. This also seems very similar to @nasthagiri's course blocks API, so maybe that can be shared between the LMS and Studio.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "the a course_outline"

if 'graderType' in fields:
# right now can't combine output of this w/ output of _get_module_info, but worthy goal
return JsonResponse(CourseGradingModel.get_section_grader_type(usage_key))
if int(request.GET.get('course_tree', 0)):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer:

if bool(request.GET.get('course_tree', False)):

course = store.get_course(xblock.location.course_key) # pylint: disable=no-member
xblock_info = xblock_summary(xblock)
course_outline = xblock_summary(course, include_children=True)
return JsonResponse({'course_outline': course_outline, 'xblock_info': xblock_info})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it would make more sense to have this logic live in _get_module_info, so why not pass that parameter to it. That would simplify this logic, and it would also have it wrapped inside bulk_operations.

xblock = _get_xblock(usage_key, request.user)
course = store.get_course(xblock.location.course_key) # pylint: disable=no-member
xblock_info = xblock_summary(xblock)
course_outline = xblock_summary(course, include_children=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this API need to return both the course outline and the info about a given XBlock? It makes it into a confusing API, IMO. Presumably the move modal already has the XBlock info, so I think all it needs is the course outline. I would rather see a single course outline API that can be used by both the full course outline and by this new modal.

'category': xblock.category
}
if include_children and xblock.has_children:
xblock_info['child_info'] = [xblock_summary(child) for child in xblock.get_children()]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this pass include_children to the xblock_summary in order to get the full tree? The default is not to include the children.



@ddt.ddt
class TestMoveItem(ItemTest):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should go in TestCourseOutline which already exists and which covers a lot of the kinds of tests that are needed. I think the course outline should take a concise=true parameter or something to give a more performant response for the move modal.

@mushtaqak
Copy link
Contributor Author

@andy-armstrong I am taking on the approach you have requested. Please see these are the draft changes but you would be able to understand what I am going to do. This may have some optimisation issues but I will compare them once this new approach is ready.

Please comment/suggest if I am going in wrong direction. Thanks.

json: returns representation of the xblock (locator id, data, and metadata).
if ?fields=graderType, it returns the graderType for the unit instead of the above.
if ?course_tree=1, it returns the a course_outline summary and XBlock summary.
if ?format=course_tree, it returns a course_outline summary.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will change this to format=consise

"""
Returns the current publish state for the specified xblock and its children
"""
child_info = None # TODO remove when resolved
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method was throwing exception, for now I have bypassed it, but I look at fixing later.

include_children_predicate=lambda xblock: xblock.has_children,
is_concise=format
)
ancestor_info = _create_xblock_ancestor_info(xblock, course_outline, is_concise=format)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andy-armstrong Should we handle the course-outline concise=true request in course_handler or xblock_handler view? What do you suggest?

We need the concise course-outline data from backend. We also need parents of the source xblock?

Here is the scenario why we need parents info :

For course outline dialog box, we need to know the parents of the source xblock( the component/item being moved). And then when showing the course-outline tree top-level items, we need to let user know which is the parent element of the source tree item and label it as 'Current Location'. You can see this all in this invisionapp screen.

So, we have two options here.

  1. We return ancestor_info using _create_xblock_ancestor_info from backend. OR
  2. We don't return ancestor info from backend but after receiving the data in JS we do the same kind of work _create_xblock_ancestor_info deos in front-end side to know the parents of the source item.

If we go with the (1) approach, I think, this would be the perfect place to handle the request. And have it's test written in where they are currently.

If we go with the (2) approach, We need to handle this request in course_handler. And have it's test written in TestCourseOutline.

What do you suggest, which approach we move forward with ?

If you think we need further discussion on this, please feel free to comment/HipChat me today, so we can discuss this further.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My recommendation would be to use the course_handler, as that is the natural API for returning the course outline. I don't recommend extending it to support additionally returning ancestor info for a block, as IMO that isn't a natural behavior for the API.

One option would be to pass the current parent down to the client in Mako, because its location is known. Then a new request doesn't need to be issued when moving it. I suppose this leaves a window of error where someone else could move the item but that seems unlikely.

Another option would be to issue two requests: one to get the course outline, and a second one to get the current location. In some ways this is better because the two pieces of information are used by different parts of the UI, so it improves the modularity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option would be to issue two requests: one to get the course outline, and a second one to get the current location. In some ways this is better because the two pieces of information are used by different parts of the UI, so it improves the modularity.

This looks a good option.

But the thing is ancestor_info = _create_xblock_ancestor_info(xblock, course_outline, is_concise=format) requires course_outline. So would we be sending the course_outline back from JS ? I don't think we should do that.

Also, another option that I looked for is that we calculate this in front-end but that would be checking entire course_outline each time a new listing of the tree nodes is shown, it may be a bit expensive when compared to one-time backend call ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mushtaqak I did a quick analysis of _create_xblock_ancestor_info and I don't see how it is using the course outline. It is passing it around to various methods but none of them seem to do much with it. You could try passing None and see what happens.

@mushtaqak
Copy link
Contributor Author

@andy-armstrong @muhammad-ammar Please review

Copy link
Contributor

@andy-armstrong andy-armstrong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mushtaqak thanks for switching the approach as I think it is much clearer now. I have a few minor comments and nits, so let me know when I should take a final look.

"""
Returns a JSON representation of the course module and recursively all of its children.
"""
is_concise = True if request.GET.get('format', False) == 'concise' else False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the inner expression is already a boolean, so this can just be:

is_concise = request.GET.get('format', False) == 'concise'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it would be clearer if the default were None or '' rather than False, since the result of the get is a string.

if 'graderType' in fields:
# right now can't combine output of this w/ output of _get_module_info, but worthy goal
return JsonResponse(CourseGradingModel.get_section_grader_type(usage_key))
elif info_type == 'ancestor':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I wonder if you should pass this in fields in the same way as graderType above, rather than introducing a new info_type parameter.

There are three optional boolean parameters:
include_ancestor_info - if true, ancestor info is added to the response
include_child_info - if true, direct child info is included in the response
is_concise - if true, returns the concise version of xblock info.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I'd add (default is false).

else:
xblock_info["staff_only_message"] = False

if is_concise:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like most of the code before this is unnecessary as it is building up an xblock_info dict and then replacing it. I think this if should be earlier before any code starts updating the xblock_info.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be easier to refactor this method into two or more methods, because the logic is hard to read when it is this long.

@mushtaqak
Copy link
Contributor Author

@andy-armstrong and @muhammad-ammar This is also ready for review :)

@catong Can you please check error messages on this PR ?

Copy link
Contributor

@andy-armstrong andy-armstrong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 great stuff, @mushtaqak!

Copy link
Contributor

@muhammad-ammar muhammad-ammar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@mushtaqak mushtaqak force-pushed the mushtaq/move-course-tree-nodes branch from e924384 to ec875f3 Compare January 12, 2017 07:36
Get ancestor info for the given xblock
- TNL-6061
@mushtaqak mushtaqak force-pushed the mushtaq/move-course-tree-nodes branch from ec875f3 to 54f962c Compare January 12, 2017 07:36
@mushtaqak
Copy link
Contributor Author

jenkins run python

@mushtaqak mushtaqak merged commit 4804978 into mushtaq/move-component Jan 12, 2017
@mushtaqak mushtaqak deleted the mushtaq/move-course-tree-nodes branch January 12, 2017 11:52
@mushtaqak mushtaqak mentioned this pull request Mar 1, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants