Repository navigation
Conversation
The new instructor dashboard had no way to generate the proctored exam results report that the legacy Data Download tab offers. - Accept a proctored_exam_results report type in GenerateReportView, using the EXAM_RESULTS permission like the v1 endpoint. - Detect the report's filename in ReportDownloadsView. - Expose proctored_exams_enabled in the course metadata response so the MFE can show the report only when the legacy dashboard would.
instructor_dashboard.py goes away with the legacy dashboard removal, so keep the shared helper in views/tools.py. Also rename the test course without proctoring from proctored_course to other_course.
openedx-forum 0.6.0 adds the ModerationAuditLog model with no PII annotation, so `make pii_check` fails on this upgrade. The model also stores post bodies that aren't cleared on user retirement. Hold forum back so the rest of the requirements upgrade can land. openedx/forum#301 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on-requirements-03698e2 chore: Upgrade Python requirements
The verification_status and verifications endpoints look the target user up from the `username` in the URL path, but were guarded by IsStaffOrOwner. That class passes when the caller's username equals any of `?username=`, request.data['username'] or request.data['user'], none of which are tied to the path. An anonymous caller (username '') passed with an empty `?username=`, and a logged-in user passed with their own name, so either could read any user's verification status and attempt history. Switch both views to (IsAuthenticated, IsStaff | IsUserInUrl), which checks only the URL username. IsStaffOrOwner is left alone: the credit views rely on its client-supplied clauses, because there the target comes from the same value that is checked. Add regression tests, shared by both endpoints, for the anonymous empty-`?username=` case and the non-owner-with-own-`?username=` case. GHSA-fpx7-53j2-fc79 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The legacy discussion AJAX views are guarded by `permitted`, which checks the caller's forum role against the course in the URL but never checks that the thread or comment named in the URL belongs to that course. A user holding a role in course A could act on course B's content by sending the request through A's URL: edit, delete, close, pin or endorse with a moderator-level role, and reply, vote, flag or follow with just the Student role every enrolled learner has. The thread read view has the same gap. `single_thread` (and the discussion board fragment, which shares `_find_thread`) checks enrollment and the topic against the course in the URL, then loads the thread by id without comparing its course. A learner enrolled in any course could read another course's thread and its responses. The loaded content already carries its own `course_id`, so `permitted` and `_find_thread` now reject it when it differs from the course in the URL (401 and 404 respectively). The check is skipped when the payload has no `course_id`, which covers Commentable content (it has no course) and test mocks. Add CrossCourseViewPermissionsTestCase, covering moderator-level and learner-level routes for both threads and comments, plus controls that still pass (own course, and a moderator using their own course's URL). Add SingleThreadCrossCourseTestCase for the read path, with a control for a thread from the user's own course. Existing tests changed, and why: `make_minimal_cs_thread` defaults the thread's `course_id` to `course-v1:dummy+dummy+dummy`, but these tests call the views through the real test course's URL. They were therefore exercising a thread from one course being acted on through another course's URL, which is the case this change now rejects. They only passed because nothing compared the two. The fixtures now give the thread the same course as the URL, so the tests exercise the behavior they were written for. The assertions are unchanged. - ThreadActionGroupIdTestCase.call_view (6 tests) - ForumEventTestCase.test_thread_voted_event (the thread cases) The new read-path test also sets `course_id` on its mocked thread, because `make_mock_thread_data` leaves it out and the check would otherwise be skipped. GHSA-3gx3-8wpj-4qc2 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Enhanced Staff Grader (ESG) proxied its ORA handler calls through `handle_xblock_callback_noauth`, which skips CSRF checks and the block's `load` access check while keeping the session user. ESG only needed the CSRF skip: it turns every call into an internal POST, including calls made while serving its GET endpoints. Call `handle_xblock_callback` instead and mark the internal request as already CSRF-checked. The ESG view has already authenticated the user, and its DRF session authentication enforces CSRF on its own unsafe methods. The handlers now also go through the normal `load` check. All of the ESG handlers require course staff in ORA, and course staff always pass `load`, so this does not refuse anyone ORA accepts. This frees the noauth view to be restricted to unauthenticated callers. Advisory: GHSA-863f-jpf7-cqhq Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`handler_noauth` exists so callers without a session can reach a few XBlock handlers: LTI 1.1 tools posting grades back (they verify an OAuth1 signature) and the mobile apps fetching video transcripts. The view accepted any handler name, though, and skips the block's `load` access check, so an anonymous caller could run any handler on any block, including blocks hidden by staff-only visibility, content groups or content type gating. Add the `XBLOCK_NOAUTH_HANDLER_ALLOWLIST` setting: dotted paths to the handler methods that may be called on this URL. Each path is imported and compared to the method the block would actually run, so operators can add their own third-party callbacks. Entries that can't be imported are skipped with a warning. Any other handler gets a 404 before it runs. The defaults cover the built-in and extracted LTI blocks, the lti_consumer LTI 1.1 outcome and result services, and the video transcript handler. Advisory: GHSA-863f-jpf7-cqhq Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`handle_xblock_callback_noauth` only marked the session user as unknown to skip the `load` check; a logged-in caller kept their real user id, so the handler loaded and saved that user's state. Combined with the view being CSRF-exempt (it serves LTI tool servers, which can't send a token) and the session cookie being `SameSite=None` in production, this let: * a logged-in learner run and be graded on blocks they can't load, and * any other site make a visiting learner's browser call handlers as them. Replace the request user with `AnonymousUser` on this view. None of the allowed handlers use the session user: the LTI handlers find the learner from the signed `sourcedId`, and transcripts are public. Advisory: GHSA-863f-jpf7-cqhq Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`handle_xblock_callback` allows anonymous GET requests (for transcripts) and set `request.user.known` to False for them, which skips the block's `load` access check. An anonymous GET could therefore run a handler on a block hidden by staff-only visibility, content groups or content type gating, for example `xmodule_handler/problem_get` to read a restricted problem. Stop marking anonymous users as unknown here, so the `load` check runs for them like it does for logged-in users. Blocks an anonymous user can load, such as transcripts in a public course, keep working. Advisory: GHSA-863f-jpf7-cqhq Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`show_reference_template` renders any Mako template in the service's lookup path with `request.GET` as its context. It is meant for development (its docstring says it 404s in production), but both the LMS and CMS URLconfs registered `/template/<path:template>` unconditionally. Because edxmako does not HTML-escape by default, any template that prints a context variable raw reflected attacker input, giving an unauthenticated reflected XSS on both the LMS and Studio origins. Register the route only when `settings.DEBUG` is true, in both URLconfs, so production deployments do not expose it at all. The new tests live in `openedx/core/djangoapps/debug/`, which CI runs under both LMS and CMS settings, so they cover both URLconfs. Advisory: GHSA-hh7c-hrv9-6qw9 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`cert_html_view_overrides` is a course Advanced Setting, so any course
author can set it, and it is merged last into the certificate web view
context, where it can replace any key. Two sinks rendered those values
without escaping: the footer printed `copyright_text` with `| n`, and
custom certificate templates (`CUSTOM_CERTIFICATE_TEMPLATES_ENABLED`)
were rendered with no default HTML filter at all. That let a course
author store script that runs on the LMS origin for anyone who opens one
of the course's certificates.
This escapes at the sinks rather than at the source:
- Custom certificate templates now render with
`default_filters=['decode.utf8', 'h']`, so every `${...}` is
HTML-escaped while the template's own markup is left alone.
- The footer renders `${copyright_text}` rather than
`${copyright_text | n, decode.utf8}`.
- The default copyright text uses a literal `©` rather than `©`, so
it still renders correctly once escaped.
Escaping cannot stop a `javascript:` URL from running when it is used as
a link, because such a URL contains no characters that escaping touches,
and no template filter in Django or Mako validates a URL scheme. So
overrides whose value parses as a script-executing URL are dropped in
the view and the configured value is used instead.
Overrides are otherwise passed through to the context untouched.
Escaping them there would corrupt URLs rendered into JavaScript strings,
and would coerce non-string overrides into strings, breaking templates
that read them as booleans or mappings.
This also removes a stray `"` from `certificate_verify_description`,
which ended with `"detached signature""".`, a leftover from the
JSON defaults the string was converted from. The value is HTML-escaped
when rendered, so the entity appeared literally on every certificate.
This changes a translatable string, so its existing translations will
need to be refreshed.
BREAKING CHANGE: certificate context values are now HTML-escaped
wherever they are rendered.
- Custom certificate templates no longer render markup that comes from a
context value. An operator who deliberately renders markup must opt
out per expression with Mako's `n` filter, e.g. `${value | n}`.
- Custom certificate templates must quote any attribute that renders a
context value, e.g. `<a href="${company_tos_url}">` rather than
`<a href=${company_tos_url}>`. Escaping does not touch spaces, so in
an unquoted attribute a value can still add an event handler.
- `copyright_text` is no longer exempt from escaping. An operator or
course author who set markup or HTML entities in it, via
`CertificateHtmlViewConfiguration` or via a course's
`cert_html_view_overrides`, will now see them rendered literally. Use
the characters themselves, for example `©` rather than `©`.
- A certificate override whose value is a `javascript:` or `vbscript:`
URL is now ignored and logged, and the configured value is used.
- The `org.openedx.learning.certificate.render.started.v1` filter
(`CertificateRenderStarted`) is affected in two ways: a context value
that a filter implementation sets or changes is now escaped wherever it
is rendered, and a `custom_template` that a filter returns is now
rendered with escaping. A filter that deliberately injects markup
should wrap the value in `openedx.core.djangolib.markup.HTML()`, which
passes through escaping untouched, or the template should opt out with
`${value | n}`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lts-report feat: add proctored exam results report to instructor v2 reports API
Regenerated via `make upgrade-package package=openedx-core`. Picks up openedx/openedx-core#913 (PATCH endpoint for a competency criteria group's logic_operator, openedx/openedx-core#760), first released in 1.18.0, so the Competency-Based Education (CBE) work can go to QA.
…enterprise Every hookable caller of openedx/features/enterprise_support/ has already been migrated behind filters, signals, and pluggable overrides, and the package itself has been moved into edx-enterprise as enterprise.platform_support. Drop the platform copy. - Delete openedx/features/enterprise_support/ in full. - Remove EnterpriseSupportConfig from INSTALLED_APPS in lms/envs/common.py. - Replace lms/urls.py's enterprise_support import with a module-private _enterprise_enabled(), keeping both halves of the original check. The conditional block and both of its includes (enterprise.urls and channel_integrations.urls) are unchanged; the helper is documented as temporary, to be removed once enterprise-integrated-channels becomes a plugin that injects its own URLs. - Delete the vestigial test_update_success_for_enterprise from the user_api accounts API tests. Ticket ENT-11510 moved readonly-field enforcement behind AccountSettingsReadOnlyFieldsRequested, and the filter test immediately below it already covers the real contract. - Relocate the package's EnterpriseLogoutTests to openedx/core/djangoapps/user_authn/views/tests/test_logout.py. It covers LogoutView._is_enterprise_target and the logout.html branch keyed on it, both of which stay in the LMS, and no other test in the repo covers them -- so deleting the package wholesale would drop their only coverage. The original's enterprise API mocks, customer fixtures and learner factory are not carried over: _is_enterprise_target is a pure regex over the redirect path and consults nothing in the enterprise app. - Drop the openedx.features.enterprise_support.utils import-linter contract from pyproject.toml. - Raise the edx-enterprise pin to 9.0.0, the release that ships enterprise.platform_support. - Refresh the stale enterprise_support reference in ADR 0010. ENT-11576
webpack.prod.config.js requires terser-webpack-plugin, but package.json never declared it; it resolved only because webpack depended on it and npm hoisted it to the top of node_modules. webpack 5.112 replaced that dependency with minimizer-webpack-plugin, so any lockfile refresh that picks up a newer webpack breaks the production build with "Cannot find module 'terser-webpack-plugin'". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Delete and re-resolve package-lock.json from package.json with `npm install --package-lock-only` so transitive dependencies move to the latest versions package.json already allows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nedx feat: delete the enterprise_support package now that it lives in edx-enterprise
…ge-lock fix: regenerate package-lock.json
Add org-tier resolution helpers to CourseWaffleFlag so callers can ask whether the flag is enabled for a whole org, and enumerate the orgs and courses that carry a force-on override. - is_enabled_for_org(org): resolve the flag at the org tier (org override takes precedence, else the global switch), sharing the per-request cache key with is_enabled via a common _get_org_override_value helper. - get_force_on_orgs() / get_force_on_course_keys(): return the orgs and course keys whose current override choice forces the flag on, so a consumer can expand a broad grant to exactly the scopes the flag resolves on when the global switch is off. These are consumed by the authz grant->scope resolution added in a following commit; they live here because org-tier resolution and the force-on enumeration are properties of the waffle flag, not of any one consumer.
Add openedx/core/djangoapps/authz/scopes.py: a general, consumer-agnostic resolver that translates a user's openedx-authz grants for a given action into the concrete orgs and course keys those grants enable under the AUTHZ_COURSE_AUTHORING_FLAG roll-out state. Every helper takes the permission action as a parameter, so the module does not know which permission a given consumer cares about: - get_cached_scopes_for_permission(user, action): one enforcer round-trip per (user, action), request-cached, keyed on both arguments. - get_authz_flag_enabled_course_keys / get_authz_flag_enabled_org_keys: per-course and org-glob grants where the flag resolves on at that tier. - user_has_platform_grant / authz_grants_platform_access: a platform-wide (course-v1:*) grant, and whether it means unrestricted access (grant AND global switch on). - get_authz_platform_orgs / get_authz_platform_course_keys: when the global switch is off, expand a platform grant to just the force-on override orgs/courses. - get_authz_org_scoped_course_keys: expand an org-wide grant, when its org flag is off, to the force-on override courses inside that org (skipping courses whose org is itself flag-on). This mirrors how the Studio authoring-home listing resolves org grants at course granularity. Helpers raise on lookup failure rather than swallowing errors; the fail-open decision belongs to the consumer. Tests seed real role assignments into the enforcer and assert the resolved scopes.
Base the Studio search access filter on a user's openedx-authz view_course grants, in addition to the existing legacy course/org roles, so an authz-granted user finds exactly the content they may view. - models.py: add search adapters over the authz.scopes helpers (_get_authz_*_for_search) that apply the search-specific concerns the general helpers leave out -- drop omit_orgs, serialize keys to strings for set-union de-dup against the legacy-role branch, and fail open on a DatabaseError so search degrades to legacy access rather than 500-ing. VIEW_COURSE_ACTION (the choice that view_course means "visible in search") is defined here, with the consumer, and passed into every scopes call. get_access_ids_for_request unions per-course grants, the platform force-on course expansion, and the org-scoped course rescue. - api.py: _get_meili_access_filter drops the filter entirely for a platform grant when the flag is globally on (authz analogue of global staff); otherwise _get_user_orgs contributes flag-on orgs to the "org IN [...]" clause and get_access_ids_for_request contributes the individual courses to the "access_id IN [...]" clause. Tests cover the adapter behavior (omit_orgs, serialization, fail-open) and the assembled filter through the token endpoint.
…ry filters Mint two independent access filters for the Studio search token endpoint: course visibility from view_course grants bound to the course index, library visibility from view_library grants bound to the library index, instead of applying one course-derived filter to both. Each filter is the union of legacy roles and authz grants, so a user retains view access if either system grants it. The library resolvers carry no authoring waffle flag (AUTHZ_COURSE_AUTHORING_FLAG is a CourseWaffleFlag that cannot resolve a LibraryLocatorV2), so a view_library grant is authoritative on its own with no per-scope flag gate. A platform-wide grant now short- circuits only its own index: a course-v1:* grant empties the course filter, a lib:* grant the library filter.
Regenerated via `make upgrade-package package=openedx-core`. Picks up openedx/openedx-core#900 (one root criteria group per competency, enforced in the database by migration openedx_learning 0017, openedx/openedx-core#872), first released in 1.19.0, and openedx/openedx-core#911 (`course_keys` scoping on the criteria-groups read endpoint, openedx/openedx-core#682), released in 1.20.0, so the Competency-Based Education (CBE) work can go to QA. Related to openedx/openedx-core#872 and openedx/openedx-core#682 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 10.3.1 to 10.3.3. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](astral-sh/setup-uv@18d369b...68559e9) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: 10.3.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
…ns/astral-sh/setup-uv-10.3.3 build(deps): bump astral-sh/setup-uv from 10.3.1 to 10.3.3
…openedx#39179) When a learner's subsection grade is persisted, the LMS now also updates the learner's competency criterion status in the same database transaction, using record_graded_object_statuses from openedx-core. The grade and the status commit or roll back together, so a learner is never shown a grade without the mastery it earned. When at least one criterion status changes, the roll-up of the levels above the criterion is queued after commit as the roll_up_competency_statuses_for_user task, which calls roll_up_competency_statuses from openedx-core. A failed enqueue is logged at error level with the user id and object ids, so support can recompute the statuses. A grade that staff enter in the gradebook counts toward competency status even when the learner never attempted the subsection. The behavior is gated by the new ENABLE_COMPETENCY_MASTERY_TRACKING setting, which defaults to False. Bumps openedx-core from 1.20.0 to 1.21.0, regenerated via `make upgrade-package package=openedx-core`. That release contains both openedx/openedx-core#862 (record_graded_object_statuses) and openedx/openedx-core#870 (roll_up_competency_statuses), which this change imports at module level. Related to openedx/openedx-core#701 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e-country-database-41268412679 Update GeoLite Database
pytest 9.2.0 renamed the private FixtureManager._arg2fixturedefs to _arg2node2fixturedefs as part of the fixture-resolution rework in pytest-dev/pytest#14635. pylint-pytest 1.1.9.post1 still reads the old name in its pytest_sessionfinish hook, so with pytest 9.2.0 every file pylint checks fails with F0002 (astroid-error): AttributeError: 'FixtureManager' object has no attribute '_arg2fixturedefs'. Did you mean: '_arg2node2fixturedefs'? That is why all seven pylint jobs went red on the requirements bump that moved pytest 9.1.1 -> 9.2.0. The fix is upstream in pylint-dev/pylint-pytest#164 (approved, unreleased), tracked by pylint-dev/pylint-pytest#163. Constrain pytest to <9.2.0 in [tool.edx_lint].uv_constraints and regenerate with `make compile-requirements`; only pytest moves. Removing the constraint once a pylint-pytest release includes that fix is openedx#39337. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…on-requirements-f2fc446 chore: Upgrade Python requirements
Django 5.2.19 security release: fixes multipart base64 upload memory use, Vary cache-key collisions, admin password hash visibility for view-only users, and GeoDjango/GDAL issues. See https://www.djangoproject.com/weblog/2026/nov/03/security-releases/ Commit generated by workflow `openedx/openedx-platform/.github/workflows/upgrade-one-python-dependency.yml@refs/heads/master` Co-authored-by: robrap <14576445+robrap@users.noreply.github.com>
* build: pin JS tests to ubuntu-24.04 The xmodule Karma video specs time out on ubuntu-26.04, which ubuntu-latest is migrating to by 2026-11-19. Pin to 24.04 until the specs are fixed. See openedx#39346. Co-Authored-By: Claude <noreply@anthropic.com> * build: pin runs-on instead of matrix.os so the job name is unchanged Keep matrix.os as ubuntu-latest so the JS job keeps its current name (and any required check on it), and pin runs-on to ubuntu-24.04. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
Reverts openedx#39055 while the interaction between course and library permissions remains under discussion.
…#39333) The page shown after a learner confirms an email change said the new address is visible on the dashboard, which never shows it. Link to the Account MFE (honouring site configuration) where the email is shown.
* feat: mathjax upgrade v2 to v3 - Pin CDN URLs to mathjax@4.1.2 - Fix mathjax_delay_renderer.js immediate path to pass DOM node - Add null guard in capa/display.js for missing math element - Fix CSS syntax: comma to semicolon in MathJax SVG rules - Pass textContent instead of innerHTML for security * fix(mathjax): use chtml output * fix(mathjax): support v4 formula previews * fix(mathjax): render formula previews with v4 - mathjax_delay_renderer.js: add typesetClear before .html() in both immediate and delayed paths, so MathJax v3/v4 re-renders replaced content - capa/display.js refreshMath: replace v2 Hub.Queue/getAllJax with v3/v4 typesetClear/typesetPromise; add typesetClear before setting preview textContent; unify rendering to AsciiMath/backtick for all non-tex-delimited input (fixes formula preview for formulaequationinput where * → multiplication and / → fraction instead of literal chars) - capa/display_spec.js: add tests for calculator syntax backtick wrapping, TeX delimiter preservation, and empty-input early return * temp: fix * fix(mathjax): enable extracted xblocks for v4 * fix(mathjax): stabilize v4 xblock rendering * fix(mathjax): restore dollar delimiters * fix(mathjax): remove redundant dollar delimiter config and detection Dollar delimiters ($...$, $$...$$) are MathJax 3 defaults, so explicit config is redundant. Remove them from config, inline detection, and tests, and simplify the tag-detection logic (no remaining self-matching pairs). * fix: remove invalid settings * chore: apply suggestion BREAKING CHANGE: The platform now loads MathJax 4.1.2 with CHTML output in place of 2.7.5 with SVG output. Custom themes that style the MathJax_SVG output classes and XBlocks or other frontend code that call the MathJax.Hub API stop rendering math until they move to the MathJax 4 API (window.MathJax config and typesetPromise) and the mjx-container output.
…uthz fix: apply openedx-authz manage_tags orgs consistently across tagging rules
…openedx#39355) Fixes the RTD build failure that surfaced on the xblocks-core upgrade PR [1]: django.core.exceptions.ImproperlyConfigured: The LOCALE_PATHS setting must be a list or a tuple. docs/docs_settings.py called into app code (get_api_docs_settings()), which reads django.conf.settings, which points right back at docs_settings. Django then tried to configure itself from the half-loaded module, where LOCALE_PATHS was still an unresolved Derived, and rejected it. This only worked before [1] by accident: xblocks-contrib's Django app plugin entry point loaded every xblock at settings-import time, and one of them touched django.conf.settings early enough to paper over the problem. xblocks-core's entry point doesn't, so the accident went away. Quick fix: use the API_DOCS_SETTINGS constant directly instead of calling get_api_docs_settings(). The real fix is that settings shouldn't import app code at all; the defaults should live in settings, and apidocs.py should import them. Draft refactoring, open for discussion: [2] [1] openedx#38473 [2] openedx#39356 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
xblocks-contrib has been renamed to xblocks-core. Its 2.0.0 release moves each built-in XBlock out of the shared xblocks_contrib namespace and into its own top-level package (xblock_problem, xblock_video, ...). Swap the dependency for xblocks-core 2.0.0 from PyPI, and point imports of the extracted blocks at the new xblock_* modules. - xblocks_contrib.poll becomes xblock_poll_question, matching its entry point and <poll_question> tag; xblock_poll is OpenCraft's newer poll XBlock. - Point XBLOCK_NOAUTH_HANDLER_ALLOWLIST at the new LTI and video handler paths. - Update docs that referred to xblocks-contrib. BREAKING CHANGE: If you override XBLOCK_NOAUTH_HANDLER_ALLOWLIST, replace the old LTI and video handler entries with their new paths, or LTI 1.1 grade passback and video transcript downloads will stop working: Remove: xblocks_contrib.lti.lti.LTIBlock.grade_handler xblocks_contrib.lti.lti_2_util.LTI20BlockMixin.lti_2_0_result_rest_handler xblocks_contrib.video.video_handlers.VideoStudentViewHandlers.transcript Add: xblock_lti.block.LTIBlock.grade_handler xblock_lti.lti_2_util.LTI20BlockMixin.lti_2_0_result_rest_handler xblock_video.video_handlers.VideoStudentViewHandlers.transcript The lti_consumer entries are unchanged. Part of: openedx/xblocks-core#237 Part of: openedx/xblocks-core#149 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjwKFzQ6Tfh5HJ5xzgmGKr
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjwKFzQ6Tfh5HJ5xzgmGKr
… tag import Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjwKFzQ6Tfh5HJ5xzgmGKr
…axonomies are skipped Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjwKFzQ6Tfh5HJ5xzgmGKr
…lock cases Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjwKFzQ6Tfh5HJ5xzgmGKr
… tag import contract Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjwKFzQ6Tfh5HJ5xzgmGKr
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjwKFzQ6Tfh5HJ5xzgmGKr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Studio's
content_taggingapp now respects the archived state that openedx-core gives taxonomies, tags, and tag associations. Three places change: the taxonomy list'stags_countleaves archived tags out;get_all_object_tags, the one read behind the tag export, block duplication, clipboard copy, and library containers, returns only associations that archiving does not hide; and course tag import keeps the associations an author cannot see, and skips tags from a taxonomy that is archived on this instance instead of creating placeholders for them.Why
openedx-core hides archived records on its read paths (openedx/openedx-core#778), and keeps an archived association when
tag_object()replaces an object's tags (tbain/openedx-core#5). Studio'scontent_taggingapp counts tags, reads associations, and imports course tags with its own queries, so without this change an author would still see archived tags in Studio's taxonomy counts and would carry archived associations along when exporting, duplicating, copying, or importing content. openedx/openedx-core#903 describes the gap and the acceptance criteria.What changed
TaxonomyOrgView.get_queryset(rest_api/v1/views.py) annotatestags_count=Count("tag", filter=Q(tag__archived=False), distinct=True), the same expression openedx-core'sTaxonomyViewuses. One annotation covers the no-filter, org, and unassigned branches.get_all_object_tags(api.py) starts its query fromObjectTag.objects.unarchived(). An association is left out when it is archived, or when its tag or taxonomy is archived. Because the CSV header is built from the returned rows, an archived taxonomy drops out of the header too. Associations under a disabled taxonomy are not hidden by archiving and still travel as before. Nothing changes at the call sites ingenerate_csv_rows,copy_object_tags,openedx/core/lib/xblock_serializer/block_serializer.py, orcontent_libraries/api/serializers.py.set_exported_object_tags(api.py):ObjectTag.objects.filter(object_id=...).unarchived().delete()in place ofdelete_object_tags, so an association hidden by archiving survives a re-import into the same course. Placeholders, associations whose tag was deleted, and associations under a disabled taxonomy are still cleared and re-created from the file. The clear stays a querysetdelete(), so openedx-core's override still archives a deletion-locked association instead of deleting it.Taxonomy.objects.filter(export_id=...).first(), becauseget_taxonomy_by_export_idreturnsNonefor an archived taxonomy and a missing one alike. A live taxonomy is tagged as before. An archived taxonomy is skipped, with alog.infonaming the content key, export id, and skipped values, and with no placeholder and no events; a placeholder would be wrong becauseObjectTag.resync()would re-link it to the archived taxonomy. A missing taxonomy still gets placeholders.content_tagging/tests/test_api.py,rest_api/v1/tests/test_views.py, and two extended cms tests (test_duplicate_tags,test_copy_and_paste_unit_with_tags). One pre-existing test,test_get_course_object_tags_with_add_tags, now compares one block's value lists order-insensitively:get_all_object_tagsorders rows byobject_idonly, so order within a taxonomy's values was always database order, and the extra joins fromunarchived()changed it.Who is affected
get_all_object_tagsno longer returns archived associations.set_exported_object_tagsno longer clears them either; usedelete_object_tagsfor a full clear.Supporting information
requirements/edx/base.txtpinsopenedx-core==1.20.0, which has none ofTag.archived,ObjectTag.objects.unarchived(), or the archive-awaredelete(). This PR was developed and tested with the openedx-core branch mounted editable in tutor dev. The pin bump is not part of this PR; the regular upgrade will pick it up once a release contains the stack above.Testing instructions
Automated, in a tutor dev CMS container with openedx-core mounted from the
tbain/903_keep_hidden_object_tags_on_savebranch:The new and changed tests, two commands (pytest keeps only the last
-k, so the Studio tests run separately):The whole package:
pytest --ds=cms.envs.test openedx/core/djangoapps/content_tagging/(all pass; 653 tests on master as of 2026-11-09).pylinton the touched files,ruff check openedx/core/djangoapps/content_tagging/, andmypy openedx/core/djangoapps/content_tagging/, withDJANGO_SETTINGS_MODULE=cms.envs.testfor pylint and mypy.Manual, in Studio with the same openedx-core branch. Nothing in the product archives a row yet, so archive one in a Django shell (
./manage.py cms shell):from openedx_tagging.models import Tag; Tag.objects.filter(taxonomy__name="<your taxonomy>", value="<a tag>").update(archived=True). Then:tags.csvhas no cell with that value.ObjectTag.objects.filter(...)in the shell shows it).Deadline
None.
Other information
get_all_object_tagsoutput, and so within atags.csvcell, is database order. The nondeterminism predates the change; the extra joins fromunarchived()moved the observed order, which is why one pre-existing test now compares order-insensitively.test_copy_container_leaves_archived_tags_out), not through a paste into a course: a library paste never applies staged tags, so no library fixture can exercise that last step.test_list_taxonomy_query_countpasses in the full view test file but fails when run alone with-k; it depends on earlier tests warming caches, and this predates the change.TaxonomyOrgView.get_querysetinviews.py, thenget_all_object_tagsandset_exported_object_tagsinapi.py, thenTestExportImportTagsintests/test_api.py.