Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
fd644d4
fix: Ensure problem response reports include all descendant problems …
efortish Feb 5, 2026
fd15557
refactor: remove deprecated sidebar toggles (#37983)
brian-smith-tcril Feb 9, 2026
c338f09
feat: Add Waffle flag for AuthZ for Course Authoring (#37985)
rodmgwgu Feb 9, 2026
d0a2212
build: Upgrade to `ora2==6.17.2` which removes loremipsum base dep (#…
kdmccormick Feb 9, 2026
3c4cf0e
fix: Nits on styles of library icon [FC-0114] (#37980)
ChrisChV Feb 9, 2026
8ca70db
refactor: xblock api upstream info and course details api (#37971)
navinkarkera Feb 9, 2026
24468b6
feat: Add enable_authz_course_authoring flag to course_waffle_flags e…
rodmgwgu Feb 10, 2026
d20b87b
Discussion service to enable permission and access provider (#37912)
salman2013 Feb 11, 2026
ef8b03b
fix: Typo in unsupported reason message in content libraries [FC-0114…
ChrisChV Feb 12, 2026
d847d22
fix: migrations to make postgresql compatible. (#35762)
qasimgulzar Feb 12, 2026
a55c1dd
chore: Switch to new openedx-learning import paths (#38004)
kdmccormick Feb 13, 2026
8dd99de
chore: optimize/correct the VideoBlock code (#38012)
farhan Feb 17, 2026
b98e41e
feat: add v2 REST API endpoints for instructor dashboard data downlo…
wgu-jesse-stewart Feb 17, 2026
4369055
fix: Add group_id and user_partition_id support to Cohorts API v1 (#3…
brianjbuck-wgu Feb 17, 2026
5e75d3c
feat: Make selectable component cards (#38010)
ChrisChV Feb 18, 2026
c70bfe9
build!: Switch to openedx-core (renamed from openedx-learning) (#38011)
kdmccormick Feb 18, 2026
7601818
Use runtime-provided XQueueService instead of constructing it in Prob…
irtazaakram Feb 19, 2026
8b3c3fd
feat: Simplify content groups v2 response JSON (#37976)
brianjbuck-wgu Feb 19, 2026
7499a5f
feat: authorize advanced settings endpoints via openedx-authz when fl…
wgu-taylor-payne Feb 19, 2026
f4ce78d
fix: Upgrade openedx-core to fix LanguageTaxonomy data bug
kdmccormick Feb 20, 2026
e5ebde8
build: Loosen openedx-core constraint to allow patch upgrades
kdmccormick Feb 20, 2026
873f42a
refactor: update openedx_content contents -> media(#38037)
ormsbee Feb 24, 2026
ee35515
test: Move VideoConfigService related tests near inside its app (#38008)
farhan Feb 24, 2026
616c966
fix: calculate last_grade_publish_date for all unreleased subsections
asajjad2 Dec 11, 2025
7d162c3
Merge pull request #37748 from asajjad2/areeb/last-publish-grade-calc…
pdpinch Feb 24, 2026
7aa28fc
feat: Upgrade Python dependency lti-consumer-xblock (#38048)
github-actions[bot] Feb 24, 2026
ef783a1
chore: bump edx-enterprise-integrated-channels to 0.1.42 (#38049)
pwnage101 Feb 25, 2026
3e522d5
feat: bump opaque-keys to get case-sensitivity support + default max_…
bradenmacdonald Feb 23, 2026
12e9af4
fix!: split modulestore's has_course(ignore_case=True) was not workin…
bradenmacdonald Feb 19, 2026
d9293af
chore: Upgrade Python dependency edx-enterprise (#38051)
github-actions[bot] Feb 25, 2026
b968eed
feat: add optional API for hiding the dates tab (#37923)
Anas12091101 Feb 26, 2026
0a9f789
feat: Upgrade Python dependency enterprise-integrated-channels (#38053)
github-actions[bot] Feb 26, 2026
7c9f468
refactor: Drop the unused legacy video upload page.
feanil Oct 30, 2025
cd7f2ae
fix: use correct Django settings for each service in CI
feanil Feb 2, 2026
e1757eb
feat!: Bump xblocks-contrib to install PDF block by default (#38055)
Kelketek Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/static-assets-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ jobs:
env:
LMS_CFG: lms/envs/minimal.yml
CMS_CFG: lms/envs/minimal.yml
DJANGO_SETTINGS_MODULE: lms.envs.production
run: |
./manage.py lms collectstatic --noinput
./manage.py cms collectstatic --noinput
DJANGO_SETTINGS_MODULE=lms.envs.production ./manage.py lms collectstatic --noinput
DJANGO_SETTINGS_MODULE=cms.envs.production ./manage.py cms collectstatic --noinput
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import django.db.migrations.operations.special
import django.db.models.deletion
import opaque_keys.edx.django.models
import openedx_learning.lib.fields
import openedx_learning.lib.validators
import openedx_django_lib.fields
import openedx_django_lib.validators
import uuid
from django.conf import settings
from django.db import migrations, models
Expand Down Expand Up @@ -107,8 +107,8 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('context_key', opaque_keys.edx.django.models.CourseKeyField(help_text='Linking status for course context key', max_length=255, unique=True)),
('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('failed', 'Failed'), ('completed', 'Completed')], help_text='Status of links in given learning context/course.', max_length=20)),
('created', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('created', models.DateTimeField(validators=[openedx_django_lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_django_lib.validators.validate_utc_datetime])),
],
options={
'verbose_name': 'Learning Context Links status',
Expand All @@ -121,13 +121,13 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='UUID')),
('upstream_usage_key', opaque_keys.edx.django.models.UsageKeyField(help_text='Upstream block usage key, this value cannot be null and useful to track upstream library blocks that do not exist yet', max_length=255)),
('upstream_context_key', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, db_index=True, help_text='Upstream context key i.e., learning_package/library key', max_length=500)),
('upstream_context_key', openedx_django_lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, db_index=True, help_text='Upstream context key i.e., learning_package/library key', max_length=500)),
('downstream_usage_key', opaque_keys.edx.django.models.UsageKeyField(max_length=255, unique=True)),
('downstream_context_key', opaque_keys.edx.django.models.CourseKeyField(db_index=True, max_length=255)),
('version_synced', models.IntegerField()),
('version_declined', models.IntegerField(blank=True, null=True)),
('created', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('created', models.DateTimeField(validators=[openedx_django_lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_django_lib.validators.validate_utc_datetime])),
('upstream_block', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='links', to='openedx_content.component')),
],
options={
Expand All @@ -140,13 +140,13 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='UUID')),
('upstream_context_key', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, db_index=True, help_text='Upstream context key i.e., learning_package/library key', max_length=500)),
('upstream_context_key', openedx_django_lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, db_index=True, help_text='Upstream context key i.e., learning_package/library key', max_length=500)),
('downstream_usage_key', opaque_keys.edx.django.models.UsageKeyField(max_length=255, unique=True)),
('downstream_context_key', opaque_keys.edx.django.models.CourseKeyField(db_index=True, max_length=255)),
('version_synced', models.IntegerField()),
('version_declined', models.IntegerField(blank=True, null=True)),
('created', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('created', models.DateTimeField(validators=[openedx_django_lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_django_lib.validators.validate_utc_datetime])),
('upstream_container_key', opaque_keys.edx.django.models.ContainerKeyField(help_text='Upstream block key (e.g. lct:...), this value cannot be null and is useful to track upstream library blocks that do not exist yet or were deleted.', max_length=255)),
('upstream_container', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='links', to='openedx_content.container')),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

import django.db.models.deletion
import opaque_keys.edx.django.models
import openedx_learning.lib.fields
import openedx_learning.lib.validators
import openedx_django_lib.fields
import openedx_django_lib.validators
from django.db import migrations, models


Expand Down Expand Up @@ -39,8 +39,8 @@ class Migration(migrations.Migration):
max_length=20,
),
),
('created', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('created', models.DateTimeField(validators=[openedx_django_lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_django_lib.validators.validate_utc_datetime])),
],
options={
'verbose_name': 'Learning Context Links status',
Expand All @@ -61,7 +61,7 @@ class Migration(migrations.Migration):
),
(
'upstream_context_key',
openedx_learning.lib.fields.MultiCollationCharField(
openedx_django_lib.fields.MultiCollationCharField(
db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'},
db_index=True,
help_text='Upstream context key i.e., learning_package/library key',
Expand All @@ -72,8 +72,8 @@ class Migration(migrations.Migration):
('downstream_context_key', opaque_keys.edx.django.models.CourseKeyField(db_index=True, max_length=255)),
('version_synced', models.IntegerField()),
('version_declined', models.IntegerField(blank=True, null=True)),
('created', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('created', models.DateTimeField(validators=[openedx_django_lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_django_lib.validators.validate_utc_datetime])),
(
'upstream_block',
models.ForeignKey(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

import django.db.models.deletion
import opaque_keys.edx.django.models
import openedx_learning.lib.fields
import openedx_learning.lib.validators
import openedx_django_lib.fields
import openedx_django_lib.validators
from django.db import migrations, models


Expand Down Expand Up @@ -40,13 +40,13 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='UUID')),
('upstream_context_key', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, db_index=True, help_text='Upstream context key i.e., learning_package/library key', max_length=500)),
('upstream_context_key', openedx_django_lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, db_index=True, help_text='Upstream context key i.e., learning_package/library key', max_length=500)),
('downstream_usage_key', opaque_keys.edx.django.models.UsageKeyField(max_length=255, unique=True)),
('downstream_context_key', opaque_keys.edx.django.models.CourseKeyField(db_index=True, max_length=255)),
('version_synced', models.IntegerField()),
('version_declined', models.IntegerField(blank=True, null=True)),
('created', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('created', models.DateTimeField(validators=[openedx_django_lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_django_lib.validators.validate_utc_datetime])),
('upstream_container_key', opaque_keys.edx.django.models.ContainerKeyField(help_text='Upstream block key (e.g. lct:...), this value cannot be null and is useful to track upstream library blocks that do not exist yet or were deleted.', max_length=255)),
('upstream_container', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='links', to='oel_publishing.container')),
],
Expand Down
13 changes: 5 additions & 8 deletions cms/djangoapps/contentstore/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
from opaque_keys.edx.django.models import ContainerKeyField, CourseKeyField, UsageKeyField
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locator import LibraryContainerLocator
from openedx_learning.api.authoring import get_published_version
from openedx_learning.api.authoring_models import Component, Container
from openedx_learning.lib.fields import (
from openedx_content.api import get_published_version
from openedx_content.models_api import Component, Container
from openedx_django_lib.fields import (
immutable_uuid_field,
key_field,
manual_date_time_field,
Expand Down Expand Up @@ -97,9 +97,9 @@ class EntityLinkBase(models.Model):
)
# A downstream entity can only link to single upstream entity
# whereas an entity can be upstream for multiple downstream entities.
downstream_usage_key = UsageKeyField(max_length=255, unique=True)
downstream_usage_key = UsageKeyField(unique=True)
# Search by course/downstream key
downstream_context_key = CourseKeyField(max_length=255, db_index=True)
downstream_context_key = CourseKeyField(db_index=True)
# This is present if the creation of this link is a consequence of
# importing a container that has one or more levels of children.
# This represents the parent (container) in the top level
Expand Down Expand Up @@ -152,7 +152,6 @@ class ComponentLink(EntityLinkBase):
blank=True,
)
upstream_usage_key = UsageKeyField(
max_length=255,
help_text=_(
"Upstream block usage key, this value cannot be null"
" and useful to track upstream library blocks that do not exist yet"
Expand Down Expand Up @@ -324,7 +323,6 @@ class ContainerLink(EntityLinkBase):
blank=True,
)
upstream_container_key = ContainerKeyField(
max_length=255,
help_text=_(
"Upstream block key (e.g. lct:...), this value cannot be null "
"and is useful to track upstream library blocks that do not exist yet "
Expand Down Expand Up @@ -564,7 +562,6 @@ class LearningContextLinksStatus(models.Model):
course or a learning context.
"""
context_key = CourseKeyField(
max_length=255,
# Single entry for a learning context or course
unique=True,
help_text=_("Linking status for course context key"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@
Tests for the course advanced settings API.
"""
import json
import pkg_resources
from unittest.mock import patch

import casbin
import ddt
from django.test import override_settings
from django.urls import reverse
from milestones.tests.utils import MilestonesTestCaseMixin
from rest_framework.test import APIClient

from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core import toggles as core_toggles
from openedx_authz.api.users import assign_role_to_user_in_scope
from openedx_authz.constants.roles import COURSE_STAFF
from openedx_authz.engine.enforcer import AuthzEnforcer
from openedx_authz.engine.utils import migrate_policy_between_enforcers


@ddt.ddt
Expand Down Expand Up @@ -91,3 +101,105 @@ def test_disabled_fetch_all_query_param(self, setting, excluded_field):
with override_settings(FEATURES={setting: False}):
resp = self.client.get(self.url, {"fetch_all": 0})
assert excluded_field not in resp.data


@patch.object(core_toggles.AUTHZ_COURSE_AUTHORING_FLAG, 'is_enabled', return_value=True)
class AdvancedSettingsAuthzTest(CourseTestCase):
"""
Tests for AdvancedCourseSettingsView authorization with openedx-authz.

These tests enable the AUTHZ_COURSE_AUTHORING_FLAG by default.
"""

def setUp(self):
super().setUp()
self._seed_database_with_policies()
self.url = reverse(
"cms.djangoapps.contentstore:v0:course_advanced_settings",
kwargs={"course_id": self.course.id},
)

# Create test users
self.authorized_user = UserFactory()
self.unauthorized_user = UserFactory()

# Assign role to authorized user
assign_role_to_user_in_scope(
self.authorized_user.username,
COURSE_STAFF.external_key,
str(self.course.id)
)
AuthzEnforcer.get_enforcer().load_policy()

# Create API clients and force_authenticate
self.authorized_client = APIClient()
self.authorized_client.force_authenticate(user=self.authorized_user)
self.unauthorized_client = APIClient()
self.unauthorized_client.force_authenticate(user=self.unauthorized_user)

def tearDown(self):
super().tearDown()
AuthzEnforcer.get_enforcer().clear_policy()

@classmethod
def _seed_database_with_policies(cls):
"""Seed the database with policies from the policy file."""
global_enforcer = AuthzEnforcer.get_enforcer()
global_enforcer.load_policy()
model_path = pkg_resources.resource_filename("openedx_authz.engine", "config/model.conf")
policy_path = pkg_resources.resource_filename("openedx_authz.engine", "config/authz.policy")
migrate_policy_between_enforcers(
source_enforcer=casbin.Enforcer(model_path, policy_path),
target_enforcer=global_enforcer,
)

def test_authorized_for_specific_course(self, mock_flag):
"""User authorized for specific course can access."""
response = self.authorized_client.get(self.url)
self.assertEqual(response.status_code, 200)

def test_unauthorized_for_specific_course(self, mock_flag):
"""User without authorization for specific course cannot access."""
response = self.unauthorized_client.get(self.url)
self.assertEqual(response.status_code, 403)

def test_unauthorized_for_different_course(self, mock_flag):
"""User authorized for one course cannot access another course."""
other_course = self.store.create_course("OtherOrg", "OtherCourse", "Run", self.user.id)
other_url = reverse(
"cms.djangoapps.contentstore:v0:course_advanced_settings",
kwargs={"course_id": other_course.id},
)
response = self.authorized_client.get(other_url)
self.assertEqual(response.status_code, 403)

def test_staff_authorized_by_default(self, mock_flag):
"""Staff users are authorized by default."""
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)

def test_superuser_authorized_by_default(self, mock_flag):
"""Superusers are authorized by default."""
superuser = UserFactory(is_superuser=True, is_staff=False)
superuser_client = APIClient()
superuser_client.force_authenticate(user=superuser)
response = superuser_client.get(self.url)
self.assertEqual(response.status_code, 200)

def test_patch_authorized_for_specific_course(self, mock_flag):
"""User authorized for specific course can PATCH."""
response = self.authorized_client.patch(
self.url,
{"display_name": {"value": "Test"}},
content_type="application/json"
)
self.assertEqual(response.status_code, 200)

def test_patch_unauthorized_for_specific_course(self, mock_flag):
"""User without authorization for specific course cannot PATCH."""
response = self.unauthorized_client.patch(
self.url,
{"display_name": {"value": "Test"}},
content_type="application/json"
)
self.assertEqual(response.status_code, 403)
Loading