Skip to content

Commit 795cdf8

Browse files
authored
Merge branch 'master' into pwnage101/ENT-11569
2 parents c0912bf + e634f00 commit 795cdf8

45 files changed

Lines changed: 1285 additions & 1342 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/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/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: 0 additions & 4 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

cms/envs/common.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,6 @@
256256

257257
MARKETING_EMAILS_OPT_IN = False
258258

259-
############################# MICROFRONTENDS ###################################
260-
COURSE_AUTHORING_MICROFRONTEND_URL = None
261-
262259
############################# SET PATH INFORMATION #############################
263260
PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/cms
264261
CMS_ROOT = REPO_ROOT / "cms" # noqa: F405

lms/djangoapps/instructor/tests/test_api_v2.py

Lines changed: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def setUp(self):
8181
self.admin = AdminFactory.create()
8282
self.instructor = InstructorFactory.create(course_key=self.course_key)
8383
self.staff = StaffFactory.create(course_key=self.course_key)
84+
self.django_staff_user = UserFactory.create(is_staff=True)
8485
self.data_researcher = UserFactory.create()
8586
CourseDataResearcherRole(self.course_key).add_users(self.data_researcher)
8687
CourseInstructorRole(self.proctored_course.id).add_users(self.instructor)
@@ -118,60 +119,93 @@ def _get_url(self, course_id=None):
118119
course_id = str(self.course_key)
119120
return reverse('instructor_api_v2:course_metadata', kwargs={'course_id': course_id})
120121

122+
@override_settings(COURSE_AUTHORING_MICROFRONTEND_URL='http://localhost:2001/authoring')
123+
@override_settings(ADMIN_CONSOLE_MICROFRONTEND_URL='http://localhost:2025/admin-console')
121124
def test_get_course_metadata_as_instructor(self):
122125
"""
123126
Test that an instructor can retrieve comprehensive course metadata.
124127
"""
125128
self.client.force_authenticate(user=self.instructor)
126129
response = self.client.get(self._get_url())
127130

128-
self.assertEqual(response.status_code, status.HTTP_200_OK) # noqa: PT009
131+
assert response.status_code == status.HTTP_200_OK
129132
data = response.data
130133

131134
# Verify basic course information
132-
self.assertEqual(data['course_id'], str(self.course_key)) # noqa: PT009
133-
self.assertEqual(data['display_name'], 'Demonstration Course') # noqa: PT009
134-
self.assertEqual(data['org'], 'edX') # noqa: PT009
135-
self.assertEqual(data['course_number'], 'DemoX') # noqa: PT009
136-
self.assertEqual(data['course_run'], 'Demo_Course') # noqa: PT009
137-
self.assertEqual(data['pacing'], 'instructor') # noqa: PT009
135+
assert data['course_id'] == str(self.course_key)
136+
assert data['display_name'] == 'Demonstration Course'
137+
assert data['org'] == 'edX'
138+
assert data['course_number'] == 'DemoX'
139+
assert data['course_run'] == 'Demo_Course'
140+
assert data['pacing'] == 'instructor'
138141

139142
# Verify enrollment counts structure
140-
self.assertIn('enrollment_counts', data) # noqa: PT009
141-
self.assertIn('total', data['enrollment_counts']) # noqa: PT009
142-
self.assertIn('total_enrollment', data) # noqa: PT009
143-
self.assertGreaterEqual(data['total_enrollment'], 3) # noqa: PT009
143+
assert 'enrollment_counts' in data
144+
assert 'total' in data['enrollment_counts']
145+
assert 'total_enrollment' in data
146+
assert data['total_enrollment'] >= 3
144147

145148
# Verify role-based enrollment counts are present
146-
self.assertIn('learner_count', data) # noqa: PT009
147-
self.assertIn('staff_count', data) # noqa: PT009
148-
self.assertEqual(data['total_enrollment'], data['learner_count'] + data['staff_count']) # noqa: PT009
149+
assert 'learner_count' in data
150+
assert 'staff_count' in data
151+
assert data['total_enrollment'] == data['learner_count'] + data['staff_count']
149152

150153
# Verify permissions structure
151-
self.assertIn('permissions', data) # noqa: PT009
154+
assert 'permissions' in data
152155
permissions_data = data['permissions']
153-
self.assertIn('admin', permissions_data) # noqa: PT009
154-
self.assertIn('instructor', permissions_data) # noqa: PT009
155-
self.assertIn('staff', permissions_data) # noqa: PT009
156-
self.assertIn('forum_admin', permissions_data) # noqa: PT009
157-
self.assertIn('finance_admin', permissions_data) # noqa: PT009
158-
self.assertIn('sales_admin', permissions_data) # noqa: PT009
159-
self.assertIn('data_researcher', permissions_data) # noqa: PT009
156+
assert 'admin' in permissions_data
157+
assert 'instructor' in permissions_data
158+
assert 'staff' in permissions_data
159+
assert 'forum_admin' in permissions_data
160+
assert 'finance_admin' in permissions_data
161+
assert 'sales_admin' in permissions_data
162+
assert 'data_researcher' in permissions_data
160163

161164
# Verify sections structure
162-
self.assertIn('tabs', data) # noqa: PT009
163-
self.assertIsInstance(data['tabs'], list) # noqa: PT009
165+
assert 'tabs' in data
166+
assert isinstance(data['tabs'], list)
164167

165168
# Verify other metadata fields
166-
self.assertIn('num_sections', data) # noqa: PT009
167-
self.assertIn('tabs', data) # noqa: PT009
168-
self.assertIn('grade_cutoffs', data) # noqa: PT009
169-
self.assertIn('course_errors', data) # noqa: PT009
170-
self.assertIn('studio_url', data) # noqa: PT009
171-
self.assertIn('disable_buttons', data) # noqa: PT009
172-
self.assertIn('has_started', data) # noqa: PT009
173-
self.assertIn('has_ended', data) # noqa: PT009
174-
self.assertIn('analytics_dashboard_message', data) # noqa: PT009
169+
assert 'num_sections' in data
170+
assert 'grade_cutoffs' in data
171+
assert 'course_errors' in data
172+
assert 'studio_url' in data
173+
assert 'disable_buttons' in data
174+
assert 'has_started' in data
175+
assert 'has_ended' in data
176+
assert 'analytics_dashboard_message' in data
177+
assert 'studio_grading_url' in data
178+
assert 'admin_console_url' in data
179+
180+
assert data['studio_grading_url'] == f'http://localhost:2001/authoring/course/{self.course.id}/settings/grading'
181+
assert data['admin_console_url'] == 'http://localhost:2025/admin-console/authz'
182+
183+
@override_settings(ADMIN_CONSOLE_MICROFRONTEND_URL='http://localhost:2025/admin-console')
184+
def test_admin_console_url_requires_instructor_access(self):
185+
"""
186+
Test that the admin console URL is only available to users with instructor access.
187+
"""
188+
# data researcher has access to course but is not an instructor
189+
self.client.force_authenticate(user=self.data_researcher)
190+
response = self.client.get(self._get_url())
191+
192+
assert response.status_code == status.HTTP_200_OK
193+
assert 'admin_console_url' in response.data
194+
data = response.data
195+
assert data['admin_console_url'] is None
196+
197+
@override_settings(ADMIN_CONSOLE_MICROFRONTEND_URL='http://localhost:2025/admin-console')
198+
def test_django_staff_user_without_instructor_access_can_see_admin_console_url(self):
199+
"""
200+
Test that Django staff users without instructor access can see the admin console URL.
201+
"""
202+
self.client.force_authenticate(user=self.django_staff_user)
203+
response = self.client.get(self._get_url())
204+
205+
assert response.status_code == status.HTTP_200_OK
206+
assert 'admin_console_url' in response.data
207+
data = response.data
208+
assert data['admin_console_url'] == 'http://localhost:2025/admin-console/authz'
175209

176210
def test_get_course_metadata_as_staff(self):
177211
"""

lms/djangoapps/instructor/views/api_v2.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ def get(self, request, course_id):
234234
"grade_cutoffs": "A is 0.9, B is 0.8, C is 0.7, D is 0.6",
235235
"course_errors": [],
236236
"studio_url": "https://studio.example.com/course/course-v1:edX+DemoX+2024",
237+
# May be null if user does not have access:
238+
"admin_console_url": "http://apps.local.openedx.io:2025/admin-console/authz",
237239
"permissions": {
238240
"admin": false,
239241
"instructor": true,

lms/djangoapps/instructor/views/serializers_v2.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from lms.djangoapps.instructor.access import FORUM_ROLES, ROLES
3535
from lms.djangoapps.instructor.views.instructor_dashboard import get_analytics_dashboard_message
3636
from openedx.core.djangoapps.django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
37+
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
3738
from xmodule.modulestore.django import modulestore
3839

3940
from .tools import DashboardError, get_student_from_identifier, parse_datetime
@@ -77,6 +78,9 @@ class CourseInformationSerializerV2(serializers.Serializer):
7778
studio_grading_url = serializers.SerializerMethodField(
7879
help_text="URL to the Studio grading settings page for the course (null if not configured)"
7980
)
81+
admin_console_url = serializers.SerializerMethodField(
82+
help_text="URL to the admin console (requires instructor access and MFE configuration, null if not accessible)"
83+
)
8084
permissions = serializers.SerializerMethodField(help_text="User permissions for instructor dashboard features")
8185
tabs = serializers.SerializerMethodField(help_text="List of course tabs with configuration and display information")
8286
disable_buttons = serializers.SerializerMethodField(
@@ -462,10 +466,27 @@ def get_gradebook_url(self, data):
462466
def get_studio_grading_url(self, data):
463467
"""Get Studio MFE grading settings URL for the course."""
464468
course_key = data['course'].id
465-
mfe_base_url = getattr(settings, 'COURSE_AUTHORING_MICROFRONTEND_URL', None)
466-
if mfe_base_url:
467-
return f'{mfe_base_url}/course/{course_key}/settings/grading'
468-
return None
469+
mfe_base_url = configuration_helpers.get_value(
470+
'COURSE_AUTHORING_MICROFRONTEND_URL',
471+
getattr(settings, 'COURSE_AUTHORING_MICROFRONTEND_URL', None)
472+
)
473+
if not mfe_base_url:
474+
return None
475+
return f'{mfe_base_url}/course/{course_key}/settings/grading'
476+
477+
def get_admin_console_url(self, data):
478+
"""Get admin console URL (requires instructor access and MFE configuration, null if not accessible)."""
479+
request = data['request']
480+
has_instructor_access = has_access(request.user, 'instructor', data['course'])
481+
mfe_base_url = configuration_helpers.get_value(
482+
'ADMIN_CONSOLE_MICROFRONTEND_URL',
483+
getattr(settings, 'ADMIN_CONSOLE_MICROFRONTEND_URL', None)
484+
)
485+
486+
has_permissions = request.user.is_staff or has_instructor_access
487+
if not mfe_base_url or not has_permissions:
488+
return None
489+
return f'{mfe_base_url}/authz'
469490

470491
def get_disable_buttons(self, data):
471492
"""Check if buttons should be disabled for large courses."""

0 commit comments

Comments
 (0)