Skip to content

Commit 20540b8

Browse files
fix: revert jwt test
2 parents f98c3fb + f8b4990 commit 20540b8

79 files changed

Lines changed: 5635 additions & 1687 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Create quarterly issues for GitHub audit
2+
on:
3+
schedule:
4+
- cron: 0 0 1 1,4,7,10 *
5+
workflow_dispatch: {}
6+
7+
env:
8+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
9+
10+
jobs:
11+
create_issue:
12+
name: Create quarterly constraint check issue
13+
runs-on: ubuntu-latest
14+
permissions:
15+
issues: write
16+
steps:
17+
- run: |
18+
# Platform constraints audit
19+
new_issue_url=$(gh issue create --repo "openedx/openedx-platform" \
20+
--title "Quarterly audit of openedx-platform constraints" \
21+
--label "code health" \
22+
--body "It is time to perform the quartely audit of constrained dependencies in \`openedx-platform\`. The goal is to remove any constraints that are no longer necessary to proactively prevent version conflicts and keep us up to date with security patches. The playbook for performing the audit can be found [here](https://openedx.atlassian.net/wiki/spaces/AC/pages/6340968449/Quarterly+Platform+Constraints+Audit).")
23+
echo "NEW_ISSUE_URL=$new_issue_url" >> $GITHUB_ENV
24+
25+
- name: Comment on issue
26+
run: gh issue comment $NEW_ISSUE_URL --body "@openedx/wg-maintenance-openedx-platform-oncall heads up on this request"

cms/djangoapps/contentstore/helpers.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,8 +509,18 @@ def _fetch_and_set_upstream_link(
509509
# temp_xblock.display_name == temp_xblock.upstream_display_name
510510
# temp_xblock.data == temp_xblock.upstream_data # for html blocks
511511
# Even then we want to set `downstream_customized` value to avoid overriding user customisations on sync
512-
downstream_customized = temp_xblock.xml_attributes.get("downstream_customized", '[]')
513-
temp_xblock.downstream_customized = json.loads(downstream_customized)
512+
downstream_customized = getattr(temp_xblock, "downstream_customized", [])
513+
# XmlMixin blocks expose raw XML attrs on `xml_attributes`; other blocks (e.g. DnD)
514+
# may not have this attribute, but still have parsed downstream_customized field.
515+
xml_attributes = getattr(temp_xblock, "xml_attributes", None)
516+
if isinstance(xml_attributes, dict):
517+
raw_downstream_customized = xml_attributes.get("downstream_customized")
518+
if isinstance(raw_downstream_customized, str):
519+
downstream_customized = json.loads(raw_downstream_customized)
520+
elif isinstance(raw_downstream_customized, list):
521+
downstream_customized = raw_downstream_customized
522+
if hasattr(temp_xblock, "downstream_customized"):
523+
temp_xblock.downstream_customized = downstream_customized
514524

515525

516526
def _import_xml_node_to_parent(

cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ def test_home_page_libraries_response(self):
329329
'can_edit': True,
330330
'is_migrated': True,
331331
'migrated_to_title': 'Test Library',
332-
'migrated_to_key': 'lib:name0:test-key',
332+
'migrated_to_key': str(self.lib_key_v2),
333333
'migrated_to_collection_key': 'test-collection',
334334
'migrated_to_collection_title': 'Test Collection',
335335
},
@@ -364,7 +364,7 @@ def test_home_page_libraries_response(self):
364364
'can_edit': True,
365365
'is_migrated': True,
366366
'migrated_to_title': 'Test Library',
367-
'migrated_to_key': 'lib:name0:test-key',
367+
'migrated_to_key': str(self.lib_key_v2),
368368
'migrated_to_collection_key': 'test-collection',
369369
'migrated_to_collection_title': 'Test Collection',
370370
}

cms/djangoapps/contentstore/views/block.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from django.views.decorators.clickjacking import xframe_options_exempt
1313
from django.views.decorators.http import require_http_methods
1414
from opaque_keys.edx.keys import CourseKey
15+
from openedx_authz.constants.permissions import COURSES_VIEW_COURSE
1516
from web_fragments.fragment import Fragment
1617

1718
from cms.djangoapps.contentstore.utils import load_services_for_studio
@@ -27,6 +28,8 @@
2728
from common.djangoapps.edxmako.shortcuts import render_to_response, render_to_string
2829
from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access
2930
from common.djangoapps.util.json_request import JsonResponse, expect_json
31+
from openedx.core.djangoapps.authz.constants import LegacyAuthoringPermission
32+
from openedx.core.djangoapps.authz.decorators import user_has_course_permission
3033
from openedx.core.djangoapps.content_tagging.toggles import is_tagging_feature_disabled
3134
from openedx.core.lib.xblock_utils import hash_resource, request_token, wrap_xblock, wrap_xblock_aside
3235
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
@@ -329,7 +332,12 @@ def xblock_outline_handler(request, usage_key_string):
329332
a course.
330333
"""
331334
usage_key = usage_key_with_run(usage_key_string)
332-
if not has_studio_read_access(request.user, usage_key.course_key):
335+
if not user_has_course_permission(
336+
request.user,
337+
COURSES_VIEW_COURSE.identifier,
338+
usage_key.course_key,
339+
LegacyAuthoringPermission.READ,
340+
):
333341
raise PermissionDenied()
334342

335343
response_format = request.GET.get("format", "html")

cms/djangoapps/contentstore/views/tests/test_block.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from opaque_keys.edx.asides import AsideUsageKeyV2
2020
from opaque_keys.edx.keys import CourseKey, UsageKey
2121
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
22+
from openedx_authz.constants.roles import COURSE_ADMIN, COURSE_AUDITOR, COURSE_EDITOR, COURSE_STAFF
2223
from openedx_events.content_authoring.data import DuplicatedXBlockData
2324
from openedx_events.content_authoring.signals import XBLOCK_DUPLICATED
2425
from openedx_events.testing import OpenEdxEventsTestMixin
@@ -54,6 +55,7 @@
5455
from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService
5556
from common.test.utils import assert_dict_contains_subset
5657
from lms.djangoapps.lms_xblock.mixin import NONSENSICAL_ACCESS_RESTRICTION
58+
from openedx.core.djangoapps.authz.tests.mixins import CourseAuthoringAuthzTestMixin
5759
from openedx.core.djangoapps.content_tagging import api as tagging_api
5860
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration
5961
from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE
@@ -3486,6 +3488,145 @@ def validate_xblock_info_consistency(
34863488
self.assertIsNone(xblock_info.get("child_info", None)) # noqa: PT009
34873489

34883490

3491+
@ddt.ddt
3492+
class TestXBlockOutlineHandlerAuthz(CourseAuthoringAuthzTestMixin, ItemTest):
3493+
"""
3494+
Unit tests for xblock_outline_handler authorization functionality.
3495+
"""
3496+
3497+
def setUp(self):
3498+
super().setUp()
3499+
user_id = self.user.id
3500+
self.chapter = BlockFactory.create(
3501+
parent_location=self.course.location,
3502+
category="chapter",
3503+
display_name="Week 1",
3504+
user_id=user_id,
3505+
)
3506+
self.sequential = BlockFactory.create(
3507+
parent_location=self.chapter.location,
3508+
category="sequential",
3509+
display_name="Lesson 1",
3510+
user_id=user_id,
3511+
)
3512+
self.vertical = BlockFactory.create(
3513+
parent_location=self.sequential.location,
3514+
category="vertical",
3515+
display_name="Unit 1",
3516+
user_id=user_id,
3517+
)
3518+
# Assign COURSE_STAFF role to authorized_user for the course
3519+
self.add_user_to_role_in_course(
3520+
self.authorized_user,
3521+
COURSE_STAFF.external_key,
3522+
self.course.id
3523+
)
3524+
3525+
def test_authorized_user_gets_json_response(self):
3526+
"""
3527+
Test that authorized user gets JSON response from xblock_outline_handler.
3528+
"""
3529+
outline_url = reverse_usage_url("xblock_outline_handler", self.usage_key)
3530+
3531+
self.client.login(username=self.authorized_user.username, password=self.password)
3532+
resp = self.client.get(outline_url, HTTP_ACCEPT="application/json")
3533+
3534+
assert resp.status_code == 200
3535+
json_response = json.loads(resp.content.decode("utf-8"))
3536+
assert "id" in json_response
3537+
assert "display_name" in json_response
3538+
assert "child_info" in json_response
3539+
3540+
@ddt.data(
3541+
COURSE_ADMIN.external_key,
3542+
COURSE_AUDITOR.external_key,
3543+
COURSE_EDITOR.external_key,
3544+
)
3545+
def test_other_course_roles_can_view_outline(self, role_key):
3546+
"""
3547+
Test that course_admin, course_auditor, and course_editor roles
3548+
can access the outline (all have COURSES_VIEW_COURSE).
3549+
"""
3550+
role_user = UserFactory(password=self.password)
3551+
self.add_user_to_role_in_course(role_user, role_key, self.course.id)
3552+
3553+
outline_url = reverse_usage_url("xblock_outline_handler", self.usage_key)
3554+
self.client.login(username=role_user.username, password=self.password)
3555+
resp = self.client.get(outline_url, HTTP_ACCEPT="application/json")
3556+
3557+
assert resp.status_code == 200
3558+
3559+
def test_unauthorized_user_gets_permission_denied(self):
3560+
"""
3561+
Test that unauthorized user gets 403 response from xblock_outline_handler.
3562+
"""
3563+
outline_url = reverse_usage_url("xblock_outline_handler", self.usage_key)
3564+
3565+
self.client.login(username=self.unauthorized_user.username, password=self.password)
3566+
resp = self.client.get(outline_url, HTTP_ACCEPT="application/json")
3567+
3568+
assert resp.status_code == 403
3569+
3570+
def test_superuser_gets_json_response(self):
3571+
"""
3572+
Test that superuser gets JSON response from xblock_outline_handler.
3573+
"""
3574+
outline_url = reverse_usage_url("xblock_outline_handler", self.usage_key)
3575+
3576+
self.client.login(username=self.super_user.username, password=self.password)
3577+
resp = self.client.get(outline_url, HTTP_ACCEPT="application/json")
3578+
3579+
assert resp.status_code == 200
3580+
json_response = json.loads(resp.content.decode("utf-8"))
3581+
assert "id" in json_response
3582+
assert "display_name" in json_response
3583+
assert "child_info" in json_response
3584+
3585+
def test_staff_user_gets_json_response(self):
3586+
"""
3587+
Test that staff user gets JSON response from xblock_outline_handler.
3588+
"""
3589+
outline_url = reverse_usage_url("xblock_outline_handler", self.usage_key)
3590+
3591+
self.client.login(username=self.staff_user.username, password=self.password)
3592+
resp = self.client.get(outline_url, HTTP_ACCEPT="application/json")
3593+
3594+
assert resp.status_code == 200
3595+
json_response = json.loads(resp.content.decode("utf-8"))
3596+
assert "id" in json_response
3597+
assert "display_name" in json_response
3598+
assert "child_info" in json_response
3599+
3600+
def test_authorized_chapter_outline(self):
3601+
"""
3602+
Test that authorized user can access chapter-level outline.
3603+
"""
3604+
outline_url = reverse_usage_url("xblock_outline_handler", self.chapter.location)
3605+
3606+
self.client.login(username=self.authorized_user.username, password=self.password)
3607+
resp = self.client.get(outline_url, HTTP_ACCEPT="application/json")
3608+
3609+
assert resp.status_code == 200
3610+
json_response = json.loads(resp.content.decode("utf-8"))
3611+
assert json_response["display_name"] == "Week 1"
3612+
assert "child_info" in json_response
3613+
# Verify that children are included (should have the sequential)
3614+
children = json_response["child_info"]["children"]
3615+
assert len(children) > 0
3616+
assert children[0]["display_name"] == "Lesson 1"
3617+
3618+
def test_unauthorized_chapter_outline(self):
3619+
"""
3620+
Test that unauthorized user cannot access chapter-level outline.
3621+
"""
3622+
outline_url = reverse_usage_url("xblock_outline_handler", self.chapter.location)
3623+
3624+
self.client.login(username=self.unauthorized_user.username, password=self.password)
3625+
resp = self.client.get(outline_url, HTTP_ACCEPT="application/json")
3626+
3627+
assert resp.status_code == 403
3628+
3629+
34893630
class TestGetMetadataWithProblemDefaults(ModuleStoreTestCase):
34903631
"""
34913632
Unit tests for _get_metadata_with_problem_defaults.

cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66
import ddt
77
from opaque_keys.edx.keys import UsageKey
8+
from openedx_content.api import signals as content_signals
89
from openedx_events.content_authoring.signals import (
910
LIBRARY_BLOCK_DELETED,
1011
XBLOCK_CREATED,
@@ -405,6 +406,7 @@ class ClipboardPasteFromV2LibraryTestCase(OpenEdxEventsTestMixin, ImmediateOnCom
405406
Test Clipboard Paste functionality with a "new" (as of Sumac) library
406407
"""
407408
ENABLED_OPENEDX_EVENTS = [
409+
content_signals.ENTITIES_DRAFT_CHANGED.event_type, # Required for library events to work
408410
LIBRARY_BLOCK_DELETED.event_type,
409411
XBLOCK_CREATED.event_type,
410412
XBLOCK_DELETED.event_type,
@@ -491,7 +493,8 @@ def test_paste_from_library_read_only_tags(self):
491493
assert object_tag.is_copied
492494

493495
# If we delete the upstream library block...
494-
library_api.delete_library_block(self.lib_block_key)
496+
with self.captureOnCommitCallbacks(execute=True): # make event handlers fire now, within TestCase transaction
497+
library_api.delete_library_block(self.lib_block_key)
495498

496499
# ...the copied tags remain, but should no longer be marked as "copied"
497500
object_tags = tagging_api.get_object_tags(new_block_key)

cms/djangoapps/modulestore_migrator/tasks.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -895,13 +895,9 @@ def _migrate_container(
895895
).publishable_entity_version
896896

897897
# Publish the container
898-
# Call post publish events synchronously to avoid
899-
# an error when calling `wait_for_post_publish_events`
900-
# inside a celery task.
901898
libraries_api.publish_container_changes(
902899
container.container_key,
903900
context.created_by,
904-
call_post_publish_events_sync=True,
905901
)
906902
context.used_container_slugs.add(container.container_key.container_id)
907903
return container_publishable_entity_version, None
@@ -963,16 +959,18 @@ def _migrate_component(
963959
return component.versioning.draft.publishable_entity_version, None
964960

965961
# If component existed and was deleted or we have to replace the current version
966-
# Create the new component version for it
967-
component_version = libraries_api.set_library_block_olx(target_key, new_olx_str=olx)
962+
paths_to_media_ids = {}
968963
for filename, media_pk in context.content_by_filename.items():
969964
filename_no_ext, _ = os.path.splitext(filename)
970965
if filename_no_ext not in olx:
971966
continue
972967
new_path = f"static/{filename}"
973-
content_api.create_component_version_media(
974-
component_version.pk, media_pk, path=new_path
975-
)
968+
paths_to_media_ids[new_path] = media_pk
969+
970+
# Create the new component version for it
971+
component_version = libraries_api.set_library_block_olx(
972+
target_key, new_olx_str=olx, paths_to_media=paths_to_media_ids,
973+
)
976974

977975
# Publish the component
978976
libraries_api.publish_component_changes(target_key, context.created_by)

cms/static/sass/elements/_modal-window.scss

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -406,43 +406,43 @@
406406
display: flex;
407407
flex-direction: column;
408408

409-
410-
// Set the components to flex so that they can grow
411-
div,
412-
form {
409+
div.edit-xblock-modal {
413410
display: flex;
414411
flex-direction: column;
415412
flex-grow: 1;
416413

417-
// Set the header to not grow
418-
header,
419-
&.modal-header {
420-
flex-grow: 0;
421-
}
414+
div.modal-content {
415+
display: flex;
416+
flex-direction: column;
417+
flex-grow: 1;
418+
padding: 0;
422419

423-
}
420+
div.xblock-editor {
421+
display: flex;
422+
flex-direction: column;
423+
flex-grow: 1;
424424

425-
ul {
426-
flex-grow: 1;
425+
div.xblock-studio_view {
426+
display: flex;
427+
flex-direction: column;
428+
flex-grow: 1;
429+
}
427430

428-
// Reset the divs inside the ul to preserve previous styling
429-
div,
430-
form {
431-
display: unset;
432-
flex-direction: unset;
433-
flex-grow: unset;
434-
}
435-
}
431+
div.editor-with-buttons {
432+
display: flex;
433+
flex-direction: column;
434+
flex-grow: 1;
435+
margin-bottom: 0;
436436

437-
&.modal-editor {
438-
.modal-content {
439-
padding: 0px;
437+
ul.settings-list {
438+
flex-grow: 1;
439+
}
440+
}
441+
}
440442
}
441443
}
442444
}
443445

444-
445-
446446
// specific modal overrides
447447
// ------------------------
448448

0 commit comments

Comments
 (0)