-
Notifications
You must be signed in to change notification settings - Fork 4.2k
refactor: Introduce VideoConfig service, move video sharing methods in it #37459
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
3be4b69 to
2ac1c33
Compare
|
I have tested the video sharing functionality. And this refactoring doesn't have any impact on how the video sharing works. I am now looking into the test case failures in this PR. |
77cc7a3 to
0eedb35
Compare
0eedb35 to
245402e
Compare
kdmccormick
left a comment
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.
Functionality and testing both look great 👍🏻
Just some more code nits to work through and then we can get this merged.
Could you re-label the commit as a refactor:? The chore: label is supposed to be for really trivial things, like a patch version bump.
| COURSE_VIDEO_SHARING_NONE = 'all-off' | ||
|
|
||
|
|
||
| class VideoSharingUtils: |
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.
This pattern (empty class with static methods) is a common way to organize functions in java, but it is not idiomatic in python. The module already serves the purpose of organizing the functions. So, please remove the VideoSharingUtils class and declare these as regular functions. If you'd like, you could rename the module to sharing.py -- this makes it clear, even without the class, that all the functions within are related to sharing.
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.
Thanks for pointing out, following pythonic way of doing things make sense, going to implement.
I gave it a try to this pattern as it could bring following benefits:
utils.pysounds extensible to me to accommodate other util methods in future.- Defined empty class VideoSharingUtils to group the related util methods
VideoSharingUtils.get_public_video_url()felt more explicit and self-documenting when importing, making it immediately clear what functionality you're using
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.
The only use of a module named utils.py, in my opinion, is for helper functions which cannot be grouped together in any better way. But, you have a good way to group these together (they are sharing-related), so you may as well make that the name of the module.
VideoSharingUtils.get_public_video_url() felt more explicit and self-documenting when importing
This is fair, and you can still achieve it without the class:
from openedx.core.djangoapps.video_config import sharing as video_sharing
video_sharing.get_public_video_url(...)| return fr'{settings.LMS_ROOT_URL}/videos/{str(usage_id)}' | ||
|
|
||
| @staticmethod | ||
| def is_public_sharing_enabled(video_block): |
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.
by taking an instance of VideoBlock as a parameter, this function is making assumptions about the fields of VideoBlock, which it should not be once the VideoBlock is defined outside of the platform.
could this function take just (usage_key: UsageKey, public_access: bool) instead?
please annotate the return type as well.
| elif service_name == 'video_config': | ||
| return VideoConfigService() |
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.
let's not special-case the video-config like this. The field-data service is weird, and isn't the best pattern to follow. Check out the user or teams_configuration xblock services to see a more standard way of injecting the service into the runtime.
| render_template=render_template or mock_render_template, | ||
| mixins=(InheritanceMixin, XModuleMixin), | ||
| services={'field-data': field_data}, | ||
| services={'field-data': field_data, 'video_config': video_config}, |
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.
If this addition is necessary in order to get tests to pass, then this is fine.
But, if tests pass without the service, then please remove it. It's best to keep the test runtime as simple as possible. For example, there's not even a user service in this runtime.
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.
Yes, without this injection tests are failing.
FAILED lms/djangoapps/courseware/tests/test_video_mongo.py::VideoBlockTest::test_get_context - xblock.exceptions.NoSuchServiceError: Service 'video_config' is not available.Detailed logs
_________________________________________________________________________________ VideoBlockTest.test_get_context _________________________________________________________________________________
self = <lms.djangoapps.courseware.tests.test_video_mongo.VideoBlockTest testMethod=test_get_context>
def test_get_context(self):
""""
Test get_context.
This test is located here and not in xmodule.tests because get_context calls editable_metadata_fields.
Which, in turn, uses settings.LANGUAGES from django setttings.
"""
correct_tabs = [
{
'name': "Basic",
'template': "video/transcripts.html",
'current': True
},
{
'name': 'Advanced',
'template': 'tabs/metadata-edit-tab.html'
}
]
> rendered_context = self.block.get_context()
lms/djangoapps/courseware/tests/test_video_mongo.py:1829:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
xmodule/video_block/video_block.py:849: in get_context
_context = MakoTemplateBlockBase.get_context(self)
xmodule/mako_block.py:56: in get_context
'editable_metadata_fields': self.editable_metadata_fields
xmodule/video_block/video_block.py:647: in editable_metadata_fields
editable_fields['public_access']['url'] = self.get_public_video_url()
xmodule/video_block/video_block.py:523: in get_public_video_url
video_config_service = self.runtime.service(self, 'video_config')
xmodule/x_module.py:1505: in service
service = super().service(block=block, service_name=service_name)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <xmodule.tests.TestDescriptorSystem object at 0xffff67fea350>
block = <VideoBlockWithMixins @E3CB license=None, name=None, parent=None, tags=[], display_name='Video', course_edit_method='S...{}, youtube_id_0_75='', youtube_id_1_0='3_yD_cEKoCk', youtube_id_1_25='', youtube_id_1_5='', youtube_is_available=True>
service_name = 'video_config'
def service(self, block, service_name):
"""Return a service, or None.
Services are objects implementing arbitrary other interfaces. They are
requested by agreed-upon names, see [XXX TODO] for a list of possible
services. The object returned depends on the service requested.
XBlocks must announce their intention to request services with the
`XBlock.needs` or `XBlock.wants` decorators. Use `needs` if you assume
that the service is available, or `wants` if your code is flexible and
can accept a None from this method.
Runtimes can override this method if they have different techniques for
finding and delivering services.
Arguments:
block (XBlock): this block's class will be examined for service
decorators.
service_name (str): the name of the service requested.
Returns:
An object implementing the requested service, or None.
"""
declaration = block.service_declaration(service_name)
if declaration is None:
raise NoSuchServiceError(f"Service {service_name!r} was not requested.")
service = self._services.get(service_name)
if service is None and declaration == "need":
> raise NoSuchServiceError(f"Service {service_name!r} is not available.")
E xblock.exceptions.NoSuchServiceError: Service 'video_config' is not available.
/mnt/xblock/xblock/runtime.py:1101: NoSuchServiceError| Args: | ||
| video_block: The video XBlock instance | ||
| Returns: | ||
| Course video sharing option or None |
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.
this information is redundant. you can omit it and add a type annotation instead.
e5e2cc1 to
4ef9a61
Compare
|
Sandbox deployment failed 💥 |
|
Sandbox deployment failed 💥 |
1 similar comment
|
Sandbox deployment failed 💥 |
c2dc35f to
d21ff53
Compare
52f9dd9 to
08fd86e
Compare
2 similar comments
|
@kdmccormick The PR is ready for another review I’ve aligned the service injection with the standard patterns, but please feel free to share any further guidance if needed. |
kdmccormick
left a comment
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.
LGTM
|
Sandbox deployment failed 💥 |
|
Sandbox deployment failed 💥 |
4971f25 to
0cd62bf
Compare
|
Sandbox deployment successful 🚀 |



Ticket: openedx/public-engineering#426