Skip to content

Tbain/903 hide archived tagging records - #3

Closed
tbain wants to merge 48 commits into
masterfrom
tbain/903_hide_archived_tagging_records
Closed

tbain wants to merge 48 commits into
masterfrom
tbain/903_hide_archived_tagging_records

Conversation

@tbain

@tbain tbain commented Nov 9, 2026

Copy link
Copy Markdown
Member

Description

Studio's content_tagging app now respects the archived state that openedx-core gives taxonomies, tags, and tag associations. Three places change: the taxonomy list's tags_count leaves 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's content_tagging app 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) annotates tags_count=Count("tag", filter=Q(tag__archived=False), distinct=True), the same expression openedx-core's TaxonomyView uses. One annotation covers the no-filter, org, and unassigned branches.
  • get_all_object_tags (api.py) starts its query from ObjectTag.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 in generate_csv_rows, copy_object_tags, openedx/core/lib/xblock_serializer/block_serializer.py, or content_libraries/api/serializers.py.
  • set_exported_object_tags (api.py):
    • clears with ObjectTag.objects.filter(object_id=...).unarchived().delete() in place of delete_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 queryset delete(), so openedx-core's override still archives a deletion-locked association instead of deleting it.
    • resolves each export id through the unfiltered Taxonomy.objects.filter(export_id=...).first(), because get_taxonomy_by_export_id returns None for an archived taxonomy and a missing one alike. A live taxonomy is tagged as before. An archived taxonomy is skipped, with a log.info naming the content key, export id, and skipped values, and with no placeholder and no events; a placeholder would be wrong because ObjectTag.resync() would re-link it to the archived taxonomy. A missing taxonomy still gets placeholders.
  • 13 new test functions (19 cases with the ddt parameters) across 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_tags orders rows by object_id only, so order within a taxonomy's values was always database order, and the extra joins from unarchived() changed it.

Who is affected

  • Course Authors, once openedx-core's archiving lands and something produces archived rows: Studio's taxonomy list counts only live tags; exporting, duplicating, copying, pasting, and importing content no longer carries archived associations; and re-importing a course over itself leaves hidden associations in place. Nothing they see changes until then.
  • Developers: get_all_object_tags no longer returns archived associations. set_exported_object_tags no longer clears them either; use delete_object_tags for a full clear.

Supporting information

Testing instructions

Automated, in a tutor dev CMS container with openedx-core mounted from the tbain/903_keep_hidden_object_tags_on_save branch:

  1. The new and changed tests, two commands (pytest keeps only the last -k, so the Studio tests run separately):

    pytest --ds=cms.envs.test -p no:randomly openedx/core/djangoapps/content_tagging/ \
      -k "excludes_archived or keeps_disabled or all_archived or test_import or tags_count"
    pytest --ds=cms.envs.test -p no:randomly \
      "cms/djangoapps/contentstore/views/tests/test_block.py::TestDuplicateItem::test_duplicate_tags" \
      "cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py::ClipboardPasteTestCase::test_copy_and_paste_unit_with_tags"
  2. The whole package: pytest --ds=cms.envs.test openedx/core/djangoapps/content_tagging/ (all pass; 653 tests on master as of 2026-11-09).

  3. pylint on the touched files, ruff check openedx/core/djangoapps/content_tagging/, and mypy openedx/core/djangoapps/content_tagging/, with DJANGO_SETTINGS_MODULE=cms.envs.test for 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:

  1. Open the Taxonomies page. The taxonomy's tag count is one lower than before.
  2. Open a block tagged with that tag and export the course. tags.csv has no cell with that value.
  3. Duplicate the block, and copy and paste it into another unit. Neither copy carries the archived tag; the original block still has its row (ObjectTag.objects.filter(...) in the shell shows it).
  4. Import the exported course back into itself. The association to the archived tag is still there, exactly once, and still hidden.

Deadline

None.

Other information

  • No model changes, no migrations, no new dependencies.
  • Known gaps:
    • Value order within a taxonomy in get_all_object_tags output, and so within a tags.csv cell, is database order. The nondeterminism predates the change; the extra joins from unarchived() moved the observed order, which is why one pre-existing test now compares order-insensitively.
    • The library-container scenario is pinned at the serializer and clipboard stage (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_count passes 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.
  • Suggested reading order: TaxonomyOrgView.get_queryset in views.py, then get_all_object_tags and set_exported_object_tags in api.py, then TestExportImportTags in tests/test_api.py.
  • AI usage: this PR was developed with Claude (Anthropic) in Cowork, with Sonnet sub-agents implementing each task test-first and a per-task review plus a whole-branch review by fresh sub-agents. I ran every test and lint command locally and reviewed the change.

Anas12091101 and others added 30 commits October 19, 2026 15:26
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 `&copy;`, 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 `&quot;` from `certificate_verify_description`,
which ended with `"detached signature"&quot;".`, 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 `&copy;`.
- 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>
feanil and others added 18 commits November 2, 2026 23:10
…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>
… 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
@tbain tbain closed this Nov 9, 2026
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.