-
Notifications
You must be signed in to change notification settings - Fork 4.2k
[FC-0009] Paste Components (OLX) into any Unit in Studio #31969
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, @bradenmacdonald! As a core committer in this repo, you can merge this once the pull request is approved per the core committer reviewer requirements and according to the agreement with your edX Champion. |
c1505b2 to
4aa324a
Compare
|
@Agrendalath @ormsbee This is ready for review. |
8415cd9 to
fa6e990
Compare
setup.cfg
Outdated
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 am now using the new api.py import linter for this new content_staging django app; this work was part of my motivation for building that.
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.
@bradenmacdonald, when I check out your branch, I get the following error in Studio logs. Do I need to do anything specific to get it working?
Exception in thread django-main-thread:
Traceback (most recent call last):
File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner
self.run()
File "/usr/lib/python3.8/threading.py", line 870, in run
self._target(*self._args, **self._kwargs)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/utils/autoreload.py", line 64, in wrapper
fn(*args, **kwargs)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/core/management/commands/runserver.py", line 118, in inner_run
self.check(display_num_errors=True)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/core/management/base.py", line 419, in check
all_issues = checks.run_checks(
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/core/checks/registry.py", line 76, in run_checks
new_errors = check(app_configs=app_configs, databases=databases)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/core/checks/urls.py", line 13, in check_url_config
return check_resolver(resolver)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/core/checks/urls.py", line 23, in check_resolver
return check_method()
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/urls/resolvers.py", line 416, in check
for pattern in self.url_patterns:
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/utils/functional.py", line 48, in __get__
res = instance.__dict__[self.name] = self.func(instance)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/urls/resolvers.py", line 602, in url_patterns
patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/utils/functional.py", line 48, in __get__
res = instance.__dict__[self.name] = self.func(instance)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/urls/resolvers.py", line 595, in urlconf_module
return import_module(self.urlconf_name)
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/importlib/__init__.py", line 127, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
File "<frozen importlib._bootstrap>", line 991, in _find_and_load
File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 848, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/edx/app/edxapp/edx-platform/cms/urls.py", line 19, in <module>
from cms.djangoapps.contentstore import views as contentstore_views
File "/edx/app/edxapp/edx-platform/cms/djangoapps/contentstore/views/__init__.py", line 5, in <module>
from .component import *
File "/edx/app/edxapp/edx-platform/cms/djangoapps/contentstore/views/component.py", line 32, in <module>
import openedx.core.djangoapps.content_staging.api as content_staging_api
File "/edx/app/edxapp/edx-platform/openedx/core/djangoapps/content_staging/api.py", line 11, in <module>
from .models import UserClipboard as _UserClipboard, StagedContent as _StagedContent
File "/edx/app/edxapp/edx-platform/openedx/core/djangoapps/content_staging/models.py", line 20, in <module>
class StagedContent(models.Model):
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/db/models/base.py", line 113, in __new__
raise RuntimeError(
RuntimeError: Model class openedx.core.djangoapps.content_staging.models.StagedContent doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS.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.
Nit: did you intentionally decide not to use def setUp(self): (to set self.course_key + self.client there)? I see you're using self.assertEqual instead of a standard assert, so it's a bit odd mix of unittest.TestCase and pytest styles.
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 copied this from the other TestCase, where only some of the tests need this setup, which is why it's not in a setUp method. Here I could put it in a setUp method, if you think that's cleaner. Though if we add more tests here, some may not need the course.
As for mixing assert styles, is there a preferred style for the codebase now? I honestly don't know which I "should" be 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.
I copied this from the other TestCase, where only some of the tests need this setup, which is why it's not in a setUp method. Here I could put it in a setUp method, if you think that's cleaner. Though if we add more tests here, some may not need the course.
No strong preference here. If you think we can have tests that won't need the course setup, then we can keep it as is.
As for mixing assert styles, is there a preferred style for the codebase now? I honestly don't know which I "should" be using.
I don't see any coding guideline for this, but there were 36 PRs like #26576 that replaced these asserts, so I believe that the pytest style is preferred.
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.
OK, I converted this test suite to pytest style.
fa6e990 to
2a9350c
Compare
Agrendalath
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.
👍
- I tested this: verified that pasting works in the devstack
- 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-securerepository: n/a
|
Looking through this more now... |
ormsbee
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.
Looks great! Just a few minor requests.
| class StagedContentData(NamedTuple): | ||
| """ Read-only data model for StagedContent """ | ||
| id: int | ||
| user_id: int | ||
| created: datetime | ||
| purpose: str | ||
| status: StagedContentStatus | ||
| block_type: str | ||
| display_name: str | ||
|
|
||
|
|
||
| class UserClipboardData(NamedTuple): | ||
| """ Read-only data model for StagedContent """ | ||
| content: StagedContentData | ||
| source_usage_key: UsageKey |
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.
Per OEP-49, please make these attr classes in data.py unless you have a strong reason for preferring named tuples (and put comments explaining that decision).
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 like NamedTuples since they're built-in and have all the functionality we need in this case, but it's no problem at all - I'll change this to use attrs and live in data.py for consistency.
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've moved them to data.py though I found some issues with the OEP-49 recommendation regarding enums - openedx/openedx-proposals#471 - so I'm sticking with Django enums for now.
| from opaque_keys.edx.keys import UsageKey | ||
|
|
||
| from .models import UserClipboard as _UserClipboard, StagedContent as _StagedContent | ||
| from .serializers import UserClipboardSerializer as _UserClipboardSerializer |
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.
Interesting way to keep people from importing these from the wrong place. I'm not sure if I prefer it to importing the module and doing models.UserClipboard, but I'm fine with it, fwiw.
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 actually thought this is what OEP-49 recommends? "When importing internal app code to be used in the api.py file, prefix it with an underscore so it’s clear it’s for internal use only." Or are you saying the convention is to use from . import models as _models?
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.
You're right, it's there. I must have just forgotten about that part, because I haven't been writing my code like that. 😛 I'll try to do so going forward.
I actually meant:
from . import modelsSo that all references are to models.UserClipboard. Granted, it doesn't mean people can't re-import it, but it's less likely.
| node = etree.fromstring(olx_str) | ||
| store = modulestore() | ||
| with store.bulk_operations(parent_key.course_key): | ||
| parent_descriptor = store.get_item(parent_key) |
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.
A very big descriptor -> block renaming refactor is dropping shortly. Please make check that your naming aligns:
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.
Hmm, not sure. What I found was that this thing is not a proper XBlock, and it doesn't have a full XBlock runtime, just a CachingDescriptorSystem. So that's why I've called it a descriptor here, and then the line below uses _load_preview_block to convert it from a descriptor to a proper XBlock. I had thought we didn't have to worry about those things anymore but I guess we still do.
So in this case I think the language is clear and necessary but I am not sure it aligns with the changes in that PR. @Agrendalath can you advise?
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.
@ormsbee, @bradenmacdonald, we still use the "descriptor" term in some places. In this case, we are retrieving an "unbound" XBlock from the Modulestore. _load_preview_block handles adding the services and binding student data. Therefore, it is fair to call it a "descriptor", as the only goal of this variable is to pass it to a function that initializes the full 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.
Great, thanks for the quick reply. That aligns with my understanding here :)
d04ba9b to
c4129ce
Compare
|
@bradenmacdonald 🎉 Your pull request was merged! Please take a moment to answer a two question survey so we can improve your experience in the future. |
|
EdX Release Notice: This PR has been deployed to the staging environment in preparation for a release to production. |
|
EdX Release Notice: This PR has been deployed to the production environment. |
Description
This continues the work from #31904 and implements ticket openedx/modular-learning#12
Affects: course authors [for now, only those who will be participating in the beta test]
Screenshot of the "Paste Component" button:
Demo video: coming
Supporting information
See tickets linked above.
Testing instructions
contentstore.enable_copy_paste_featurefor Everyone, if you haven't already.Note: handling of static assets, intra-course links, pythonlib.zip, content groups, and anything along those lines is out of scope for now. Pasting within a course should always work fine. Pasting between courses may have issues if the component uses one or more of these features.
Deadline
"None" if there's no rush, or provide a specific date or event (and reason) if there is one.
Other information
Private ref: MNG-3605