feat: AuthZ for course authoring compatibility layer (#38013)

This commit is contained in:
Rodrigo Mendez
2026-03-06 10:35:17 -06:00
committed by GitHub
parent 0c5e96d566
commit 12a46e6463
26 changed files with 628 additions and 174 deletions

View File

@@ -8,6 +8,7 @@ from datetime import datetime
from unittest import mock
import ddt
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
import pytest
from ccx_keys.locator import CCXLocator
from django.conf import settings
@@ -34,7 +35,7 @@ from openedx.core.djangoapps.content.block_structure.api import get_course_in_ca
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
@mock.patch.dict(
@@ -234,7 +235,7 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__ = True
# TODO: decrease query count as part of REVO-28
QUERY_COUNT = 34
QUERY_COUNT = 36
TEST_DATA = {
('no_overrides', 1, True, False): (QUERY_COUNT, 2),

View File

@@ -10,6 +10,7 @@ from django.test.client import RequestFactory
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.content.block_structure.api import clear_course_from_cache
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import SampleCourseFactory, check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order
@@ -17,6 +18,8 @@ from xmodule.modulestore.tests.sample_courses import BlockInfo # lint-amnesty,
from ..api import get_blocks
QUERY_COUNT_TABLE_IGNORELIST = AUTHZ_TABLES
class TestGetBlocks(SharedModuleStoreTestCase):
"""
@@ -196,7 +199,7 @@ class TestGetBlocksQueryCountsBase(SharedModuleStoreTestCase):
get_blocks on the given course.
"""
with check_mongo_calls(expected_mongo_queries):
with self.assertNumQueries(expected_sql_queries):
with self.assertNumQueries(expected_sql_queries, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
get_blocks(self.request, course.location, self.user)
@@ -212,11 +215,11 @@ class TestGetBlocksQueryCounts(TestGetBlocksQueryCountsBase):
self._get_blocks(
course,
expected_mongo_queries=0,
expected_sql_queries=14,
expected_sql_queries=16,
)
@ddt.data(
(ModuleStoreEnum.Type.split, 2, 24),
(ModuleStoreEnum.Type.split, 2, 26),
)
@ddt.unpack
def test_query_counts_uncached(self, store_type, expected_mongo_queries, num_sql_queries):

View File

@@ -16,10 +16,14 @@ from lms.djangoapps.gating import api as lms_gating_api
import openedx.core.djangoapps.content.block_structure.api as bs_api
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
from openedx.core.djangoapps.course_apps.toggles import EXAMS_IDA
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
from openedx.core.lib.gating import api as gating_api
from ..milestones import MilestonesAndSpecialExamsTransformer
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
@ddt.ddt
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True})
@@ -171,7 +175,7 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
# get data back. This would happen as a part of publishing in a production system.
bs_api.update_course_in_cache(self.course.id)
with self.assertNumQueries(4):
with self.assertNumQueries(6, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
self.get_blocks_and_check_against_expected(self.user, expected_blocks_before_completion)
# clear the request cache to simulate a new request
@@ -184,7 +188,7 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
self.user,
)
with self.assertNumQueries(4):
with self.assertNumQueries(4, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
self.get_blocks_and_check_against_expected(self.user, self.ALL_BLOCKS_EXCEPT_SPECIAL)
def test_staff_access(self):

View File

@@ -7,6 +7,7 @@ import datetime
import itertools
from unittest.mock import Mock, patch
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
import pytest
import ddt
import pytz
@@ -70,7 +71,7 @@ from openedx.features.enterprise_support.tests.factories import (
)
from crum import set_current_request
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
# pylint: disable=protected-access
@@ -878,16 +879,16 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
if user_attr_name == 'user_staff' and action == 'see_exists':
# always checks staff role, and if the course has started, check the duration configuration
if course_attr_name == 'course_started':
num_queries = 2
else:
num_queries = 1
elif user_attr_name == 'user_normal' and action == 'see_exists':
if course_attr_name == 'course_started':
num_queries = 4
else:
num_queries = 3
elif user_attr_name == 'user_normal' and action == 'see_exists':
if course_attr_name == 'course_started':
num_queries = 6
else:
# checks staff role and enrollment data
num_queries = 2
num_queries = 4
elif user_attr_name == 'user_anonymous' and action == 'see_exists':
if course_attr_name == 'course_started':
num_queries = 1
@@ -896,7 +897,7 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
else:
# if the course has started, check the duration configuration
if action == 'see_exists' and course_attr_name == 'course_started':
num_queries = 3
num_queries = 5
else:
num_queries = 0
@@ -950,17 +951,17 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
if user_attr_name == 'user_staff':
if course_attr_name == 'course_started':
# read: CourseAccessRole + django_comment_client.Role
num_queries = 2
num_queries = 4
else:
# read: CourseAccessRole + EnterpriseCourseEnrollment
num_queries = 2
num_queries = 4
elif user_attr_name == 'user_normal':
if course_attr_name == 'course_started':
# read: CourseAccessRole + django_comment_client.Role + FBEEnrollmentExclusion + CourseMode
num_queries = 4
num_queries = 6
else:
# read: CourseAccessRole + CourseEnrollmentAllowed + EnterpriseCourseEnrollment
num_queries = 3
num_queries = 5
elif user_attr_name == 'user_anonymous':
if course_attr_name == 'course_started':
# read: CourseMode

View File

@@ -4,6 +4,7 @@ Test for lms courseware app, module data (runtime data storage for XBlocks)
import json
from functools import partial
from unittest.mock import Mock, patch
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES, FilteredQueryCountMixin
import pytest
from django.db import connections, DatabaseError
@@ -13,6 +14,7 @@ from xblock.exceptions import KeyValueMultiSaveError
from xblock.fields import BlockScope, Scope, ScopeIds
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from lms.djangoapps.courseware.model_data import DjangoKeyValueStore, FieldDataCache, InvalidScopeError
from lms.djangoapps.courseware.models import (
StudentModule,
@@ -27,6 +29,8 @@ from lms.djangoapps.courseware.tests.factories import StudentModuleFactory as cm
from lms.djangoapps.courseware.tests.factories import StudentPrefsFactory
from lms.djangoapps.courseware.tests.factories import UserStateSummaryFactory
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
def mock_field(scope, name):
field = Mock()
@@ -239,7 +243,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
assert exception_context.value.saved_field_names == []
class TestMissingStudentModule(TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
class TestMissingStudentModule(FilteredQueryCountMixin, TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
# Tell Django to clean out all databases, not just default
databases = set(connections)
@@ -276,7 +280,9 @@ class TestMissingStudentModule(TestCase): # lint-amnesty, pylint: disable=missi
# on the StudentModule).
# Django 1.8 also has a number of other BEGIN and SAVESTATE queries.
with self.assertNumQueries(4, using='default'):
with self.assertNumQueries(2, using='student_module_history'):
with self.assertNumQueries(2,
using='student_module_history',
table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
self.kvs.set(user_state_key('a_field'), 'a_value')
assert 1 == sum(len(cache) for cache in self.field_data_cache.cache.values())

View File

@@ -89,7 +89,7 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
from openedx.core.djangoapps.credit.api import set_credit_requirements
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import get_mock_request
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES, get_mock_request
from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE
from openedx.core.lib.url_utils import quote_slashes
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
@@ -108,7 +108,7 @@ from openedx.features.enterprise_support.tests.mixins.enterprise import Enterpri
from openedx.features.enterprise_support.api import add_enterprise_customer_to_session
from enterprise.api.v1.serializers import EnterpriseCustomerSerializer
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
FEATURES_WITH_DISABLE_HONOR_CERTIFICATE = settings.FEATURES.copy()
FEATURES_WITH_DISABLE_HONOR_CERTIFICATE['DISABLE_HONOR_CERTIFICATES'] = True
@@ -1283,8 +1283,8 @@ class ProgressPageTests(ProgressPageBaseTests):
self.assertContains(resp, "earned a certificate for this course.")
@ddt.data(
(True, 54),
(False, 54),
(True, 56),
(False, 56),
)
@ddt.unpack
def test_progress_queries_paced_courses(self, self_paced, query_count):
@@ -1299,7 +1299,7 @@ class ProgressPageTests(ProgressPageBaseTests):
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
self.setup_course()
with self.assertNumQueries(
54, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST
56, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST
), check_mongo_calls(2):
self._get_progress_page()

View File

@@ -156,8 +156,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
assert mock_block_structure_create.call_count == 1
@ddt.data(
(ModuleStoreEnum.Type.split, 1, 42, True),
(ModuleStoreEnum.Type.split, 1, 42, False),
(ModuleStoreEnum.Type.split, 1, 47, True),
(ModuleStoreEnum.Type.split, 1, 47, False),
)
@ddt.unpack
def test_query_counts(self, default_store, num_mongo_calls, num_sql_calls, create_multiple_subsections):
@@ -168,7 +168,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self._apply_recalculate_subsection_grade()
@ddt.data(
(ModuleStoreEnum.Type.split, 1, 42),
(ModuleStoreEnum.Type.split, 1, 47),
)
@ddt.unpack
def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls):
@@ -256,7 +256,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
UserPartition.scheme_extensions = None
@ddt.data(
(ModuleStoreEnum.Type.split, 1, 42),
(ModuleStoreEnum.Type.split, 1, 47),
)
@ddt.unpack
def test_persistent_grades_on_course(self, default_store, num_mongo_queries, num_sql_queries):

View File

@@ -16,6 +16,7 @@ from datetime import datetime, timedelta
from unittest.mock import ANY, MagicMock, Mock, patch
import ddt
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
import pytest
import unicodecsv
from django.conf import settings
@@ -85,6 +86,8 @@ _TEAMS_CONFIG = TeamsConfig({
})
USE_ON_DISK_GRADE_REPORT = 'lms.djangoapps.instructor_task.tasks_helper.grades.use_on_disk_grade_reporting'
QUERY_COUNT_TABLE_IGNORELIST = AUTHZ_TABLES
class InstructorGradeReportTestCase(TestReportMixin, InstructorTaskCourseTestCase):
""" Base class for grade report tests. """
@@ -411,7 +414,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'):
with check_mongo_calls(2):
with self.assertNumQueries(46):
with self.assertNumQueries(48, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
CourseGradeReport.generate(None, None, course.id, {}, 'graded')
def test_inactive_enrollments(self):
@@ -2215,7 +2218,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
'failed': 0,
'skipped': 2
}
with self.assertNumQueries(61):
with self.assertNumQueries(69, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
self.assertCertificatesGenerated(task_input, expected_results)
@ddt.data(

View File

@@ -10,10 +10,14 @@ from django.test.client import RequestFactory
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from lms.djangoapps.teams.serializers import BulkTeamCountTopicSerializer, MembershipSerializer, TopicSerializer
from lms.djangoapps.teams.tests.factories import CourseTeamFactory, CourseTeamMembershipFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
from openedx.core.lib.teams_config import TeamsConfig
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
class SerializerTestCase(SharedModuleStoreTestCase):
"""
@@ -75,7 +79,9 @@ class TopicSerializerTestCase(SerializerTestCase):
Verifies that the `TopicSerializer` correctly displays a topic with a
team count of 0, and that it takes a known number of SQL queries.
"""
with self.assertNumQueries(3): # 2 split modulestore MySQL queries, 1 for Teams
with self.assertNumQueries(
3, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST
): # 2 split modulestore MySQL queries, 1 for Teams
serializer = TopicSerializer(
self.course.teamsets[0].cleaned_data,
context={'course_id': self.course.id},
@@ -91,7 +97,9 @@ class TopicSerializerTestCase(SerializerTestCase):
CourseTeamFactory.create(
course_id=self.course.id, topic_id=self.course.teamsets[0].teamset_id
)
with self.assertNumQueries(3): # 2 split modulestore MySQL queries, 1 for Teams
with self.assertNumQueries(
3, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST
): # 2 split modulestore MySQL queries, 1 for Teams
serializer = TopicSerializer(
self.course.teamsets[0].cleaned_data,
context={'course_id': self.course.id},
@@ -110,7 +118,9 @@ class TopicSerializerTestCase(SerializerTestCase):
)
CourseTeamFactory.create(course_id=self.course.id, topic_id=duplicate_topic['id'])
CourseTeamFactory.create(course_id=second_course.id, topic_id=duplicate_topic['id'])
with self.assertNumQueries(3): # 2 split modulestore MySQL queries, 1 for Teams
with self.assertNumQueries(
3, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST
): # 2 split modulestore MySQL queries, 1 for Teams
serializer = TopicSerializer(
self.course.teamsets[0].cleaned_data,
context={'course_id': self.course.id},
@@ -163,7 +173,7 @@ class BaseTopicSerializerTestCase(SerializerTestCase):
"""
Verify that the serializer produced the expected topics.
"""
with self.assertNumQueries(num_queries):
with self.assertNumQueries(num_queries, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
page = Paginator(
self.course.teams_configuration.cleaned_data['teamsets'],
self.PAGE_SIZE,
@@ -203,7 +213,7 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
query.
"""
topics = self.setup_topics(teams_per_topic=0)
self.assert_serializer_output(topics, num_teams_per_topic=0, num_queries=2)
self.assert_serializer_output(topics, num_teams_per_topic=0, num_queries=4)
def test_topics_with_team_counts(self):
"""
@@ -212,7 +222,7 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
"""
teams_per_topic = 10
topics = self.setup_topics(teams_per_topic=teams_per_topic)
self.assert_serializer_output(topics, num_teams_per_topic=teams_per_topic, num_queries=2)
self.assert_serializer_output(topics, num_teams_per_topic=teams_per_topic, num_queries=4)
def test_subset_of_topics(self):
"""
@@ -221,7 +231,7 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
"""
teams_per_topic = 10
topics = self.setup_topics(num_topics=self.NUM_TOPICS, teams_per_topic=teams_per_topic)
self.assert_serializer_output(topics, num_teams_per_topic=teams_per_topic, num_queries=2)
self.assert_serializer_output(topics, num_teams_per_topic=teams_per_topic, num_queries=4)
def test_scoped_within_course(self):
"""Verify that team counts are scoped within a course."""
@@ -235,7 +245,7 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
}),
)
CourseTeamFactory.create(course_id=second_course.id, topic_id=duplicate_topic['id'])
self.assert_serializer_output(first_course_topics, num_teams_per_topic=teams_per_topic, num_queries=2)
self.assert_serializer_output(first_course_topics, num_teams_per_topic=teams_per_topic, num_queries=4)
def _merge_dicts(self, first, second):
"""Convenience method to merge two dicts in a single expression"""
@@ -251,7 +261,9 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
request = RequestFactory().get('/api/team/v0/topics')
request.user = self.user
with self.assertNumQueries(num_queries + 2): # num_queries on teams tables, plus 2 split modulestore queries
with self.assertNumQueries(
num_queries + 2, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST
): # num_queries on teams tables, plus 2 split modulestore queries
serializer = self.serializer(
topics,
context={
@@ -269,4 +281,4 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
with no topics.
"""
self.course.teams_configuration = TeamsConfig({'topics': []})
self.assert_serializer_output([], num_teams_per_topic=0, num_queries=1)
self.assert_serializer_output([], num_teams_per_topic=0, num_queries=3)