backend view to return tree for course outline and any given block.#14204
backend view to return tree for course outline and any given block.#14204mushtaqak merged 1 commit intomushtaq/move-componentfrom
Conversation
c50ed61 to
a1f03ed
Compare
|
@cpennington Do you have some time to review this ? |
92b82f4 to
fda38d1
Compare
muhammad-ammar
left a comment
There was a problem hiding this comment.
@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): |
There was a problem hiding this comment.
@mushtaqak I think CourseOutlineTreeTest or TestCourseOutlineTree would be more appropriate because we not moving anything right now.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Tests for course outline tree.?
| """ | ||
| def setUp(self): | ||
| """ | ||
| Creates the test course structure and a few components to 'move'. |
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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. | ||
| """ |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)}) |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
What else is included in the response? Can we just check if response is {}?
There was a problem hiding this comment.
We don't get the empty response but we get _get_module_info of the block.
|
@andy-armstrong Do you have some time to review this PR. Thanks :) |
|
@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). |
|
@andy-armstrong sure, I can think we can use/re-use some of the code like |
|
@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 |
|
@andy-armstrong The methods in PR that you mentioned above, had been reworked in this commit f061bbc and now it is However, we just need the basic data display name, id/location, category and children. |
|
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. |
|
@andy-armstrong Instead of reusing If we are using the |
|
@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 |
There was a problem hiding this comment.
@mushtaqak this is not an argument. we only have two arguments.
|
@andy-armstrong May you please have a look :) Thanks |
andy-armstrong
left a comment
There was a problem hiding this comment.
@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. |
There was a problem hiding this comment.
I don't like 0/1 for boolean parameters. Our standard convention is true/false, such as here:
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)): |
There was a problem hiding this comment.
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}) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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()] |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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.
|
@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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
@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.
- We return
ancestor_infousing_create_xblock_ancestor_infofrom backend. OR - 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_infodeos 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
@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.
|
@andy-armstrong @muhammad-ammar Please review |
andy-armstrong
left a comment
There was a problem hiding this comment.
@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 |
There was a problem hiding this comment.
Nit: the inner expression is already a boolean, so this can just be:
is_concise = request.GET.get('format', False) == 'concise'
There was a problem hiding this comment.
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': |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Nit: I'd add (default is false).
| else: | ||
| xblock_info["staff_only_message"] = False | ||
|
|
||
| if is_concise: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
@andy-armstrong and @muhammad-ammar This is also ready for review :) @catong Can you please check error messages on this PR ? |
andy-armstrong
left a comment
There was a problem hiding this comment.
👍 great stuff, @mushtaqak!
e924384 to
ec875f3
Compare
Get ancestor info for the given xblock - TNL-6061
ec875f3 to
54f962c
Compare
|
jenkins run python |
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 :
Below is the sample ancestor info we can get :
TNL-6061
Testing
Reviewers
If you've been tagged for review, please check your corresponding box once you've given the 👍 or approve review.
Post-review