Skip to content

fix: LP-1102 show all orgs to global staff and course admin orgs when creating a library - #509

Merged
vgulati-apphelix merged 7 commits into
release-ulmofrom
vgulati/LP-1102
Oct 9, 2026
Merged

vgulati-apphelix merged 7 commits into
release-ulmofrom
vgulati/LP-1102

Conversation

@vgulati-apphelix

@vgulati-apphelix vgulati-apphelix commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Description

Fixes the organization dropdown in Studio's "New library" form, which shows too few organizations when ORGANIZATIONS_AUTOCREATE is disabled (as on edx.org stage and prod).

Since openedx#36094, allow_to_create_new_org requires ORGANIZATIONS_AUTOCREATE. When it is off, the Authoring MFE builds the dropdown from allowed_organizations_for_libraries, which only lists organizations where the user has a staff role. As a result:

  1. Global staff see only the organizations they personally hold a role in, although they may create libraries in any organization.
  2. Course Admins (instructor role) never see their course's organization, although _user_can_create_library_for_org already lets them create legacy libraries there.

Changes, behind a new temporary waffle flag contentstore.expanded_library_creation_orgs (off by default):

  • get_allowed_organizations_for_libraries() returns every active organization for global staff. Inactive organizations are left out because ensure_organization() rejects them when autocreate is off, matching Studio's /organizations list.
  • get_organizations_for_non_course_creators() also includes organizations where the user has the instructor role, so Course Admins get the same organizations as Course Staff.

Legacy library create permissions are unchanged, since _user_can_create_library_for_org already allows these orgs. v2 library creation (LibraryRootView.post) authorizes the org with this same list. So with the flag on, a course creator who is not global staff can also create v2 libraries in orgs where they are a Course Admin. This keeps the dropdown and the v2 check in sync. With the flag off, behavior is unchanged.

Roles affected: Course Author (global staff and Course Admins in Studio).

Supporting information

Testing instructions

  1. Set ORGANIZATIONS_AUTOCREATE = False and create a few organizations in Django admin, including one inactive.
  2. Enable the flag at /admin/waffle/flag/ → contentstore.expanded_library_creation_orgs. Course Admins are usually not staff or superusers, so set Everyone = Yes or add them under Users.
  3. As global staff, go to Studio Home → "+ New library" and open the organization dropdown.
    • Flag off: only organizations you hold a role in.
    • Flag on: every active organization; the inactive one is not listed.
  4. As a non-staff Course Admin (not Course Staff) on a course in org X:
    • Flag off: X is not listed.
    • Flag on: X is listed, and "Create legacy library" in X succeeds.
  5. As Course Staff, X is listed with the flag on and off (unchanged).

Unit tests (ddt cases for each role, flag on and off):

  • test_library.py: get_allowed_organizations_for_libraries per role, plus test_legacy_library_org_offered_and_created, which uses stage/prod settings to check that the org is in the home API dropdown and that POST /library/ creates the legacy library.
  • test_home.py: the home API returns all active organizations to global staff.

Other information

  • Rollout: enable the flag in stage, verify, then prod. A new waffle flag row has Superusers checked by default, so superusers get the new behavior as soon as the row is created. The flag is temporary (target removal 2027-01-07); after that the flag-on behavior becomes the only path.
  • Out of scope:
    • Creating new (v2) libraries still requires global staff or course creator status (CAN_CREATE_CONTENT_LIBRARY).
    • With ENABLE_ORGANIZATION_STAFF_ACCESS_FOR_CONTENT_LIBRARIES off, the dropdown still comes only from the user's course-creator grant (unchanged). edx.org uses the default, True.
  • Test fix: HomePageLibrariesViewTest hard-coded the OrganizationFactory sequence name (name0). The new tests create organizations earlier in the module, so it now uses the created library key.
  • Upstream: an equivalent openedx PR (without the temporary flag) will follow.

🤖 Generated with Claude Code

Since allow_to_create_new_org started requiring ORGANIZATIONS_AUTOCREATE
(openedx#36094), the Authoring MFE picks the org list from
allowed_organizations_for_libraries whenever autocreate is disabled.
That list only includes orgs where the user has a course
creator grant or a staff CourseAccessRole. Global staff therefore saw
only a few orgs in the New Library dropdown, and an org typed by hand is rejected.
The library create API (server side) lets global staff use any org.

get_allowed_organizations_for_libraries() now returns every organization
when user.is_staff is set (the same check as GlobalStaff and
user_can_create_organizations). Course staff, course admins and course
creators still get only their role-based orgs.
@vgulati-apphelix vgulati-apphelix changed the title Vgulati/lp 1102 fix: LP-1102 show all orgs to global staff and course admin orgs when creating a library Oct 7, 2026
…raries

Behind the contentstore.expanded_library_creation_orgs flag:
- Global staff are offered only active organizations, since
  ensure_organization() rejects inactive ones when autocreate is off.
- Course admins (instructors) are offered their courses' organizations,
  as course staff already are.

Update the toggle description and add tests for both cases.
Comment thread cms/djangoapps/contentstore/views/course.py Outdated
Comment thread cms/djangoapps/contentstore/views/tests/test_library.py Outdated
Comment thread cms/djangoapps/contentstore/views/tests/test_library.py Outdated

@abhalsod-sonata abhalsod-sonata left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks correct and CI is green. It reuses the existing role helpers (OrgInstructorRole.get_orgs_for_user next to the existing OrgStaffRole call) and keeps everything behind the flag. The inline comments are mostly about trimming the diff. One thing to fix in the description:

v2 library creation does change server-side

The description says "Server-side create permissions are unchanged; only the dropdown list changes." That holds for legacy libraries, but not for v2. LibraryRootView.post (openedx/core/djangoapps/content_libraries/rest_api/libraries.py) authorizes the org with the same helper:

if not (
    user_can_create_organizations(request.user) or
    org_name in get_allowed_organizations_for_libraries(request.user)
):
    raise ValidationError(...)

With edx.org settings (ENABLE_CREATOR_GROUP=True, ENABLE_ORGANIZATION_STAFF_ACCESS_FOR_CONTENT_LIBRARIES=True) and the flag on, take a course creator who is not global staff and who is a Course Admin (only the instructor row) on a course in org X. That user can now create a v2 library in X. With the flag off, they get 400 "User not allowed to create libraries in 'X'".

I think this is the right behavior. Admin is a superset of Staff, and the change keeps the dropdown and the v2 check in sync, so the dropdown never offers an org that v2 create then rejects. It just needs to be stated. Suggested replacement for that sentence:

Legacy library create permissions are unchanged, since _user_can_create_library_for_org already allows these orgs. v2 library creation (LibraryRootView.post) authorizes the org with this same list. So with the flag on, a course creator who is not global staff can also create v2 libraries in orgs where they are a Course Admin. This keeps the dropdown and the v2 check in sync. With the flag off, behavior is unchanged.

@vgulati-apphelix

Copy link
Copy Markdown
Member Author

Code looks correct and CI is green. It reuses the existing role helpers (OrgInstructorRole.get_orgs_for_user next to the existing OrgStaffRole call) and keeps everything behind the flag. The inline comments are mostly about trimming the diff. One thing to fix in the description:

v2 library creation does change server-side

The description says "Server-side create permissions are unchanged; only the dropdown list changes." That holds for legacy libraries, but not for v2. LibraryRootView.post (openedx/core/djangoapps/content_libraries/rest_api/libraries.py) authorizes the org with the same helper:

if not (
    user_can_create_organizations(request.user) or
    org_name in get_allowed_organizations_for_libraries(request.user)
):
    raise ValidationError(...)

With edx.org settings (ENABLE_CREATOR_GROUP=True, ENABLE_ORGANIZATION_STAFF_ACCESS_FOR_CONTENT_LIBRARIES=True) and the flag on, take a course creator who is not global staff and who is a Course Admin (only the instructor row) on a course in org X. That user can now create a v2 library in X. With the flag off, they get 400 "User not allowed to create libraries in 'X'".

I think this is the right behavior. Admin is a superset of Staff, and the change keeps the dropdown and the v2 check in sync, so the dropdown never offers an org that v2 create then rejects. It just needs to be stated. Suggested replacement for that sentence:

Legacy library create permissions are unchanged, since _user_can_create_library_for_org already allows these orgs. v2 library creation (LibraryRootView.post) authorizes the org with this same list. So with the flag on, a course creator who is not global staff can also create v2 libraries in orgs where they are a Course Admin. This keeps the dropdown and the v2 check in sync. With the flag off, behavior is unchanged.

Agreed. Updated the description to call out the v2 behavior.

@vgulati-apphelix
vgulati-apphelix merged commit 21023a5 into release-ulmo Oct 9, 2026
64 checks passed
@vgulati-apphelix
vgulati-apphelix deleted the vgulati/LP-1102 branch October 9, 2026 06:07
robrap added a commit that referenced this pull request Oct 9, 2026
Batch 1 of the ulmo upgrade [LP-1148]: bring back upstream release/ulmo.1
(Django 5.2) by reverting the May 27 revert, on the current release-ulmo.

Conflict resolutions (13 files):
- .github/workflows/check-for-tutorial-prs.yml: keep deleted (#507).
- create_user_gdpr_testing.py: keep deleted (fork removal, ENT-11576).
- xmodule/js/src/video/02_html5_hls_video.js: accept upstream's move to
  xmodule/assets/video/public/js/; the fork's HLS retry change is ported
  in a separate commit.
- contentstore/views/course.py: keep #509's LP-1102 staff branch and toggle
  import, then upstream's set-based org logic; drop the removed
  use_new_home_page / use_new_course_outline_page imports.
- modulestore_migrator/models.py: keep the fork's `.. no_pii:` annotations
  (#352).
- test_content_libraries.py: import both CourseKey (#509) and
  LibraryCollectionLocator (ulmo.1).
- user_api accounts test_api.py: keep #517's timezone/pytz imports and
  ulmo.1's ZoneInfo.
- course_duration_limits test_access.py: keep the fork's RequestCache
  clearing, with ulmo.1's ZoneInfo.
- requirements/common_constraints.txt: keep the fork's pip<26.2.1 and
  social-auth constraints (supersede ulmo.1's pip<25.3).
- requirements/edx/*.txt: lti-consumer-xblock 9.14.3 (ulmo.1), ora2 6.17.2
  (fork; it no longer needs loremipsum).

Co-Authored-By: Claude <noreply@anthropic.com>
robrap added a commit that referenced this pull request Oct 9, 2026
Batch 1 of the ulmo upgrade [LP-1148]: bring back upstream release/ulmo.1
(Django 5.2) by reverting the May 27 revert, on the current release-ulmo.

Conflict resolutions (13 files):
- .github/workflows/check-for-tutorial-prs.yml: keep deleted (#507).
- create_user_gdpr_testing.py: keep deleted (fork removal, ENT-11576).
- xmodule/js/src/video/02_html5_hls_video.js: accept upstream's move to
  xmodule/assets/video/public/js/; the fork's HLS retry change is ported
  in a separate commit.
- contentstore/views/course.py: keep #509's LP-1102 staff branch and toggle
  import, then upstream's set-based org logic; drop the removed
  use_new_home_page / use_new_course_outline_page imports.
- modulestore_migrator/models.py: keep the fork's `.. no_pii:` annotations
  (#352).
- test_content_libraries.py: import both CourseKey (#509) and
  LibraryCollectionLocator (ulmo.1).
- user_api accounts test_api.py: keep #517's timezone/pytz imports and
  ulmo.1's ZoneInfo.
- course_duration_limits test_access.py: keep the fork's RequestCache
  clearing, with ulmo.1's ZoneInfo.
- requirements/common_constraints.txt: keep the fork's pip<26.2.1 and
  social-auth constraints (supersede ulmo.1's pip<25.3).
- requirements/edx/*.txt: lti-consumer-xblock 9.14.3 (ulmo.1), ora2 6.17.2
  (fork; it no longer needs loremipsum).

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants