-
Notifications
You must be signed in to change notification settings - Fork 4k
fix: load user preference for notes visibility in courseware API and toggle_notes.html template #33096
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Thanks for the pull request, @ArturGaspar! Please note that it may take us up to several weeks or months to complete a review and merge your PR. Feel free to add as much of the following information to the ticket as you can:
All technical communication about the code itself will be done via the GitHub pull request interface. As a reminder, our process documentation is here. Please let us know once your PR is ready for our review and all tests are green. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ArturGaspar 👍 Great job on tracking this down on both apps. It was confusing to even test due to the button and the visibility agreeing and disagreeing randomly.
- I tested this: Followed the testing instructions in the description and verified that the visibility works in the expected way
- I read through the code
- I checked for accessibility issues - NA
- Includes documentation
One small suggestion – it would be nice to have some unit tests for the is_visible_for_user
. Not a blocker, as I don't see any control statements in the function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ArturGaspar, I'm missing the explanation of why this is happening - both in the PR description and in the commit message.
This mechanism is currently broken because the course instance provided by the CourseOverview
's private attribute is not bound to any user. Therefore, it always returns True
for the edxnotes_visibility
field (which is the default value).
The code added in your solution binds the student data to the Course XBlock, so it is correct. However, the edxnotes
property of the CourseOverview
model remains broken, so I'd like to suggest an alternative approach to you. Instead of doing this in the edxnotes/helpers
, we could add something like this to openedx/core/djangoapps/content/course_overviews/models.py
(preferably below the _original_course
property):
def bind_course_for_student(self, user):
"""
Bind user-specific field data to the Course XBlock.
By default, the retrieved course XBlock is "unbound" - it means that any field from the `user_info` scope
(like `edxnotes_visibility`) returns its default value.
"""
from lms.djangoapps.courseware.block_render import get_block_for_descriptor
get_block_for_descriptor(
user,
None,
self._original_course,
FieldDataCache([self._original_course], self._original_course.id, user),
self._original_course.id,
)
Then, we could add the following line here:
self.overview.bind_course_for_student(self.effective_user)
The other changes would not be needed, then. We will also eliminate the additional get_course_with_access
call to MongoDB. (Technically, we could even make the bind_course_for_student
method return the course instance and assign it here to remove one more call, but this is outside of the scope of this ticket.)
Adding this method to the CourseOverview
improves the code reusability. I know it's not an ideal solution, as it requires an additional method call, but the CourseOverview
model is used in many places within the edx-platform
, so I'd like to avoid making this change implicit.
Also, a note regarding your change to the lms/templates/edxnotes/toggle_notes.html
template - this change is redundant. The course XBlock used in this template is instantiated within the render_xblock
function. It's not apparent from reading this function's code, but this call initializes the runtime and implicitly binds the field-data to an XBlock it's initializing, thus binding it to the parent XBlocks.
The final note - I was confused while testing this, as the Ctrl key triggers the "Show/Hide Notes" button, so using "Ctrl+R" to reload the page initiated an additional request.
@Agrendalath I have made the change as you suggested and tested that it works. However, I believe that the change to the
|
Well, the runtime initialization and data binding is a very tangled process. I explained some of its aspects in this comment. The following debug log should partially explain how the existing course instance is bound in the [164] > /edx/app/edxapp/edx-platform/xmodule/x_module.py(592)bind_for_student()
-> if self.scope_ids.user_id is not None and user_id == self.scope_ids.user_id:
(Pdb++) l
587
588 from xmodule.course_block import CourseBlock
589 if isinstance(self, CourseBlock):
590 import pdb; pdb.set_trace()
591 # Skip rebinding if we're already bound a user, and it's this user.
592 -> if self.scope_ids.user_id is not None and user_id == self.scope_ids.user_id:
593 if getattr(self.runtime, 'position', None):
594 self.position = self.runtime.position # update the position of the tab
595 return
596
597 # If we are switching users mid-request, save the data from the old user.
(Pdb++) w
# I removed the previous calls from this stack to improve readability.
[124] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/views/views.py(1573)render_xblock()
-> block, _ = get_block_by_usage_id(
[125] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/block_render.py(862)get_block_by_usage_id()
-> instance = get_block_for_descriptor(
[126] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/block_render.py(470)get_block_for_descriptor()
-> access = has_access(user, 'load', block, course_key)
[127] /usr/lib/python3.8/contextlib.py(75)inner()
-> return func(*args, **kwds)
[128] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(153)has_access()
-> return _has_access_to_block(user, action, obj, course_key)
[129] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(597)_has_access_to_block()
-> return _dispatch(checkers, action, user, block)
[130] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(683)_dispatch()
-> result = table[action]()
[131] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(568)can_load()
-> group_access_response = _has_group_access(block, user, course_key)
[132] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(458)_has_group_access()
-> merged_access = block.merged_group_access
[133] /edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/lazy/lazy.py(36)__get__()
-> inst.__dict__[name] = value = self.__func(inst)
[134] /edx/app/edxapp/edx-platform/lms/djangoapps/lms_xblock/mixin.py(121)merged_group_access()
-> parent = self.get_parent()
[135] /edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/xblock/mixins.py(373)get_parent()
-> self._parent_block = self.runtime.get_block(self.parent)
[136] /edx/app/edxapp/edx-platform/xmodule/x_module.py(1410)get_block()
-> return self.get_block_for_descriptor(block)
[137] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/block_render.py(524)inner_get_block()
-> return get_block_for_descriptor(
[138] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/block_render.py(470)get_block_for_descriptor()
-> access = has_access(user, 'load', block, course_key)
[139] /usr/lib/python3.8/contextlib.py(75)inner()
-> return func(*args, **kwds)
[140] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(153)has_access()
-> return _has_access_to_block(user, action, obj, course_key)
[141] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(597)_has_access_to_block()
-> return _dispatch(checkers, action, user, block)
[142] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(683)_dispatch()
-> result = table[action]()
[143] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(568)can_load()
-> group_access_response = _has_group_access(block, user, course_key)
[144] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(458)_has_group_access()
-> merged_access = block.merged_group_access
[145] /edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/lazy/lazy.py(36)__get__()
-> inst.__dict__[name] = value = self.__func(inst)
[146] /edx/app/edxapp/edx-platform/lms/djangoapps/lms_xblock/mixin.py(121)merged_group_access()
-> parent = self.get_parent()
[147] /edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/xblock/mixins.py(373)get_parent()
-> self._parent_block = self.runtime.get_block(self.parent)
[148] /edx/app/edxapp/edx-platform/xmodule/x_module.py(1410)get_block()
-> return self.get_block_for_descriptor(block)
[149] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/block_render.py(524)inner_get_block()
-> return get_block_for_descriptor(
[150] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/block_render.py(470)get_block_for_descriptor()
-> access = has_access(user, 'load', block, course_key)
[151] /usr/lib/python3.8/contextlib.py(75)inner()
-> return func(*args, **kwds)
[152] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(153)has_access()
-> return _has_access_to_block(user, action, obj, course_key)
[153] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(597)_has_access_to_block()
-> return _dispatch(checkers, action, user, block)
[154] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(683)_dispatch()
-> result = table[action]()
[155] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(568)can_load()
-> group_access_response = _has_group_access(block, user, course_key)
[156] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/access.py(458)_has_group_access()
-> merged_access = block.merged_group_access
[157] /edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/lazy/lazy.py(36)__get__()
-> inst.__dict__[name] = value = self.__func(inst)
[158] /edx/app/edxapp/edx-platform/lms/djangoapps/lms_xblock/mixin.py(121)merged_group_access()
-> parent = self.get_parent()
[159] /edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/xblock/mixins.py(373)get_parent()
-> self._parent_block = self.runtime.get_block(self.parent)
[160] /edx/app/edxapp/edx-platform/xmodule/x_module.py(1410)get_block()
-> return self.get_block_for_descriptor(block)
[161] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/block_render.py(524)inner_get_block()
-> return get_block_for_descriptor(
[162] /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/block_render.py(455)get_block_for_descriptor()
-> block.bind_for_student(
[163] /edx/app/edxapp/edx-platform/xmodule/seq_block.py(280)bind_for_student()
-> super().bind_for_student(user_id, wrappers)
[164] > /edx/app/edxapp/edx-platform/xmodule/x_module.py(592)bind_for_student()
-> if self.scope_ids.user_id is not None and user_id == self.scope_ids.user_id:
(Pdb++) self.id
CourseLocator('demo', 'demo', 'demo', None, None)
(Pdb++) id(self)
140456642287936
(Pdb++) c
[124] > /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/views/views.py(1640)render_xblock()
-> 'fragment': fragment,
(Pdb++) l
1652 'missed_deadlines': missed_deadlines,
1653 'missed_gated_content': missed_gated_content,
1654 'has_ended': course.has_ended(),
1655 'web_app_course_url': get_learning_mfe_home_url(course_key=course.id, url_fragment='home'),
1656 'on_courseware_page': True,
1657 'verified_upgrade_link': verified_upgrade_deadline_link(request.user, course=course),
1658 'is_learning_mfe': is_learning_mfe,
1659 'is_mobile_app': is_mobile_app,
1660 'render_course_wide_assets': True,
1661
1662 **optimization_flags,
(Pdb++) id(course)
140456642287936
(Pdb++) c
[135] > /tmp/mako_lms/9db8c4f0cff738fe62d3b9ba5eff080f/edxnotes/toggle_notes.html.py(44)render_body()
-> edxnotes_visibility = course.edxnotes_visibility
(Pdb++) id(course)
140456642287936 Hopefully, we will be able to simplify it further in the future. |
@Agrendalath In my testing the call to The request is specifically The > /edx/app/edxapp/edx-platform/lms/djangoapps/courseware/views/views.py(1639)render_xblock()
-> 'fragment': fragment,
(Pdb) l
1634 optimization_flags = get_optimization_flags_for_content(block, fragment)
1635
1636 import pdb; pdb.set_trace()
1637
1638 context = {
1639 -> 'fragment': fragment,
1640 'course': course,
1641 'block': block,
1642 'disable_accordion': True,
1643 'allow_iframing': True,
1644 'disable_header': True,
(Pdb) course.edxnotes_visibility
True
(Pdb) block.edxnotes_visibility
False |
@ArturGaspar, that's interesting - I cannot reproduce this behavior on my devstack. But as it happens in yours, it's indeed better to keep this approach. I found one bug while checking this - when I use the "View this course as: Specific Student" option, I get the following traceback: 2023-08-31 12:21:14,128 ERROR 1574 [django.request] [user None] [ip None] log.py:224 - Internal Server Error: /api/courseware/course/course-v1:demo+demo+demo
Traceback (most recent call last):
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/core/handlers/base.py", line 181, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/usr/lib/python3.8/contextlib.py", line 75, in inner
return func(*args, **kwds)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/views/generic/base.py", line 70, in view
return self.dispatch(request, *args, **kwargs)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/rest_framework/views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/rest_framework/views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
raise exc
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/rest_framework/views.py", line 506, in dispatch
response = handler(request, *args, **kwargs)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/rest_framework/generics.py", line 208, in get
return self.retrieve(request, *args, **kwargs)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/rest_framework/mixins.py", line 54, in retrieve
instance = self.get_object()
File "/edx/app/edxapp/edx-platform/openedx/core/djangoapps/courseware_api/views.py", line 524, in get_object
overview = CoursewareMeta(
File "/edx/app/edxapp/edx-platform/openedx/core/djangoapps/courseware_api/views.py", line 91, in __init__
self.overview.bind_course_for_student(self.effective_user)
File "/edx/app/edxapp/edx-platform/openedx/core/djangoapps/content/course_overviews/models.py", line 856, in bind_course_for_student
get_block_for_descriptor(
File "/edx/app/edxapp/edx-platform/lms/djangoapps/courseware/block_render.py", line 409, in get_block_for_descriptor
student_kvs = MasqueradingKeyValueStore(student_kvs, request.session)
AttributeError: 'NoneType' object has no attribute 'session' Let's bind the course below this line and make it accept def bind_course_for_student(self, request):
"""
Bind user-specific field data to the Course XBlock.
By default, the retrieved course XBlock is "unbound" - it means that any field from the `user_info` scope
(like `edxnotes_visibility`) returns its default value.
"""
from lms.djangoapps.courseware.block_render import get_block_for_descriptor
get_block_for_descriptor(
request.user,
request,
self._original_course,
FieldDataCache([self._original_course], self._original_course.id, request.user),
self._original_course.id,
) |
@Agrendalath That bug should be fixed in the latest push I made.
|
@ArturGaspar, with this approach, the masquerade does not seem to be working correctly in the
The approach mentioned in my previous comment seems to be resolving it. Side note: When you click the "Show/Hide Notes" button while masquerading as another user, this |
Add a method bind_course_for_student to course overview model, in order to load user preferences before course defaults.
@Agrendalath Done. |
Bind the course overview object to a user in courseware API, in order to use user preferences over course defaults.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
- I tested this: checked that we use the correct value of
edxnotes_visibility
- I read through the code
- I checked for accessibility issues: n/a
- Includes documentation: n/a
- I made sure any change in configuration variables is reflected in the corresponding client's
configuration-secure
repository: n/a
@ormsbee, would you like to do a quick sanity check of this approach? |
@ArturGaspar 🎉 Your pull request was merged! Please take a moment to answer a two question survey so we can improve your experience in the future. |
2U Release Notice: This PR has been deployed to the edX staging environment in preparation for a release to production. |
2U Release Notice: This PR has been deployed to the edX production environment. |
Description
Impacts Learner users.
Fixes openedx/wg-build-test-release#227
Supporting information
openedx/wg-build-test-release#227
openedx/frontend-app-learning#1005
Testing instructions
(Parts from openedx/wg-build-test-release#227)
*Due to the issue documented at openedx/frontend-app-learning#1170, the "Show/Hide Notes" button persists the state that is opposite to the one applied. I.e., if the button is used and notes are hidden, they will be visible when the page is reloaded and vice-versa. Running the MFE with the branch from that pull request should allow observing the correct non-reversed behaviour. This is independent of this issue.