diff --git a/.github/workflows/docker-compose.yml.mysqldbdump b/.github/workflows/docker-compose.yml.mysqldbdump index 6affa8c788..87f0321374 100644 --- a/.github/workflows/docker-compose.yml.mysqldbdump +++ b/.github/workflows/docker-compose.yml.mysqldbdump @@ -2,7 +2,7 @@ version: '3' services: mysql: image: mysql:5.7 - container_name: edx.devstack.mysql57 + container_name: edx.devstack.mysql80 ports: - '3306:3306' environment: diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index b6861e4ddc..b7a249bfa5 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -256,7 +256,7 @@ def update_special_exams_and_publish(course_key_str): """ from cms.djangoapps.contentstore.exams import register_exams from cms.djangoapps.contentstore.proctoring import register_special_exams as register_exams_legacy - from openedx.core.djangoapps.credit.signals import on_course_publish + from openedx.core.djangoapps.credit.signals.handlers import on_course_publish course_key = CourseKey.from_string(course_key_str) LOGGER.info('Attempting to register exams for course %s', course_key_str) diff --git a/cms/djangoapps/contentstore/tests/test_course_create_rerun.py b/cms/djangoapps/contentstore/tests/test_course_create_rerun.py index 264d6de8ff..fcba8f6e1b 100644 --- a/cms/djangoapps/contentstore/tests/test_course_create_rerun.py +++ b/cms/djangoapps/contentstore/tests/test_course_create_rerun.py @@ -208,6 +208,10 @@ class TestCourseListing(ModuleStoreTestCase): self.assertEqual(response.status_code, 403) @override_settings(FEATURES={'ENABLE_CREATOR_GROUP': True}) + @mock.patch( + 'cms.djangoapps.course_creators.admin.render_to_string', + mock.Mock(side_effect=mock_render_to_string, autospec=True) + ) def test_course_creation_when_user_in_org_with_creator_role(self): """ Tests course creation with user having the organization content creation role. @@ -218,6 +222,9 @@ class TestCourseListing(ModuleStoreTestCase): 'description': 'Testing Organization Description', }) update_org_role(self.global_admin, OrgContentCreatorRole, self.user, [self.source_course_key.org]) + self.course_creator_entry.all_organizations = True + self.course_creator_entry.state = CourseCreator.GRANTED + self.creator_admin.save_model(self.request, self.course_creator_entry, None, True) self.assertIn(self.source_course_key.org, get_allowed_organizations(self.user)) response = self.client.ajax_post(self.course_create_rerun_url, { 'org': self.source_course_key.org, diff --git a/cms/djangoapps/contentstore/tests/test_tasks.py b/cms/djangoapps/contentstore/tests/test_tasks.py index d0fcd78210..cf82a6d165 100644 --- a/cms/djangoapps/contentstore/tests/test_tasks.py +++ b/cms/djangoapps/contentstore/tests/test_tasks.py @@ -195,7 +195,7 @@ class RegisterExamsTaskTestCase(CourseTestCase): # pylint: disable=missing-clas @mock.patch('cms.djangoapps.contentstore.proctoring.register_special_exams') def test_register_exams_failure(self, _mock_register_exams_proctoring, _mock_register_exams_service): """ credit requirements update signal fires even if exam registration fails """ - with mock.patch('openedx.core.djangoapps.credit.signals.on_course_publish') as course_publish: + with mock.patch('openedx.core.djangoapps.credit.signals.handlers.on_course_publish') as course_publish: _mock_register_exams_proctoring.side_effect = Exception('boom!') update_special_exams_and_publish(str(self.course.id)) course_publish.assert_called() diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 4a70e5028c..a55bb3db9a 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -1786,7 +1786,7 @@ def get_organizations(user): Returns the list of organizations for which the user is allowed to create courses. """ course_creator = CourseCreator.objects.filter(user=user).first() - if not course_creator: + if not course_creator or course_creator.state != CourseCreator.GRANTED: return [] elif course_creator.all_organizations: organizations = Organization.objects.all().values_list('short_name', flat=True) diff --git a/cms/djangoapps/contentstore/views/library.py b/cms/djangoapps/contentstore/views/library.py index 2d1501d420..17aa24c571 100644 --- a/cms/djangoapps/contentstore/views/library.py +++ b/cms/djangoapps/contentstore/views/library.py @@ -83,8 +83,9 @@ def user_can_create_library(user, org=None): is_course_creator = get_course_creator_status(user) == 'granted' has_org_staff_role = OrgStaffRole().get_orgs_for_user(user).exists() has_course_staff_role = UserBasedRole(user=user, role=CourseStaffRole.ROLE).courses_with_role().exists() + has_course_admin_role = UserBasedRole(user=user, role=CourseInstructorRole.ROLE).courses_with_role().exists() - return is_course_creator or has_org_staff_role or has_course_staff_role + return is_course_creator or has_org_staff_role or has_course_staff_role or has_course_admin_role else: # EDUCATOR-1924: DISABLE_LIBRARY_CREATION overrides DISABLE_COURSE_CREATION, if present. disable_library_creation = settings.FEATURES.get('DISABLE_LIBRARY_CREATION', None) diff --git a/cms/djangoapps/contentstore/views/tests/test_credit_eligibility.py b/cms/djangoapps/contentstore/views/tests/test_credit_eligibility.py index 3d6d2d9e81..5cfa2ded2b 100644 --- a/cms/djangoapps/contentstore/views/tests/test_credit_eligibility.py +++ b/cms/djangoapps/contentstore/views/tests/test_credit_eligibility.py @@ -9,7 +9,7 @@ from cms.djangoapps.contentstore.tests.utils import CourseTestCase from cms.djangoapps.contentstore.utils import reverse_course_url from openedx.core.djangoapps.credit.api import get_credit_requirements from openedx.core.djangoapps.credit.models import CreditCourse -from openedx.core.djangoapps.credit.signals import on_course_publish +from openedx.core.djangoapps.credit.signals.handlers import on_course_publish from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order diff --git a/cms/djangoapps/contentstore/views/tests/test_library.py b/cms/djangoapps/contentstore/views/tests/test_library.py index bac450b3bd..f6b7a48a68 100644 --- a/cms/djangoapps/contentstore/views/tests/test_library.py +++ b/cms/djangoapps/contentstore/views/tests/test_library.py @@ -19,7 +19,7 @@ from organizations.exceptions import InvalidOrganizationException from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase, parse_json from cms.djangoapps.contentstore.utils import reverse_course_url, reverse_library_url from cms.djangoapps.course_creators.views import add_user_with_status_granted as grant_course_creator_status -from common.djangoapps.student.roles import LibraryUserRole, CourseStaffRole +from common.djangoapps.student.roles import LibraryUserRole, CourseStaffRole, CourseInstructorRole from xmodule.modulestore.tests.factories import LibraryFactory # lint-amnesty, pylint: disable=wrong-import-order from cms.djangoapps.course_creators.models import CourseCreator @@ -101,6 +101,14 @@ class UnitTestLibraries(CourseTestCase): auth.add_users(self.user, CourseStaffRole(self.course.id), nostaff_user) self.assertEqual(user_can_create_library(nostaff_user), True) + # When creator groups are enabled, course instructor members can create libraries + @mock.patch("cms.djangoapps.contentstore.views.library.LIBRARIES_ENABLED", True) + def test_library_creator_status_with_course_instructor_role_for_enabled_creator_group_setting(self): + _, nostaff_user = self.create_non_staff_authed_user_client() + with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}): + auth.add_users(self.user, CourseInstructorRole(self.course.id), nostaff_user) + self.assertEqual(user_can_create_library(nostaff_user), True) + @ddt.data( (False, False, True), (False, True, False), @@ -480,9 +488,14 @@ class UnitTestLibraries(CourseTestCase): # Assert that the method returned the expected value self.assertEqual(organizations, []) with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}): - organizations = get_allowed_organizations_for_libraries(self.user) - # Assert that the method returned the expected value - self.assertEqual(organizations, ['org1', 'org2']) + # Assert that correct org values are returned based on course creator state + for course_creator_state in CourseCreator.STATES: + course_creator.state = course_creator_state + organizations = get_allowed_organizations_for_libraries(self.user) + if course_creator_state != CourseCreator.GRANTED: + self.assertEqual(organizations, []) + else: + self.assertEqual(organizations, ['org1', 'org2']) with mock.patch.dict( 'django.conf.settings.FEATURES', {"ENABLE_ORGANIZATION_STAFF_ACCESS_FOR_CONTENT_LIBRARIES": True} diff --git a/cms/envs/common.py b/cms/envs/common.py index 2021d372bc..bcb7665210 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2787,3 +2787,7 @@ SPECTACULAR_SETTINGS = { 'SERVE_INCLUDE_SCHEMA': False, 'PREPROCESSING_HOOKS': ['cms.lib.spectacular.cms_api_filter'], # restrict spectacular to CMS API endpoints } + +#### Event bus publishing #### +## Will be more filled out as part of https://github.com/edx/edx-arch-experiments/issues/381 +EVENT_BUS_PRODUCER_CONFIG = {} diff --git a/cms/envs/devstack-experimental.yml b/cms/envs/devstack-experimental.yml index 969e932937..e33c024b0f 100644 --- a/cms/envs/devstack-experimental.yml +++ b/cms/envs/devstack-experimental.yml @@ -208,7 +208,7 @@ DATABASES: ATOMIC_REQUESTS: true CONN_MAX_AGE: 0 ENGINE: django.db.backends.mysql - HOST: edx.devstack.mysql57 + HOST: edx.devstack.mysql80 NAME: edxapp OPTIONS: isolation_level: read committed @@ -218,7 +218,7 @@ DATABASES: read_replica: CONN_MAX_AGE: 0 ENGINE: django.db.backends.mysql - HOST: edx.devstack.mysql57 + HOST: edx.devstack.mysql80 NAME: edxapp OPTIONS: isolation_level: read committed @@ -228,7 +228,7 @@ DATABASES: student_module_history: CONN_MAX_AGE: 0 ENGINE: django.db.backends.mysql - HOST: edx.devstack.mysql57 + HOST: edx.devstack.mysql80 NAME: edxapp_csmh OPTIONS: isolation_level: read committed diff --git a/common/djangoapps/util/tests/test_db.py b/common/djangoapps/util/tests/test_db.py index 77f2ac07f0..4a16c2a20a 100644 --- a/common/djangoapps/util/tests/test_db.py +++ b/common/djangoapps/util/tests/test_db.py @@ -1,10 +1,8 @@ """Tests for util.db module.""" -from importlib.metadata import version from io import StringIO import ddt -import pytest from django.core.management import call_command from django.db.transaction import TransactionManagementError, atomic from django.test import TestCase, TransactionTestCase @@ -12,8 +10,6 @@ from django.test.utils import override_settings from common.djangoapps.util.db import enable_named_outer_atomic, generate_int_id, outer_atomic -GET_DJANGO_VERSION = int(version('django').split('.')[0]) - def do_nothing(): """Just return.""" @@ -119,7 +115,6 @@ class GenerateIntIdTestCase(TestCase): assert int_id in list(set(range(minimum, (maximum + 1))) - used_ids) -@pytest.mark.skipif(GET_DJANGO_VERSION > 3, reason="django4.2 brings new migrations, so only run for dj32 for now.") class MigrationTests(TestCase): """ Tests for migrations. diff --git a/lms/envs/bok_choy_docker.yml b/lms/envs/bok_choy_docker.yml new file mode 100644 index 0000000000..b68beb648d --- /dev/null +++ b/lms/envs/bok_choy_docker.yml @@ -0,0 +1,184 @@ +# ingested edx-platform/lms/envs/bok_choy_docker.auth.json +# ingested edx-platform/lms/envs/bok_choy_docker.env.json +ACTIVATION_EMAIL_SUPPORT_LINK: https://support.example.com/activation-email-help.html +ANALYTICS_DASHBOARD_URL: '' +AWS_ACCESS_KEY_ID: '' +AWS_SECRET_ACCESS_KEY: '' +BUGS_EMAIL: bugs@example.com +BULK_EMAIL_DEFAULT_FROM_EMAIL: no-reply@example.com +CACHES: + celery: + BACKEND: django.core.cache.backends.memcached.PyMemcacheCache + KEY_FUNCTION: common.djangoapps.util.memcache.safe_key + KEY_PREFIX: integration_celery + LOCATION: ['edx.devstack.memcached:11211'] + OPTIONS: + no_delay: true + ignore_exc: true + use_pooling: true + default: + BACKEND: django.core.cache.backends.memcached.PyMemcacheCache + KEY_FUNCTION: common.djangoapps.util.memcache.safe_key + KEY_PREFIX: sandbox_default + LOCATION: ['edx.devstack.memcached:11211'] + OPTIONS: + no_delay: true + ignore_exc: true + use_pooling: true + general: + BACKEND: django.core.cache.backends.memcached.PyMemcacheCache + KEY_FUNCTION: common.djangoapps.util.memcache.safe_key + KEY_PREFIX: sandbox_general + LOCATION: ['edx.devstack.memcached:11211'] + OPTIONS: + no_delay: true + ignore_exc: true + use_pooling: true + mongo_metadata_inheritance: + BACKEND: django.core.cache.backends.memcached.PyMemcacheCache + KEY_FUNCTION: common.djangoapps.util.memcache.safe_key + KEY_PREFIX: integration_mongo_metadata_inheritance + LOCATION: ['edx.devstack.memcached:11211'] + OPTIONS: + no_delay: true + ignore_exc: true + use_pooling: true + staticfiles: + BACKEND: django.core.cache.backends.memcached.PyMemcacheCache + KEY_FUNCTION: common.djangoapps.util.memcache.safe_key + KEY_PREFIX: integration_static_files + LOCATION: ['edx.devstack.memcached:11211'] + OPTIONS: + no_delay: true + ignore_exc: true + use_pooling: true +CELERY_BROKER_HOSTNAME: localhost +CELERY_BROKER_PASSWORD: celery +CELERY_BROKER_TRANSPORT: amqp +CELERY_BROKER_USER: celery +CERT_QUEUE: certificates +CMS_BASE: '** OVERRIDDEN **' +CODE_JAIL: + limits: {REALTIME: 3, VMEM: 0} +COMMENTS_SERVICE_KEY: password +COMMENTS_SERVICE_URL: http://edx.devstack.lms:4567 +CONTACT_EMAIL: info@example.com +CONTENTSTORE: + DOC_STORE_CONFIG: + collection: modulestore + db: test + host: [edx.devstack.mongo] + port: 27017 + ENGINE: xmodule.contentstore.mongo.MongoContentStore + OPTIONS: + db: test + host: [edx.devstack.mongo] + port: 27017 +DATABASES: + default: {ENGINE: django.db.backends.mysql, HOST: edx.devstack.mysql80, NAME: edxtest, + PASSWORD: '', PORT: '3306', USER: root} + student_module_history: {ENGINE: django.db.backends.mysql, HOST: edx.devstack.mysql80, + NAME: student_module_history_test, PASSWORD: '', PORT: '3306', USER: root} +DEFAULT_FEEDBACK_EMAIL: feedback@example.com +DEFAULT_FROM_EMAIL: registration@example.com +DJFS: {aws_access_key_id: test, aws_secret_access_key: test, bucket: test, prefix: test, + type: s3fs} +DOC_STORE_CONFIG: + collection: modulestore + db: test + host: [edx.devstack.mongo] + port: 27017 +EMAIL_BACKEND: django.core.mail.backends.dummy.EmailBackend +EVENT_TRACKING_BACKENDS: + mongo: + ENGINE: eventtracking.backends.mongodb.MongoBackend + OPTIONS: + collection: events + database: test + host: [edx.devstack.mongo] + port: 27017 +FEATURES: {ALLOW_AUTOMATED_SIGNUPS: true, AUTOMATIC_AUTH_FOR_TESTING: true, + AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING: true, CERTIFICATES_HTML_VIEW: true, + CERTIFICATES_INSTRUCTOR_GENERATION: true, CUSTOM_COURSES_EDX: true, + ENABLE_COURSE_DISCOVERY: true, ENABLE_DISCUSSION_SERVICE: true, ENABLE_GRADE_DOWNLOADS: true, + ENABLE_SPECIAL_EXAMS: true, ENABLE_THIRD_PARTY_AUTH: true, + ENABLE_VERIFIED_CERTIFICATES: true, EXPOSE_CACHE_PROGRAMS_ENDPOINT: true, MODE_CREATION_FOR_TESTING: true, + PREVIEW_LMS_BASE: 'preview.localhost:8003', RESTRICT_AUTOMATIC_AUTH: false, SHOW_HEADER_LANGUAGE_SELECTOR: true} +GITHUB_REPO_ROOT: '** OVERRIDDEN **' +JWT_AUTH: {JWT_PRIVATE_SIGNING_JWK: '{"e": "AQAB", "d": "HIiV7KNjcdhVbpn3KT-I9n3JPf5YbGXsCIedmPqDH1d4QhBofuAqZ9zebQuxkRUpmqtYMv0Zi6ECSUqH387GYQF_XvFUFcjQRPycISd8TH0DAKaDpGr-AYNshnKiEtQpINhcP44I1AYNPCwyoxXA1fGTtmkKChsuWea7o8kytwU5xSejvh5-jiqu2SF4GEl0BEXIAPZsgbzoPIWNxgO4_RzNnWs6nJZeszcaDD0CyezVSuH9QcI6g5QFzAC_YuykSsaaFJhZ05DocBsLczShJ9Omf6PnK9xlm26I84xrEh_7x4fVmNBg3xWTLh8qOnHqGko93A1diLRCrKHOvnpvgQ", + "n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ", + "q": "3T3DEtBUka7hLGdIsDlC96Uadx_q_E4Vb1cxx_4Ss_wGp1Loz3N3ZngGyInsKlmbBgLo1Ykd6T9TRvRNEWEtFSOcm2INIBoVoXk7W5RuPa8Cgq2tjQj9ziGQ08JMejrPlj3Q1wmALJr5VTfvSYBu0WkljhKNCy1KB6fCby0C9WE", + "p": "vUqzWPZnDG4IXyo-k5F0bHV0BNL_pVhQoLW7eyFHnw74IOEfSbdsMspNcPSFIrtgPsn7981qv3lN_staZ6JflKfHayjB_lvltHyZxfl0dvruShZOx1N6ykEo7YrAskC_qxUyrIvqmJ64zPW3jkuOYrFs7Ykj3zFx3Zq1H5568G0", + "kid": "BTZ9HA6K", "kty": "RSA"}', JWT_PUBLIC_SIGNING_JWK_SET: '{"keys": [{"kid": + "BTZ9HA6K", "e": "AQAB", "kty": "RSA", "n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ"}]}', + JWT_SECRET_KEY: super-secret-key} +LMS_BASE: http://edx.devstack.lms:18003 +LMS_ROOT_URL: http://edx.devstack.lms:18003 +LOCAL_LOGLEVEL: INFO +LOGGING_ENV: sandbox +LOG_DIR: '** OVERRIDDEN **' +MEDIA_URL: /media/ +MKTG_URL_LINK_MAP: {ABOUT: about, BLOG: blog, CAREERS: careers, CONTACT: contact, + COURSES: courses, DONATE: donate, HELP_CENTER: help-center, HONOR: honor, NEWS: news, + PRESS: press, PRIVACY: privacy, ROOT: root, SITEMAP.XML: sitemap_xml, TOS: tos, + WHAT_IS_VERIFIED_CERT: verified-certificate} +MODULESTORE: + default: + ENGINE: xmodule.modulestore.mixed.MixedModuleStore + OPTIONS: + mappings: {} + stores: + - DOC_STORE_CONFIG: + collection: modulestore + db: test + host: [edx.devstack.mongo] + port: 27017 + ENGINE: xmodule.modulestore.mongo.DraftMongoModuleStore + NAME: draft + OPTIONS: + collection: modulestore + db: test + default_class: xmodule.hidden_block.HiddenBlock + fs_root: '** OVERRIDDEN **' + host: [edx.devstack.mongo] + port: 27017 + render_template: common.djangoapps.edxmako.shortcuts.render_to_string + - ENGINE: xmodule.modulestore.xml.XMLModuleStore + NAME: xml + OPTIONS: {data_dir: '** OVERRIDDEN **', default_class: xmodule.hidden_block.HiddenBlock} +PASSWORD_RESET_SUPPORT_LINK: https://support.example.com/password-reset-help.html +REGISTRATION_EXTENSION_FORM: openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm +REGISTRATION_EXTRA_FIELDS: {city: hidden, country: required, gender: optional, goals: optional, + honor_code: required, level_of_education: optional, mailing_address: optional, terms_of_service: hidden, + year_of_birth: optional} +SECRET_KEY: 'bokchoy_docker_secret_key' +SERVER_EMAIL: devops@example.com +SESSION_COOKIE_DOMAIN: null +SITE_NAME: localhost:8003 +SOCIAL_SHARING_SETTINGS: {CERTIFICATE_FACEBOOK: true, CERTIFICATE_FACEBOOK_TEXT: 'Testing + facebook feature:', CUSTOM_COURSE_URLS: true, DASHBOARD_FACEBOOK: true, DASHBOARD_TWITTER: true, + DASHBOARD_TWITTER_TEXT: 'Testing feature:'} +STATIC_URL_BASE: /static/ +SUPPORT_SITE_LINK: https://support.example.com +SYSLOG_SERVER: '' +TECH_SUPPORT_EMAIL: technical@example.com +THIRD_PARTY_AUTH_BACKENDS: [social_core.backends.google.GoogleOAuth2, social_core.backends.linkedin.LinkedinOAuth2, + social_core.backends.facebook.FacebookOAuth2, common.djangoapps.third_party_auth.dummy.DummyBackend, + common.djangoapps.third_party_auth.saml.SAMLAuthBackend] +TIME_ZONE: America/New_York +TRACKING_BACKENDS: + mongo: + ENGINE: common.djangoapps.track.backends.mongodb.MongoBackend + OPTIONS: + collection: events + database: test + host: [edx.devstack.mongo] + port: 27017 +WIKI_ENABLED: true +XQUEUE_INTERFACE: + basic_auth: [edx, edx] + django_auth: {password: password, username: lms} + url: '** OVERRIDDEN **' +ZENDESK_API_KEY: '' +ZENDESK_USER: '' + diff --git a/lms/envs/common.py b/lms/envs/common.py index 2c12037e92..90d5517fa4 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -5384,3 +5384,7 @@ EXPIRED_NOTIFICATIONS_DELETE_BATCH_SIZE = 10000 #### django-simple-history## # disable indexing on date field its coming from django-simple-history. SIMPLE_HISTORY_DATE_INDEX = False + +#### Event bus publishing #### +## Will be more filled out as part of https://github.com/edx/edx-arch-experiments/issues/381 +EVENT_BUS_PRODUCER_CONFIG = {} diff --git a/lms/envs/devstack-experimental.yml b/lms/envs/devstack-experimental.yml index 5ab37724ae..50958af4d6 100644 --- a/lms/envs/devstack-experimental.yml +++ b/lms/envs/devstack-experimental.yml @@ -228,7 +228,7 @@ DATABASES: ATOMIC_REQUESTS: true CONN_MAX_AGE: 0 ENGINE: django.db.backends.mysql - HOST: edx.devstack.mysql57 + HOST: edx.devstack.mysql80 NAME: edxapp OPTIONS: isolation_level: read committed @@ -238,7 +238,7 @@ DATABASES: read_replica: CONN_MAX_AGE: 0 ENGINE: django.db.backends.mysql - HOST: edx.devstack.mysql57 + HOST: edx.devstack.mysql80 NAME: edxapp OPTIONS: isolation_level: read committed @@ -248,7 +248,7 @@ DATABASES: student_module_history: CONN_MAX_AGE: 0 ENGINE: django.db.backends.mysql - HOST: edx.devstack.mysql57 + HOST: edx.devstack.mysql80 NAME: edxapp_csmh OPTIONS: isolation_level: read committed diff --git a/lms/static/js/learner_dashboard/spec/program_details_view_spec.js b/lms/static/js/learner_dashboard/spec/program_details_view_spec.js index 795667d2de..2387ade00b 100644 --- a/lms/static/js/learner_dashboard/spec/program_details_view_spec.js +++ b/lms/static/js/learner_dashboard/spec/program_details_view_spec.js @@ -544,6 +544,28 @@ describe('Program Details View', () => { .toContainText(body); }; + const testSubscriptionSunsetting = (state, heading, body) => { + const subscriptionData = { + ...options.subscriptionData[0], + subscription_state: state, + }; + // eslint-disable-next-line no-use-before-define + view = initView({ + // eslint-disable-next-line no-undef + programData: $.extend({}, options.programData, { + subscription_eligible: false, + }), + isUserB2CSubscriptionsEnabled: true, + subscriptionData: [subscriptionData], + }); + view.render(); + expect(view.$('.upgrade-subscription')[0]).not.toBeInDOM(); + expect(view.$('.upgrade-subscription .upgrade-button')).not + .toContainText(heading); + expect(view.$('.upgrade-subscription .subscription-info-brief')).not + .toContainText(body); + }; + const initView = (updates) => { // eslint-disable-next-line no-undef const viewOptions = $.extend({}, options, updates); @@ -709,8 +731,8 @@ describe('Program Details View', () => { ); }); - it('should render the get subscription link if program is subscription eligible', () => { - testSubscriptionState( + it('should not render the get subscription link if program is not active', () => { + testSubscriptionSunsetting( 'pre', 'Start 7-day free trial', '$100/month USD subscription after trial ends. Cancel anytime.', @@ -734,8 +756,8 @@ describe('Program Details View', () => { ); }); - it('should render appropriate subscription text when subscription is inactive', () => { - testSubscriptionState( + it('should not render appropriate subscription text when subscription is inactive', () => { + testSubscriptionSunsetting( 'inactive', 'Restart my subscription', '$100/month USD subscription. Cancel anytime.', diff --git a/lms/static/js/learner_dashboard/spec/sidebar_view_spec.js b/lms/static/js/learner_dashboard/spec/sidebar_view_spec.js index c1f7e4734e..04c936908e 100644 --- a/lms/static/js/learner_dashboard/spec/sidebar_view_spec.js +++ b/lms/static/js/learner_dashboard/spec/sidebar_view_spec.js @@ -32,35 +32,10 @@ describe('Sidebar View', () => { expect(view).toBeDefined(); }); - it('should not render the subscription upsell section if B2CSubscriptions are disabled', () => { - view.remove(); - view = new SidebarView({ - el: '.sidebar', - context: { - ...context, - isUserB2CSubscriptionsEnabled: false, - }, - }); - view.render(); + it('should not render the subscription upsell section', () => { expect(view.$('.js-subscription-upsell')[0]).not.toBeInDOM(); }); - it('should render the subscription upsell section', () => { - expect(view.$('.js-subscription-upsell')[0]).toBeInDOM(); - expect(view.$('.js-subscription-upsell .badge').html().trim()) - .toEqual('New'); - expect(view.$('.js-subscription-upsell h4').html().trim()) - .toMatch(/^Monthly program subscriptions . more flexible, more affordable$/); - expect(view.$('.js-subscription-upsell .advertise-message').html().trim()) - .toEqual( - 'Now available for many popular programs, affordable monthly subscription pricing can help you manage your budget more effectively. Subscriptions start at $39/month USD per program, after a 7-day full access free trial. Cancel at any time.', - ); - expect(view.$('.js-subscription-upsell a span:last').html().trim()) - .toEqual('Explore subscription options'); - expect(view.$('.js-subscription-upsell a').attr('href')) - .toEqual('https://www.example.org/program-subscriptions'); - }); - it('should load the exploration panel given a marketing URL', () => { expect(view.$('.program-advertise .advertise-message').html().trim()) .toEqual( diff --git a/lms/static/js/learner_dashboard/views/program_details_view.js b/lms/static/js/learner_dashboard/views/program_details_view.js index 8d6c418147..220840c182 100644 --- a/lms/static/js/learner_dashboard/views/program_details_view.js +++ b/lms/static/js/learner_dashboard/views/program_details_view.js @@ -215,9 +215,13 @@ class ProgramDetailsView extends Backbone.View { && this.remainingCourseCollection.length === 0 ); + const isSubscriptionActiveSunsetting = ( + this.subscriptionModel.get('subscriptionState') === 'active' + ) + return ( this.options.isUserB2CSubscriptionsEnabled - && this.options.programData.subscription_eligible + && isSubscriptionActiveSunsetting && !programPurchasedWithoutSubscription ); } diff --git a/lms/static/js/learner_dashboard/views/sidebar_view.js b/lms/static/js/learner_dashboard/views/sidebar_view.js index 9cc9245f20..3359eac1b4 100644 --- a/lms/static/js/learner_dashboard/views/sidebar_view.js +++ b/lms/static/js/learner_dashboard/views/sidebar_view.js @@ -3,7 +3,6 @@ import Backbone from 'backbone'; import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils'; import NewProgramsView from './explore_new_programs_view'; -import SubscriptionUpsellView from './subscription_upsell_view'; import sidebarTpl from '../../../templates/learner_dashboard/sidebar.underscore'; @@ -30,12 +29,6 @@ class SidebarView extends Backbone.View { } postRender() { - if (this.context.isUserB2CSubscriptionsEnabled) { - this.subscriptionUpsellView = new SubscriptionUpsellView({ - subscriptionUpsellData: this.context.subscriptionUpsellData, - }); - } - this.newProgramsView = new NewProgramsView({ context: this.context, }); diff --git a/lms/templates/learner_dashboard/sidebar.underscore b/lms/templates/learner_dashboard/sidebar.underscore index 8f8db1b97a..7f02d9862e 100644 --- a/lms/templates/learner_dashboard/sidebar.underscore +++ b/lms/templates/learner_dashboard/sidebar.underscore @@ -1,4 +1 @@ -<% if (isUserB2CSubscriptionsEnabled) { %> - -<% } %>
diff --git a/openedx/core/djangoapps/credit/apps.py b/openedx/core/djangoapps/credit/apps.py index 6107d73de7..e057cc5cab 100644 --- a/openedx/core/djangoapps/credit/apps.py +++ b/openedx/core/djangoapps/credit/apps.py @@ -15,7 +15,7 @@ class CreditConfig(AppConfig): name = 'openedx.core.djangoapps.credit' def ready(self): - from . import signals # lint-amnesty, pylint: disable=unused-import + from .signals import handlers # lint-amnesty, pylint: disable=unused-import if settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'): from .services import CreditService set_runtime_service('credit', CreditService()) diff --git a/openedx/core/djangoapps/credit/signals/__init__.py b/openedx/core/djangoapps/credit/signals/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/core/djangoapps/credit/signals.py b/openedx/core/djangoapps/credit/signals/handlers.py similarity index 54% rename from openedx/core/djangoapps/credit/signals.py rename to openedx/core/djangoapps/credit/signals/handlers.py index de22fe42a8..3748f1cfba 100644 --- a/openedx/core/djangoapps/credit/signals.py +++ b/openedx/core/djangoapps/credit/signals/handlers.py @@ -5,15 +5,78 @@ This file contains receivers of course publication signals. import logging +from django.contrib.auth import get_user_model +from django.core.exceptions import ObjectDoesNotExist from django.dispatch import receiver from django.utils import timezone from opaque_keys.edx.keys import CourseKey +from openedx_events.learning.signals import ( + EXAM_ATTEMPT_ERRORED, + EXAM_ATTEMPT_REJECTED, + EXAM_ATTEMPT_RESET, + EXAM_ATTEMPT_SUBMITTED, + EXAM_ATTEMPT_VERIFIED +) +from openedx.core.djangoapps.credit.api.eligibility import ( + is_credit_course, + remove_credit_requirement_status, + set_credit_requirement_status +) from openedx.core.djangoapps.signals.signals import COURSE_GRADE_CHANGED +User = get_user_model() + log = logging.getLogger(__name__) +def handle_exam_event(signal, event_data, credit_status=None): + """ + update credit requirements based on exam event + """ + user_data = event_data.student_user + course_key = event_data.course_key + usage_key = event_data.usage_key + request_namespace = 'exam' + + # quick exit, if course is not credit enabled + if not is_credit_course(course_key): + return + + # log any activity to the credit requirements table + log.info( + f'Recieved {signal} signal, changing credit requirement for ' + f'user_id={user_data.id}, course_key_or_id={course_key} ' + f'content_id={usage_key}' + ) + + try: + user = User.objects.get(id=user_data.id) + except ObjectDoesNotExist: + log.error( + 'Error occurred while handling exam event for ' + f'{user_data.id} and content_id {usage_key}. ' + 'User does not exist!' + ) + return + + if signal == EXAM_ATTEMPT_RESET: + remove_credit_requirement_status( + user.username, + course_key, + request_namespace, + str(usage_key), + ) + else: + set_credit_requirement_status( + user.username, + course_key, + request_namespace, + str(usage_key), + credit_status + ) + + def on_course_publish(course_key): """ Will receive a delegated 'course_published' signal from cms/djangoapps/contentstore/signals.py @@ -94,3 +157,48 @@ def listen_for_grade_calculation(sender, user, course_grade, course_key, deadlin api.set_credit_requirement_status( user, course_id, 'grade', 'grade', status=status, reason=reason ) + + +@receiver(EXAM_ATTEMPT_RESET) +def listen_for_exam_reset(sender, signal, **kwargs): + """ + exam reset event from the event bus + """ + event_data = kwargs.get('exam_attempt') + handle_exam_event(signal, event_data) + + +@receiver(EXAM_ATTEMPT_SUBMITTED) +def listen_for_exam_submitted(sender, signal, **kwargs): + """ + exam submission event from the event bus + """ + event_data = kwargs.get('exam_attempt') + handle_exam_event(signal, event_data, credit_status='submitted') + + +@receiver(EXAM_ATTEMPT_VERIFIED) +def listen_for_exam_verified(sender, signal, **kwargs): + """ + exam verification event from the event bus + """ + event_data = kwargs.get('exam_attempt') + handle_exam_event(signal, event_data, credit_status='satisfied') + + +@receiver(EXAM_ATTEMPT_REJECTED) +def listen_for_exam_rejected(sender, signal, **kwargs): + """ + exam rejection event from the event bus + """ + event_data = kwargs.get('exam_attempt') + handle_exam_event(signal, event_data, credit_status='failed') + + +@receiver(EXAM_ATTEMPT_ERRORED) +def listen_for_exam_errored(sender, signal, **kwargs): + """ + exam error event from the event bus + """ + event_data = kwargs.get('exam_attempt') + handle_exam_event(signal, event_data, credit_status='failed') diff --git a/openedx/core/djangoapps/credit/tests/test_signals.py b/openedx/core/djangoapps/credit/tests/test_signals.py index 198f88e875..c3331c0ecf 100644 --- a/openedx/core/djangoapps/credit/tests/test_signals.py +++ b/openedx/core/djangoapps/credit/tests/test_signals.py @@ -1,23 +1,41 @@ """ -Tests for minimum grade requirement status +Tests for minimum grade and credit requirement status """ +from datetime import datetime, timedelta, timezone +from unittest import mock +from uuid import uuid4 -from datetime import datetime, timedelta - -from unittest.mock import MagicMock import ddt import pytz from django.test.client import RequestFactory +from opaque_keys.edx.keys import UsageKey +from openedx_events.data import EventsMetadata +from openedx_events.learning.data import ExamAttemptData, UserData, UserPersonalData +from openedx_events.learning.signals import ( + EXAM_ATTEMPT_ERRORED, + EXAM_ATTEMPT_REJECTED, + EXAM_ATTEMPT_RESET, + EXAM_ATTEMPT_SUBMITTED, + EXAM_ATTEMPT_VERIFIED +) from common.djangoapps.course_modes.models import CourseMode -from openedx.core.djangoapps.credit.api import get_credit_requirement_status, set_credit_requirements -from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider -from openedx.core.djangoapps.credit.signals import listen_for_grade_calculation -from openedx.core.djangolib.testing.utils import skip_unless_lms from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.tests.factories import UserFactory -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order +from openedx.core.djangoapps.credit.api import get_credit_requirement_status, set_credit_requirements +from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider +from openedx.core.djangoapps.credit.signals.handlers import ( + listen_for_exam_errored, + listen_for_exam_rejected, + listen_for_exam_reset, + listen_for_exam_submitted, + listen_for_exam_verified, + listen_for_grade_calculation +) +from openedx.core.djangolib.testing.utils import skip_unless_lms +from xmodule.modulestore.tests.django_utils import \ + ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order @@ -76,7 +94,7 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase): def assert_requirement_status(self, grade, due_date, expected_status): """ Verify the user's credit requirement status is as expected after simulating a grading calculation. """ - course_grade = MagicMock() + course_grade = mock.MagicMock() course_grade.percent = grade listen_for_grade_calculation(None, self.user, course_grade, self.course.id, due_date) req_status = get_credit_requirement_status(self.course.id, self.request.user.username, 'grade', 'grade') @@ -129,3 +147,146 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase): """Test with valid grades submitted before deadline with non-verified enrollment.""" self.enrollment.update_enrollment(mode, True) self.assert_requirement_status(0.8, self.VALID_DUE_DATE, None) + + +@skip_unless_lms +@ddt.ddt +class TestExamEvents(ModuleStoreTestCase): + """ + Test exam events + """ + HANDLERS = { + EXAM_ATTEMPT_ERRORED: listen_for_exam_errored, + EXAM_ATTEMPT_REJECTED: listen_for_exam_rejected, + EXAM_ATTEMPT_RESET: listen_for_exam_reset, + EXAM_ATTEMPT_SUBMITTED: listen_for_exam_submitted, + EXAM_ATTEMPT_VERIFIED: listen_for_exam_verified, + } + + def setUp(self): + super().setUp() + self.course = CourseFactory.create( + org='TestX', number='999', display_name='Test Course' + ) + self.subsection_key = UsageKey.from_string('block-v1:edX+TestX+Test_Course+type@sequential+block@subsection') + + self.user = UserFactory() + + # Enable the course for credit + CreditCourse.objects.create( + course_key=self.course.id, + enabled=True, + ) + + @staticmethod + def _get_exam_event_data(user, course, usage_key): + """ create ExamAttemptData object for user """ + return ExamAttemptData( + student_user=UserData( + id=user.id, + is_active=True, + pii=UserPersonalData( + username=user.username, + email=user.email, + ), + ), + course_key=course.id, + usage_key=usage_key, + exam_type='timed', + requesting_user=None, + ) + + @staticmethod + def _get_exam_event_metadata(event_signal): + """ create metadata object for event """ + return EventsMetadata( + event_type=event_signal.event_type, + id=uuid4(), + minorversion=0, + source='openedx/lms/web', + sourcehost='lms.test', + time=datetime.now(timezone.utc) + ) + + @mock.patch('openedx.core.djangoapps.credit.signals.handlers.remove_credit_requirement_status', autospec=True) + def test_exam_reset(self, mock_remove_credit_status): + """ + Test exam reset event + """ + event_data = self._get_exam_event_data(self.user, self.course, self.subsection_key) + event_metadata = self._get_exam_event_metadata(EXAM_ATTEMPT_RESET) + + listen_for_exam_reset(None, EXAM_ATTEMPT_RESET, event_metadata=event_metadata, exam_attempt=event_data) + + mock_remove_credit_status.assert_called_once_with( + self.user.username, self.course.id, 'exam', str(self.subsection_key) + ) + + @ddt.data( + (EXAM_ATTEMPT_ERRORED, 'failed'), + (EXAM_ATTEMPT_REJECTED, 'failed'), + (EXAM_ATTEMPT_VERIFIED, 'satisfied'), + (EXAM_ATTEMPT_SUBMITTED, 'submitted'), + ) + @ddt.unpack + @mock.patch('openedx.core.djangoapps.credit.signals.handlers.set_credit_requirement_status', autospec=True) + def test_exam_update_event(self, event_signal, expected_status, mock_set_credit_status): + """ + Test exam events that update credit status + """ + event_data = self._get_exam_event_data(self.user, self.course, self.subsection_key) + event_metadata = self._get_exam_event_metadata(event_signal) + + handler = self.HANDLERS.get(event_signal) + handler(None, event_signal, event_metadata=event_metadata, exam_attempt=event_data) + + mock_set_credit_status.assert_called_once_with( + self.user.username, self.course.id, 'exam', str(self.subsection_key), status=expected_status + ) + + @ddt.data( + EXAM_ATTEMPT_RESET, + EXAM_ATTEMPT_REJECTED, + EXAM_ATTEMPT_ERRORED, + EXAM_ATTEMPT_VERIFIED, + EXAM_ATTEMPT_SUBMITTED, + ) + def test_exam_event_bad_user(self, event_signal): + """ + Test exam event with a user that does not exist in the LMS + """ + self.user.id = 999 # don't save to db so user doesn't exist + event_data = self._get_exam_event_data(self.user, self.course, self.subsection_key) + event_metadata = self._get_exam_event_metadata(event_signal) + handler = self.HANDLERS.get(event_signal) + + with mock.patch('openedx.core.djangoapps.credit.signals.handlers.log.error') as mock_log: + handler(None, event_signal, event_metadata=event_metadata, exam_attempt=event_data) + mock_log.assert_called_once_with( + 'Error occurred while handling exam event for ' + f'{self.user.id} and content_id {self.subsection_key}. ' + 'User does not exist!' + ) + + @ddt.data( + EXAM_ATTEMPT_RESET, + EXAM_ATTEMPT_REJECTED, + EXAM_ATTEMPT_ERRORED, + EXAM_ATTEMPT_VERIFIED, + EXAM_ATTEMPT_SUBMITTED, + ) + @mock.patch('openedx.core.djangoapps.credit.signals.handlers.remove_credit_requirement_status', autospec=True) + @mock.patch('openedx.core.djangoapps.credit.signals.handlers.set_credit_requirement_status', autospec=True) + def test_exam_event_non_credit_course(self, event_signal, mock_remove_credit_status, mock_set_credit_status): + """ + Credit credit logic should not run on non-credit courses + """ + non_credit_course = CourseFactory.create() + event_data = self._get_exam_event_data(self.user, non_credit_course, self.subsection_key) + event_metadata = self._get_exam_event_metadata(event_signal) + handler = self.HANDLERS.get(event_signal) + + handler(None, event_signal, event_metadata=event_metadata, exam_attempt=event_data) + + mock_remove_credit_status.assert_not_called() + mock_set_credit_status.assert_not_called() diff --git a/openedx/core/djangoapps/credit/tests/test_tasks.py b/openedx/core/djangoapps/credit/tests/test_tasks.py index 7d520f22f3..47c4b50925 100644 --- a/openedx/core/djangoapps/credit/tests/test_tasks.py +++ b/openedx/core/djangoapps/credit/tests/test_tasks.py @@ -11,7 +11,7 @@ from edx_proctoring.api import create_exam from openedx.core.djangoapps.credit.api import get_credit_requirements from openedx.core.djangoapps.credit.exceptions import InvalidCreditRequirements from openedx.core.djangoapps.credit.models import CreditCourse -from openedx.core.djangoapps.credit.signals import on_course_publish +from openedx.core.djangoapps.credit.signals.handlers import on_course_publish from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory # lint-amnesty, pylint: disable=wrong-import-order diff --git a/openedx/core/djangoapps/enrollments/views.py b/openedx/core/djangoapps/enrollments/views.py index 80f6df8531..f413cb761e 100644 --- a/openedx/core/djangoapps/enrollments/views.py +++ b/openedx/core/djangoapps/enrollments/views.py @@ -1066,7 +1066,9 @@ class EnrollmentAllowedView(APIView): **Example Request** - POST /api/enrollment/v1/enrollment_allowed + POST /api/enrollment/v1/enrollment_allowed/ + + Note: The URL for this request must finish with / Example request data: ``` @@ -1086,7 +1088,6 @@ class EnrollmentAllowedView(APIView): - `auto_enroll` (optional, bool: default=false, _body_) **Responses** - - 200: Success, enrollment allowed found. - 400: Bad request, missing data. - 403: Forbidden, you need to be staff. - 409: Conflict, enrollment allowed already exists. @@ -1122,7 +1123,9 @@ class EnrollmentAllowedView(APIView): **Example Request** - DELETE /api/enrollment/v1/enrollment_allowed + DELETE /api/enrollment/v1/enrollment_allowed/ + + Note: The URL for this request must finish with / Example request data: ``` diff --git a/openedx/core/djangoapps/user_authn/views/login.py b/openedx/core/djangoapps/user_authn/views/login.py index fa098c7237..4880e58a43 100644 --- a/openedx/core/djangoapps/user_authn/views/login.py +++ b/openedx/core/djangoapps/user_authn/views/login.py @@ -344,7 +344,7 @@ def _track_user_login(user, request): segment.identify( user.id, { - 'email': request.POST.get('email'), + 'email': user.email, 'username': user.username }, { diff --git a/openedx/testing/coverage_context_listener/pytest_plugin.py b/openedx/testing/coverage_context_listener/pytest_plugin.py new file mode 100644 index 0000000000..a3679027e8 --- /dev/null +++ b/openedx/testing/coverage_context_listener/pytest_plugin.py @@ -0,0 +1,29 @@ +""" +A pytest plugin that reports test contexts to coverage running in another process. +""" + +import pytest + + +class RemoteContextPlugin: + """ + Pytest plugin for reporting pytests contexts to coverage running in another process + """ + def __init__(self, config): + self.config = config + self.active = config.getoption("pytest-contexts") + + +@pytest.hookimpl(tryfirst=True) +def pytest_configure(config): + config.pluginmanager.register(RemoteContextPlugin(config), "remotecontextplugin") + + +def pytest_addoption(parser): + group = parser.getgroup("coverage") + group.addoption( + "--pytest-remote-contexts", + action="store_true", + dest="pytest-contexts", + help="Capture the pytest contexts that coverage is being captured in in another process", + ) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 9465159646..77f60e9937 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -26,7 +26,7 @@ django-storages==1.14 # The team that owns this package will manually bump this package rather than having it pulled in automatically. # This is to allow them to better control its deployment and to do it in a process that works better # for them. -edx-enterprise==4.3.3 +edx-enterprise==4.5.0 # django-oauth-toolkit version >=2.0.0 has breaking changes. More details # mentioned on this issue https://github.com/openedx/edx-platform/issues/32884 @@ -120,9 +120,6 @@ libsass==0.10.0 # greater version breaking upgrade builds click==8.1.6 -# openedx-events 8.6.0 introduces publishing via configuration. Ticket to unpin: https://github.com/edx/edx-arch-experiments/issues/381 -openedx-events<8.6.0 # Open edX Events from Hooks Extension Framework (OEP-50) - # pinning this version to avoid updates while the library is being developed openedx-learning==0.1.7 diff --git a/requirements/edx-sandbox/py38.txt b/requirements/edx-sandbox/py38.txt index 27138116fd..d1cc50e869 100644 --- a/requirements/edx-sandbox/py38.txt +++ b/requirements/edx-sandbox/py38.txt @@ -4,7 +4,7 @@ # # make upgrade # -cffi==1.15.1 +cffi==1.16.0 # via cryptography chem==1.2.0 # via -r requirements/edx-sandbox/py38.in @@ -20,9 +20,9 @@ cryptography==38.0.4 # via # -c requirements/edx-sandbox/../constraints.txt # -r requirements/edx-sandbox/py38.in -cycler==0.11.0 +cycler==0.12.0 # via matplotlib -fonttools==4.42.1 +fonttools==4.43.0 # via matplotlib importlib-resources==6.1.0 # via matplotlib @@ -57,7 +57,7 @@ numpy==1.22.4 # scipy openedx-calc==3.0.1 # via -r requirements/edx-sandbox/py38.in -packaging==23.1 +packaging==23.2 # via matplotlib pillow==9.5.0 # via diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 8b7d83aceb..63dfd49229 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -77,13 +77,13 @@ boto==2.39.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in -boto3==1.28.53 +boto3==1.28.58 # via # -r requirements/edx/kernel.in # django-ses # fs-s3fs # ora2 -botocore==1.31.53 +botocore==1.31.58 # via # -r requirements/edx/kernel.in # boto3 @@ -107,7 +107,7 @@ certifi==2023.7.22 # py2neo # requests # snowflake-connector-python -cffi==1.15.1 +cffi==1.16.0 # via # cryptography # pynacl @@ -288,7 +288,7 @@ django-filter==23.3 # edx-enterprise # lti-consumer-xblock # openedx-blockstore -django-ipware==5.0.0 +django-ipware==5.0.1 # via # -r requirements/edx/kernel.in # edx-enterprise @@ -406,7 +406,7 @@ drf-jwt==1.19.2 # via edx-drf-extensions drf-nested-routers==0.93.4 # via openedx-blockstore -drf-spectacular==0.26.4 +drf-spectacular==0.26.5 # via -r requirements/edx/kernel.in drf-yasg==1.21.5 # via @@ -480,7 +480,7 @@ edx-drf-extensions==8.10.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.3.3 +edx-enterprise==4.5.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in @@ -492,7 +492,7 @@ edx-i18n-tools==1.2.0 # via ora2 edx-milestones==0.5.0 # via -r requirements/edx/kernel.in -edx-name-affirmation==2.3.6 +edx-name-affirmation==2.3.7 # via -r requirements/edx/kernel.in edx-opaque-keys[django]==2.5.1 # via @@ -599,7 +599,7 @@ html5lib==1.1 # via # -r requirements/edx/kernel.in # ora2 -icalendar==5.0.8 +icalendar==5.0.10 # via -r requirements/edx/kernel.in idna==3.4 # via @@ -735,7 +735,7 @@ mysqlclient==2.2.0 # via # -r requirements/edx/kernel.in # openedx-blockstore -newrelic==9.0.0 +newrelic==9.1.0 # via # -r requirements/edx/bundled.in # edx-django-utils @@ -758,7 +758,7 @@ oauthlib==3.2.2 # social-auth-core olxcleaner==0.2.1 # via -r requirements/edx/kernel.in -openai==0.28.0 +openai==0.28.1 # via edx-enterprise openedx-atlas==0.5.0 # via -r requirements/edx/kernel.in @@ -774,9 +774,8 @@ openedx-django-require==2.1.0 # via -r requirements/edx/kernel.in openedx-django-wiki==2.0.3 # via -r requirements/edx/kernel.in -openedx-events==8.5.0 +openedx-events==8.8.0 # via - # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in # edx-event-bus-kafka # edx-event-bus-redis @@ -792,11 +791,11 @@ openedx-mongodbproxy==0.2.0 # via -r requirements/edx/kernel.in optimizely-sdk==4.1.1 # via -r requirements/edx/bundled.in -ora2==5.5.0 +ora2==5.5.3 # via -r requirements/edx/bundled.in oscrypto==1.3.0 # via snowflake-connector-python -packaging==23.1 +packaging==23.2 # via # drf-yasg # gunicorn @@ -977,7 +976,7 @@ random2==1.0.1 # via -r requirements/edx/kernel.in recommender-xblock==2.0.1 # via -r requirements/edx/bundled.in -redis==5.0.0 +redis==5.0.1 # via # -r requirements/edx/kernel.in # walrus @@ -1018,7 +1017,7 @@ rpds-py==0.10.3 # via # jsonschema # referencing -ruamel-yaml==0.17.32 +ruamel-yaml==0.17.33 # via drf-yasg ruamel-yaml-clib==0.2.7 # via ruamel-yaml @@ -1028,7 +1027,7 @@ rules==3.3 # edx-enterprise # edx-proctoring # openedx-learning -s3transfer==0.6.2 +s3transfer==0.7.0 # via boto3 sailthru-client==2.2.3 # via edx-ace @@ -1159,7 +1158,7 @@ uritemplate==4.1.1 # coreapi # drf-spectacular # drf-yasg -urllib3==1.26.16 +urllib3==1.26.17 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/paver.txt @@ -1181,7 +1180,7 @@ walrus==0.9.3 # via edx-event-bus-redis watchdog==3.0.0 # via -r requirements/edx/paver.txt -wcwidth==0.2.6 +wcwidth==0.2.8 # via prompt-toolkit web-fragments==2.1.0 # via @@ -1204,7 +1203,7 @@ wrapt==1.15.0 # via # -r requirements/edx/paver.txt # deprecated -xblock[django]==1.7.0 +xblock[django]==1.8.0 # via # -r requirements/edx/kernel.in # acid-xblock diff --git a/requirements/edx/coverage.txt b/requirements/edx/coverage.txt index 00f316f2b1..93af4c1f82 100644 --- a/requirements/edx/coverage.txt +++ b/requirements/edx/coverage.txt @@ -6,7 +6,7 @@ # chardet==5.2.0 # via diff-cover -coverage==7.3.1 +coverage==7.3.2 # via -r requirements/edx/coverage.in diff-cover==7.7.0 # via -r requirements/edx/coverage.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index a24ee36e9b..0f42820cfe 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -143,14 +143,14 @@ boto==2.39.0 # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -boto3==1.28.53 +boto3==1.28.58 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # django-ses # fs-s3fs # ora2 -botocore==1.31.53 +botocore==1.31.58 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -185,7 +185,7 @@ certifi==2023.7.22 # py2neo # requests # snowflake-connector-python -cffi==1.15.1 +cffi==1.16.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -273,7 +273,7 @@ coreschema==0.0.4 # -r requirements/edx/testing.txt # coreapi # drf-yasg -coverage[toml]==7.3.1 +coverage[toml]==7.3.2 # via # -r requirements/edx/testing.txt # pytest-cov @@ -472,7 +472,7 @@ django-filter==23.3 # edx-enterprise # lti-consumer-xblock # openedx-blockstore -django-ipware==5.0.0 +django-ipware==5.0.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -653,7 +653,7 @@ drf-nested-routers==0.93.4 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # openedx-blockstore -drf-spectacular==0.26.4 +drf-spectacular==0.26.5 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -748,7 +748,7 @@ edx-drf-extensions==8.10.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.3.3 +edx-enterprise==4.5.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt @@ -772,7 +772,7 @@ edx-milestones==0.5.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -edx-name-affirmation==2.3.6 +edx-name-affirmation==2.3.7 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -895,7 +895,7 @@ faker==19.6.2 # via # -r requirements/edx/testing.txt # factory-boy -fastapi==0.103.1 +fastapi==0.103.2 # via # -r requirements/edx/testing.txt # pact-python @@ -982,7 +982,7 @@ httpx==0.23.3 # via # -r requirements/edx/testing.txt # pact-python -icalendar==5.0.8 +icalendar==5.0.10 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1000,7 +1000,7 @@ imagesize==1.4.1 # via # -r requirements/edx/doc.txt # sphinx -import-linter==1.11.1 +import-linter==1.12.0 # via -r requirements/edx/testing.txt importlib-metadata==6.8.0 # via @@ -1236,7 +1236,7 @@ mysqlclient==2.2.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # openedx-blockstore -newrelic==9.0.0 +newrelic==9.1.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1271,7 +1271,7 @@ olxcleaner==0.2.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -openai==0.28.0 +openai==0.28.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1302,9 +1302,8 @@ openedx-django-wiki==2.0.3 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -openedx-events==8.5.0 +openedx-events==8.8.0 # via - # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # edx-event-bus-kafka @@ -1327,7 +1326,7 @@ optimizely-sdk==4.1.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -ora2==5.5.0 +ora2==5.5.3 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1336,7 +1335,7 @@ oscrypto==1.3.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # snowflake-connector-python -packaging==23.1 +packaging==23.2 # via # -r requirements/edx/../pip-tools.txt # -r requirements/edx/doc.txt @@ -1472,11 +1471,11 @@ pycryptodomex==3.19.0 # lti-consumer-xblock # pyjwkest # snowflake-connector-python -pydantic==2.3.0 +pydantic==2.4.2 # via # -r requirements/edx/testing.txt # fastapi -pydantic-core==2.6.3 +pydantic-core==2.10.1 # via # -r requirements/edx/testing.txt # pydantic @@ -1710,7 +1709,7 @@ recommender-xblock==2.0.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -redis==5.0.0 +redis==5.0.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1768,7 +1767,7 @@ rpds-py==0.10.3 # -r requirements/edx/testing.txt # jsonschema # referencing -ruamel-yaml==0.17.32 +ruamel-yaml==0.17.33 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1785,7 +1784,7 @@ rules==3.3 # edx-enterprise # edx-proctoring # openedx-learning -s3transfer==0.6.2 +s3transfer==0.7.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -2047,11 +2046,11 @@ tqdm==4.66.1 # openai types-pytz==2023.3.1.1 # via django-stubs -types-pyyaml==6.0.12.11 +types-pyyaml==6.0.12.12 # via # django-stubs # djangorestframework-stubs -types-requests==2.31.0.3 +types-requests==2.31.0.6 # via djangorestframework-stubs types-urllib3==1.26.25.14 # via types-requests @@ -2101,7 +2100,7 @@ uritemplate==4.1.1 # coreapi # drf-spectacular # drf-yasg -urllib3==1.26.16 +urllib3==1.26.17 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt @@ -2148,7 +2147,7 @@ watchdog==3.0.0 # -r requirements/edx/development.in # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -wcwidth==0.2.6 +wcwidth==0.2.8 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -2184,7 +2183,7 @@ wrapt==1.15.0 # -r requirements/edx/testing.txt # astroid # deprecated -xblock[django]==1.7.0 +xblock[django]==1.8.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 90293ebd9e..2ccab0b634 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -103,13 +103,13 @@ boto==2.39.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt -boto3==1.28.53 +boto3==1.28.58 # via # -r requirements/edx/base.txt # django-ses # fs-s3fs # ora2 -botocore==1.31.53 +botocore==1.31.58 # via # -r requirements/edx/base.txt # boto3 @@ -133,7 +133,7 @@ certifi==2023.7.22 # py2neo # requests # snowflake-connector-python -cffi==1.15.1 +cffi==1.16.0 # via # -r requirements/edx/base.txt # cryptography @@ -345,7 +345,7 @@ django-filter==23.3 # edx-enterprise # lti-consumer-xblock # openedx-blockstore -django-ipware==5.0.0 +django-ipware==5.0.1 # via # -r requirements/edx/base.txt # edx-enterprise @@ -481,7 +481,7 @@ drf-nested-routers==0.93.4 # via # -r requirements/edx/base.txt # openedx-blockstore -drf-spectacular==0.26.4 +drf-spectacular==0.26.5 # via -r requirements/edx/base.txt drf-yasg==1.21.5 # via @@ -556,7 +556,7 @@ edx-drf-extensions==8.10.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.3.3 +edx-enterprise==4.5.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt @@ -570,7 +570,7 @@ edx-i18n-tools==1.2.0 # ora2 edx-milestones==0.5.0 # via -r requirements/edx/base.txt -edx-name-affirmation==2.3.6 +edx-name-affirmation==2.3.7 # via -r requirements/edx/base.txt edx-opaque-keys[django]==2.5.1 # via @@ -694,7 +694,7 @@ html5lib==1.1 # via # -r requirements/edx/base.txt # ora2 -icalendar==5.0.8 +icalendar==5.0.10 # via -r requirements/edx/base.txt idna==3.4 # via @@ -869,7 +869,7 @@ mysqlclient==2.2.0 # via # -r requirements/edx/base.txt # openedx-blockstore -newrelic==9.0.0 +newrelic==9.1.0 # via # -r requirements/edx/base.txt # edx-django-utils @@ -895,7 +895,7 @@ oauthlib==3.2.2 # social-auth-core olxcleaner==0.2.1 # via -r requirements/edx/base.txt -openai==0.28.0 +openai==0.28.1 # via # -r requirements/edx/base.txt # edx-enterprise @@ -914,9 +914,8 @@ openedx-django-require==2.1.0 # via -r requirements/edx/base.txt openedx-django-wiki==2.0.3 # via -r requirements/edx/base.txt -openedx-events==8.5.0 +openedx-events==8.8.0 # via - # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # edx-event-bus-kafka # edx-event-bus-redis @@ -932,13 +931,13 @@ openedx-mongodbproxy==0.2.0 # via -r requirements/edx/base.txt optimizely-sdk==4.1.1 # via -r requirements/edx/base.txt -ora2==5.5.0 +ora2==5.5.3 # via -r requirements/edx/base.txt oscrypto==1.3.0 # via # -r requirements/edx/base.txt # snowflake-connector-python -packaging==23.1 +packaging==23.2 # via # -r requirements/edx/base.txt # drf-yasg @@ -1158,7 +1157,7 @@ random2==1.0.1 # via -r requirements/edx/base.txt recommender-xblock==2.0.1 # via -r requirements/edx/base.txt -redis==5.0.0 +redis==5.0.1 # via # -r requirements/edx/base.txt # walrus @@ -1204,7 +1203,7 @@ rpds-py==0.10.3 # -r requirements/edx/base.txt # jsonschema # referencing -ruamel-yaml==0.17.32 +ruamel-yaml==0.17.33 # via # -r requirements/edx/base.txt # drf-yasg @@ -1218,7 +1217,7 @@ rules==3.3 # edx-enterprise # edx-proctoring # openedx-learning -s3transfer==0.6.2 +s3transfer==0.7.0 # via # -r requirements/edx/base.txt # boto3 @@ -1416,7 +1415,7 @@ uritemplate==4.1.1 # coreapi # drf-spectacular # drf-yasg -urllib3==1.26.16 +urllib3==1.26.17 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt @@ -1443,7 +1442,7 @@ walrus==0.9.3 # edx-event-bus-redis watchdog==3.0.0 # via -r requirements/edx/base.txt -wcwidth==0.2.6 +wcwidth==0.2.8 # via # -r requirements/edx/base.txt # prompt-toolkit @@ -1469,7 +1468,7 @@ wrapt==1.15.0 # via # -r requirements/edx/base.txt # deprecated -xblock[django]==1.7.0 +xblock[django]==1.8.0 # via # -r requirements/edx/base.txt # acid-xblock diff --git a/requirements/edx/paver.txt b/requirements/edx/paver.txt index 46af4d76cc..272e8a34a9 100644 --- a/requirements/edx/paver.txt +++ b/requirements/edx/paver.txt @@ -54,7 +54,7 @@ stevedore==5.1.0 # edx-opaque-keys typing-extensions==4.8.0 # via edx-opaque-keys -urllib3==1.26.16 +urllib3==1.26.17 # via # -c requirements/edx/../constraints.txt # requests diff --git a/requirements/edx/semgrep.txt b/requirements/edx/semgrep.txt index ab6f0a120f..fb696264ac 100644 --- a/requirements/edx/semgrep.txt +++ b/requirements/edx/semgrep.txt @@ -52,7 +52,7 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -packaging==23.1 +packaging==23.2 # via semgrep peewee==3.16.3 # via semgrep @@ -68,17 +68,17 @@ referencing==0.30.2 # jsonschema-specifications requests==2.31.0 # via semgrep -rich==13.5.3 +rich==13.6.0 # via semgrep rpds-py==0.10.3 # via # jsonschema # referencing -ruamel-yaml==0.17.32 +ruamel-yaml==0.17.33 # via semgrep ruamel-yaml-clib==0.2.7 # via ruamel-yaml -semgrep==1.41.0 +semgrep==1.42.0 # via -r requirements/edx/semgrep.in tomli==2.0.1 # via semgrep @@ -88,7 +88,7 @@ typing-extensions==4.8.0 # semgrep ujson==5.8.0 # via python-lsp-jsonrpc -urllib3==1.26.16 +urllib3==1.26.17 # via # -c requirements/edx/../constraints.txt # requests diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 8cf809e83d..5dbcd9c8d8 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -108,13 +108,13 @@ boto==2.39.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt -boto3==1.28.53 +boto3==1.28.58 # via # -r requirements/edx/base.txt # django-ses # fs-s3fs # ora2 -botocore==1.31.53 +botocore==1.31.58 # via # -r requirements/edx/base.txt # boto3 @@ -140,7 +140,7 @@ certifi==2023.7.22 # py2neo # requests # snowflake-connector-python -cffi==1.15.1 +cffi==1.16.0 # via # -r requirements/edx/base.txt # cryptography @@ -210,7 +210,7 @@ coreschema==0.0.4 # -r requirements/edx/base.txt # coreapi # drf-yasg -coverage[toml]==7.3.1 +coverage[toml]==7.3.2 # via # -r requirements/edx/coverage.txt # pytest-cov @@ -376,7 +376,7 @@ django-filter==23.3 # edx-enterprise # lti-consumer-xblock # openedx-blockstore -django-ipware==5.0.0 +django-ipware==5.0.1 # via # -r requirements/edx/base.txt # edx-enterprise @@ -506,7 +506,7 @@ drf-nested-routers==0.93.4 # via # -r requirements/edx/base.txt # openedx-blockstore -drf-spectacular==0.26.4 +drf-spectacular==0.26.5 # via -r requirements/edx/base.txt drf-yasg==1.21.5 # via @@ -581,7 +581,7 @@ edx-drf-extensions==8.10.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.3.3 +edx-enterprise==4.5.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt @@ -598,7 +598,7 @@ edx-lint==5.3.4 # via -r requirements/edx/testing.in edx-milestones==0.5.0 # via -r requirements/edx/base.txt -edx-name-affirmation==2.3.6 +edx-name-affirmation==2.3.7 # via -r requirements/edx/base.txt edx-opaque-keys[django]==2.5.1 # via @@ -689,7 +689,7 @@ factory-boy==3.3.0 # via -r requirements/edx/testing.in faker==19.6.2 # via factory-boy -fastapi==0.103.1 +fastapi==0.103.2 # via pact-python fastavro==1.8.3 # via @@ -746,7 +746,7 @@ httpretty==1.1.4 # via -r requirements/edx/testing.in httpx==0.23.3 # via pact-python -icalendar==5.0.8 +icalendar==5.0.10 # via -r requirements/edx/base.txt idna==3.4 # via @@ -757,7 +757,7 @@ idna==3.4 # rfc3986 # snowflake-connector-python # yarl -import-linter==1.11.1 +import-linter==1.12.0 # via -r requirements/edx/testing.in importlib-metadata==6.8.0 # via @@ -933,7 +933,7 @@ mysqlclient==2.2.0 # via # -r requirements/edx/base.txt # openedx-blockstore -newrelic==9.0.0 +newrelic==9.1.0 # via # -r requirements/edx/base.txt # edx-django-utils @@ -959,7 +959,7 @@ oauthlib==3.2.2 # social-auth-core olxcleaner==0.2.1 # via -r requirements/edx/base.txt -openai==0.28.0 +openai==0.28.1 # via # -r requirements/edx/base.txt # edx-enterprise @@ -978,9 +978,8 @@ openedx-django-require==2.1.0 # via -r requirements/edx/base.txt openedx-django-wiki==2.0.3 # via -r requirements/edx/base.txt -openedx-events==8.5.0 +openedx-events==8.8.0 # via - # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # edx-event-bus-kafka # edx-event-bus-redis @@ -996,13 +995,13 @@ openedx-mongodbproxy==0.2.0 # via -r requirements/edx/base.txt optimizely-sdk==4.1.1 # via -r requirements/edx/base.txt -ora2==5.5.0 +ora2==5.5.3 # via -r requirements/edx/base.txt oscrypto==1.3.0 # via # -r requirements/edx/base.txt # snowflake-connector-python -packaging==23.1 +packaging==23.2 # via # -r requirements/edx/base.txt # drf-yasg @@ -1105,9 +1104,9 @@ pycryptodomex==3.19.0 # lti-consumer-xblock # pyjwkest # snowflake-connector-python -pydantic==2.3.0 +pydantic==2.4.2 # via fastapi -pydantic-core==2.6.3 +pydantic-core==2.10.1 # via pydantic pygments==2.16.1 # via @@ -1287,7 +1286,7 @@ random2==1.0.1 # via -r requirements/edx/base.txt recommender-xblock==2.0.1 # via -r requirements/edx/base.txt -redis==5.0.0 +redis==5.0.1 # via # -r requirements/edx/base.txt # walrus @@ -1335,7 +1334,7 @@ rpds-py==0.10.3 # -r requirements/edx/base.txt # jsonschema # referencing -ruamel-yaml==0.17.32 +ruamel-yaml==0.17.33 # via # -r requirements/edx/base.txt # drf-yasg @@ -1349,7 +1348,7 @@ rules==3.3 # edx-enterprise # edx-proctoring # openedx-learning -s3transfer==0.6.2 +s3transfer==0.7.0 # via # -r requirements/edx/base.txt # boto3 @@ -1546,7 +1545,7 @@ uritemplate==4.1.1 # coreapi # drf-spectacular # drf-yasg -urllib3==1.26.16 +urllib3==1.26.17 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt @@ -1578,7 +1577,7 @@ walrus==0.9.3 # edx-event-bus-redis watchdog==3.0.0 # via -r requirements/edx/base.txt -wcwidth==0.2.6 +wcwidth==0.2.8 # via # -r requirements/edx/base.txt # prompt-toolkit @@ -1605,7 +1604,7 @@ wrapt==1.15.0 # -r requirements/edx/base.txt # astroid # deprecated -xblock[django]==1.7.0 +xblock[django]==1.8.0 # via # -r requirements/edx/base.txt # acid-xblock diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt index f9aebfe766..ea283f4ad3 100644 --- a/requirements/pip-tools.txt +++ b/requirements/pip-tools.txt @@ -12,7 +12,7 @@ click==8.1.6 # pip-tools importlib-metadata==6.8.0 # via build -packaging==23.1 +packaging==23.2 # via build pip-tools==7.3.0 # via -r requirements/pip-tools.in diff --git a/scripts/reset-test-db.sh b/scripts/reset-test-db.sh new file mode 100755 index 0000000000..4700f5a99a --- /dev/null +++ b/scripts/reset-test-db.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash + +############################################################################ +# +# reset-test-db.sh +# +# Resets the MySQL test database for the bok-choy acceptance tests. +# +# If it finds a cached schema and migration history, it will start +# from the cached version to speed up migrations. +# +# If no cached database exists, it will create one. This can be +# checked into the repo to speed up future tests. +# +# Note that we do NOT want to re-use the cache between test runs! +# A newer commit could introduce migrations that do not exist +# in other commits, which could cause migrations to fail in the other +# commits. +# +# For this reason, we always use a cache that was committed to master +# at the time the branch was created. +# +############################################################################ + +# Fail fast +set -e + +DB_CACHE_DIR="common/test/db_cache" + +if [[ -z "$BOK_CHOY_HOSTNAME" ]]; then + MYSQL_HOST="" + SETTINGS="bok_choy" +else + MYSQL_HOST="--host=edx.devstack.mysql80" + SETTINGS="bok_choy_docker" +fi + +for i in "$@"; do + case $i in + -r|--rebuild_cache) + REBUILD_CACHE=true + ;; + -m|--migrations) + APPLY_MIGRATIONS=true + ;; + -c|--calculate_migrations) + CALCULATE_MIGRATIONS=true + ;; + -u|--use-existing-db) + USE_EXISTING_DB=true + ;; + esac +done + +declare -A databases +declare -a database_order +databases=(["default"]="edxtest" ["student_module_history"]="student_module_history_test") +database_order=("default" "student_module_history") + +calculate_migrations() { + echo "Calculating migrations for fingerprinting." + output_file="common/test/db_cache/bok_choy_${db}_migrations.yaml" + # Redirect stdout to /dev/null because the script will print + # out all migrations to both stdout and the output file. + ./manage.py lms --settings "$SETTINGS" show_unapplied_migrations --database "$db" --output_file "$output_file" 1>/dev/null +} + +run_migrations() { + echo "Running the lms migrations on the $db bok_choy DB." + ./manage.py lms --settings "$SETTINGS" migrate --database "$db" --traceback --noinput + echo "Running the cms migrations on the $db bok_choy DB." + ./manage.py cms --settings "$SETTINGS" migrate --database "$db" --traceback --noinput +} + +load_cache_into_db() { + echo "Loading the schema from the filesystem into the $db MySQL DB." + mysql "$MYSQL_HOST" -u root "${databases["$db"]}" < "$DB_CACHE_DIR/bok_choy_schema_$db.sql" + echo "Loading the fixture data from the filesystem into the $db MySQL DB." + ./manage.py lms --settings "$SETTINGS" loaddata --database "$db" "$DB_CACHE_DIR/bok_choy_data_$db.json" + echo "Loading the migration data from the filesystem into the $db MySQL DB." + mysql "$MYSQL_HOST" -u root "${databases["$db"]}" < "$DB_CACHE_DIR/bok_choy_migrations_data_$db.sql" +} + +rebuild_cache_for_db() { + # Make sure the DB has all migrations applied + run_migrations + + # Dump the schema and data to the cache + echo "Using the dumpdata command to save the $db fixture data to the filesystem." + ./manage.py lms --settings "$SETTINGS" dumpdata --database "$db" > "$DB_CACHE_DIR/bok_choy_data_$db.json" --exclude=api_admin.Catalog + echo "Saving the schema of the $db bok_choy DB to the filesystem." + mysqldump "$MYSQL_HOST" -u root --no-data --skip-comments --skip-dump-date "${databases[$db]}" > "$DB_CACHE_DIR/bok_choy_schema_$db.sql" + + # dump_data does not dump the django_migrations table so we do it separately. + echo "Saving the django_migrations table of the $db bok_choy DB to the filesystem." + mysqldump $MYSQL_HOST -u root --no-create-info --skip-comments --skip-dump-date "${databases["$db"]}" django_migrations > "$DB_CACHE_DIR/bok_choy_migrations_data_$db.sql" +} + +for db in "${database_order[@]}"; do + if ! [[ $USE_EXISTING_DB ]]; then + echo "CREATE DATABASE IF NOT EXISTS ${databases[$db]};" | mysql $MYSQL_HOST -u root + + # Clear out the test database + # + # We are using the reset_db command which uses "DROP DATABASE" and + # "CREATE DATABASE" in case the tests are being run in an environment (e.g. devstack + # or a jenkins worker environment) that already ran tests on another commit that had + # different migrations that created, dropped, or altered tables. + echo "Issuing a reset_db command to the $db bok_choy MySQL database." + ./manage.py lms --settings "$SETTINGS" reset_db --traceback --router "$db" + fi + + if ! [[ $CALCULATE_MIGRATIONS ]]; then + # If there are cached database schemas/data, then load them. + # If they are missing, then we will want to build new cache files even if + # not explicitly directed to do so via arguments passed to this script. + if [[ ! -f $DB_CACHE_DIR/bok_choy_schema_$db.sql || ! -f $DB_CACHE_DIR/bok_choy_data_$db.json || ! -f $DB_CACHE_DIR/bok_choy_migrations_data_$db.sql ]]; then + REBUILD_CACHE=true + else + load_cache_into_db + fi + fi +done + +if [[ $REBUILD_CACHE ]]; then + echo "Cleaning the DB cache directory and building new files." + mkdir -p $DB_CACHE_DIR && rm -f $DB_CACHE_DIR/bok_choy* + + for db in "${database_order[@]}"; do + rebuild_cache_for_db + done +elif [[ $APPLY_MIGRATIONS ]]; then + for db in "${database_order[@]}"; do + run_migrations + done +elif [[ $CALCULATE_MIGRATIONS ]]; then + for db in "${database_order[@]}"; do + calculate_migrations + done +fi + diff --git a/scripts/xblock/requirements.txt b/scripts/xblock/requirements.txt index fcde0635a0..b39be98983 100644 --- a/scripts/xblock/requirements.txt +++ b/scripts/xblock/requirements.txt @@ -6,11 +6,11 @@ # certifi==2023.7.22 # via requests -charset-normalizer==3.2.0 +charset-normalizer==3.3.0 # via requests idna==3.4 # via requests requests==2.31.0 # via -r scripts/xblock/requirements.in -urllib3==2.0.5 +urllib3==2.0.6 # via requests diff --git a/xmodule/tests/test_vertical.py b/xmodule/tests/test_vertical.py index 1e8ababa91..223cf3889c 100644 --- a/xmodule/tests/test_vertical.py +++ b/xmodule/tests/test_vertical.py @@ -5,24 +5,26 @@ Tests for vertical block. # pylint: disable=protected-access +import json from collections import namedtuple from datetime import datetime, timedelta -import json from unittest.mock import Mock, patch -import pytz import ddt -from fs.memoryfs import MemoryFS +import pytz from django.contrib.auth.models import AnonymousUser from django.test import override_settings +from edx_toggles.toggles.testutils import override_waffle_switch +from fs.memoryfs import MemoryFS from openedx_filters import PipelineStep from openedx_filters.learning.filters import VerticalBlockChildRenderStarted, VerticalBlockRenderCompleted +from ..vertical_block import XBLOCK_SKILL_TAG_VERIFICATION_SWITCH +from ..x_module import AUTHOR_VIEW, PUBLIC_VIEW, STUDENT_VIEW from . import prepare_block_runtime from .helpers import StubUserService from .xml import XModuleXmlImportTest from .xml import factories as xml -from ..x_module import STUDENT_VIEW, AUTHOR_VIEW, PUBLIC_VIEW COMPLETION_DELAY = 9876 @@ -360,6 +362,7 @@ class VerticalBlockTestCase(BaseVerticalBlockTest): }, }, ) + @override_waffle_switch(XBLOCK_SKILL_TAG_VERIFICATION_SWITCH, True) def test_vertical_block_child_render_started_filter_execution(self): """ Test the VerticalBlockChildRenderStarted filter's effects on student view. @@ -382,6 +385,7 @@ class VerticalBlockTestCase(BaseVerticalBlockTest): }, }, ) + @override_waffle_switch(XBLOCK_SKILL_TAG_VERIFICATION_SWITCH, True) def test_vertical_block_child_render_is_skipped_on_filter_exception(self): """ Test VerticalBlockChildRenderStarted filter can be used to skip child blocks. @@ -405,6 +409,7 @@ class VerticalBlockTestCase(BaseVerticalBlockTest): }, }, ) + @override_waffle_switch(XBLOCK_SKILL_TAG_VERIFICATION_SWITCH, True) def test_vertical_block_render_completed_filter_execution(self): """ Test the VerticalBlockRenderCompleted filter's execution. @@ -427,6 +432,7 @@ class VerticalBlockTestCase(BaseVerticalBlockTest): }, }, ) + @override_waffle_switch(XBLOCK_SKILL_TAG_VERIFICATION_SWITCH, True) def test_vertical_block_render_output_is_changed_on_filter_exception(self): """ Test VerticalBlockRenderCompleted filter can be used to prevent vertical block from rendering. diff --git a/xmodule/vertical_block.py b/xmodule/vertical_block.py index d6a19353b2..10ff0a5408 100644 --- a/xmodule/vertical_block.py +++ b/xmodule/vertical_block.py @@ -9,17 +9,19 @@ from datetime import datetime from functools import reduce import pytz +from edx_toggles.toggles import WaffleSwitch from lxml import etree +from openedx_filters.learning.filters import VerticalBlockChildRenderStarted, VerticalBlockRenderCompleted from web_fragments.fragment import Fragment from xblock.core import XBlock # lint-amnesty, pylint: disable=wrong-import-order from xblock.fields import Boolean, Scope -from openedx_filters.learning.filters import VerticalBlockChildRenderStarted, VerticalBlockRenderCompleted + from xmodule.mako_block import MakoTemplateBlockBase from xmodule.progress import Progress from xmodule.seq_block import SequenceFields from xmodule.studio_editable import StudioEditableBlock -from xmodule.util.misc import is_xblock_an_assignment from xmodule.util.builtin_assets import add_webpack_js_to_fragment +from xmodule.util.misc import is_xblock_an_assignment from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW, XModuleFields from xmodule.xml_block import XmlMixin @@ -32,6 +34,17 @@ _ = lambda text: text # HACK: This shouldn't be hard-coded to two types # OBSOLETE: This obsoletes 'type' CLASS_PRIORITY = ['video', 'problem'] +WAFFLE_NAMESPACE = 'xblocks' +# .. toggle_name: xblocks.xblock_skill_tag_verification' +# .. toggle_implementation: WaffleSwitch +# .. toggle_default: False +# .. toggle_description: Set to True to get learner verification of the skills associated with the xblock. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2023-10-04 +# .. toggle_target_removal_date: None +XBLOCK_SKILL_TAG_VERIFICATION_SWITCH = WaffleSwitch( # lint-amnesty, pylint: disable=toggle-missing-annotation + f'{WAFFLE_NAMESPACE}.xblock_skill_tag_verification', __name__ +) class VerticalFields: @@ -116,16 +129,16 @@ class VerticalBlock( child_block_context['wrap_xblock_data'] = { 'mark-completed-on-view-after-delay': complete_on_view_delay } - - try: - # .. filter_implemented_name: VerticalBlockChildRenderStarted - # .. filter_type: org.openedx.learning.vertical_block_child.render.started.v1 - child, child_block_context = VerticalBlockChildRenderStarted.run_filter( - block=child, context=child_block_context - ) - except VerticalBlockChildRenderStarted.PreventChildBlockRender as exc: - log.info("Skipping %s from vertical block. Reason: %s", child, exc.message) - continue + if XBLOCK_SKILL_TAG_VERIFICATION_SWITCH.is_enabled(): + try: + # .. filter_implemented_name: VerticalBlockChildRenderStarted + # .. filter_type: org.openedx.learning.vertical_block_child.render.started.v1 + child, child_block_context = VerticalBlockChildRenderStarted.run_filter( + block=child, context=child_block_context + ) + except VerticalBlockChildRenderStarted.PreventChildBlockRender as exc: + log.info("Skipping %s from vertical block. Reason: %s", child, exc.message) + continue rendered_child = child.render(view, child_block_context) fragment.add_fragment_resources(rendered_child) @@ -167,15 +180,16 @@ class VerticalBlock( add_webpack_js_to_fragment(fragment, 'VerticalStudentView') fragment.initialize_js('VerticalStudentView') - try: - # .. filter_implemented_name: VerticalBlockRenderCompleted - # .. filter_type: org.openedx.learning.vertical_block.render.completed.v1 - _, fragment, context, view = VerticalBlockRenderCompleted.run_filter( - block=self, fragment=fragment, context=context, view=view - ) - except VerticalBlockRenderCompleted.PreventVerticalBlockRender as exc: - log.info("VerticalBlock rendering stopped. Reason: %s", exc.message) - fragment.content = exc.message + if XBLOCK_SKILL_TAG_VERIFICATION_SWITCH.is_enabled(): + try: + # .. filter_implemented_name: VerticalBlockRenderCompleted + # .. filter_type: org.openedx.learning.vertical_block.render.completed.v1 + _, fragment, context, view = VerticalBlockRenderCompleted.run_filter( + block=self, fragment=fragment, context=context, view=view + ) + except VerticalBlockRenderCompleted.PreventVerticalBlockRender as exc: + log.info("VerticalBlock rendering stopped. Reason: %s", exc.message) + fragment.content = exc.message return fragment