Skip to content

Conversation

@bradenmacdonald
Copy link
Contributor

@bradenmacdonald bradenmacdonald commented Mar 20, 2023

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:

Screenshot 2023-03-20 at 10 51 15 AM

Demo video: coming

Supporting information

See tickets linked above.

Testing instructions

  1. Go to (studio)/admin/waffle/flag/ and enable contentstore.enable_copy_paste_feature for Everyone, if you haven't already.
  2. Open two browser tabs so you can see both at the same time. Ideally, they're in different courses.
  3. Use the new Actions menu to select Copy. You should see an animation and the "Paste Component" button should instantly appear in any open browser tabs.
  4. Try pasting into the same course and different course.
  5. Refresh the page and ensure the "Paste Component" button still appears
  6. Try various combinations of copying and pasting
    • e.g. try copy, then delete the source block, then paste. It should still work!
    • e.g. try within the same course, multiple pastes into the same unit (should get unique IDs), into different courses, etc.

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

@openedx-webhooks openedx-webhooks added open-source-contribution PR author is not from Axim or 2U core committer labels Mar 20, 2023
@openedx-webhooks
Copy link

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.

@bradenmacdonald bradenmacdonald force-pushed the paste-olx branch 2 times, most recently from c1505b2 to 4aa324a Compare April 19, 2023 20:45
@bradenmacdonald
Copy link
Contributor Author

@Agrendalath @ormsbee This is ready for review.

setup.cfg Outdated
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 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.

Copy link
Member

@Agrendalath Agrendalath left a 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.
Edit: nevermind, running `make studio-static` fixed this.

Copy link
Member

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.

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 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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

@Agrendalath Agrendalath left a 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-secure repository: n/a

@ormsbee
Copy link
Contributor

ormsbee commented Apr 26, 2023

Looking through this more now...

Copy link
Contributor

@ormsbee ormsbee left a 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.

Comment on lines 19 to 33
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
Copy link
Contributor

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).

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 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.

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'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
Copy link
Contributor

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.

Copy link
Contributor Author

@bradenmacdonald bradenmacdonald Apr 26, 2023

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?

Copy link
Contributor

@ormsbee ormsbee Apr 26, 2023

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 models

So 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)
Copy link
Contributor

@ormsbee ormsbee Apr 26, 2023

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:

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Contributor Author

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 :)

@bradenmacdonald bradenmacdonald merged commit 8ee1f66 into openedx:master Apr 27, 2023
@bradenmacdonald bradenmacdonald deleted the paste-olx branch April 27, 2023 16:58
@openedx-webhooks
Copy link

@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-pipeline-bot
Copy link
Contributor

EdX Release Notice: This PR has been deployed to the staging environment in preparation for a release to production.

@edx-pipeline-bot
Copy link
Contributor

EdX Release Notice: This PR has been deployed to the production environment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core committer open-source-contribution PR author is not from Axim or 2U

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

6 participants