Merge branch 'master' into iamsobanjaved/django-42-lts

This commit is contained in:
Awais Qureshi
2024-01-30 12:53:33 +05:00
committed by GitHub
59 changed files with 1806 additions and 425 deletions

View File

@@ -62,7 +62,7 @@ class Command(BaseCommand):
return result
def handle(self, *args, **options):
def handle(self, *args, **options): # pylint: disable=too-many-statements
"""
By convention set by Django developers, this method actually executes command's actions.
So, there could be no better docstring than emphasize this once again.
@@ -88,8 +88,8 @@ class Command(BaseCommand):
logging.warning('Reducing logging to WARNING level for easier progress tracking')
if index_all_courses_option:
index_names = (CoursewareSearchIndexer.INDEX_NAME, CourseAboutSearchIndexer.INDEX_NAME)
if setup_option:
index_names = (CoursewareSearchIndexer.INDEX_NAME, CourseAboutSearchIndexer.INDEX_NAME)
for index_name in index_names:
try:
searcher = SearchEngine.get_search_engine(index_name)
@@ -116,16 +116,15 @@ class Command(BaseCommand):
elif active_option:
# in case of --active, we get the list of course keys from all courses
# that are stored in the modulestore and filter out the non-active
course_keys = []
all_courses = modulestore().get_courses()
today = date.today()
all_courses = modulestore().get_courses()
for course in all_courses:
# Omitting courses without a start date as well as
# couses that already ended (end date is in the past)
if not course.start or (course.end and course.end.date() < today):
continue
course_keys.append(course.id)
# We keep the courses that has a start date and either don't have an end date
# or the end date is not in the past.
active_courses = filter(lambda course: course.start
and (not course.end or course.end.date() >= today),
all_courses)
course_keys = list(map(lambda course: course.id, active_courses))
logging.warning(f'Selected {len(course_keys)} active courses over a total of {len(all_courses)}.')
@@ -135,16 +134,28 @@ class Command(BaseCommand):
total = len(course_keys)
logging.warning(f'Reindexing {total} courses...')
reindexed = 0
start = time()
count = 0
success = 0
errors = []
for course_key in course_keys:
try:
count += 1
CoursewareSearchIndexer.do_course_reindex(store, course_key)
reindexed += 1
if reindexed % 10 == 0 or reindexed == total:
now = time()
t = now - start
logging.warning(f'{reindexed}/{total} reindexed in {t:.1f} seconds.')
success += 1
if count % 10 == 0 or count == total:
t = time() - start
remaining = total - success - len(errors)
logging.warning(f'{success} courses reindexed in {t:.1f} seconds. {remaining} remaining...')
except Exception as exc: # lint-amnesty, pylint: disable=broad-except
errors.append(course_key)
logging.exception('Error indexing course %s due to the error: %s.', course_key, exc)
t = time() - start
logging.warning(f'{success} of {total} courses reindexed succesfully. Total running time: {t:.1f} seconds.')
if errors:
logging.warning('Reindex failed for %s courses:', len(errors))
for course_key in errors:
logging.warning(course_key)

View File

@@ -49,7 +49,6 @@ from cms.djangoapps.contentstore.utils import (
delete_course,
reverse_course_url,
reverse_url,
get_taxonomy_tags_widget_url,
)
from cms.djangoapps.contentstore.views.component import ADVANCED_COMPONENT_TYPES
from common.djangoapps.course_action_state.managers import CourseActionStateItemNotFoundError
@@ -1416,15 +1415,12 @@ class ContentStoreTest(ContentStoreTestCase):
course.location.course_key
)
taxonomy_tags_widget_url = get_taxonomy_tags_widget_url(course.id)
self.assertContains(
resp,
'<article class="outline outline-complex outline-course" data-locator="{locator}" data-course-key="{course_key}" data-course-assets="{assets_url}" data-taxonomy-tags-widget-url="{taxonomy_tags_widget_url}" >'.format( # lint-amnesty, pylint: disable=line-too-long
'<article class="outline outline-complex outline-course" data-locator="{locator}" data-course-key="{course_key}" data-course-assets="{assets_url}" >'.format( # lint-amnesty, pylint: disable=line-too-long
locator=str(course.location),
course_key=str(course.id),
assets_url=assets_url,
taxonomy_tags_widget_url=taxonomy_tags_widget_url,
),
status_code=200,
html=True

View File

@@ -315,6 +315,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
'is_reorderable': is_reorderable,
'can_edit': can_edit,
'can_edit_visibility': context.get('can_edit_visibility', is_course),
'course_authoring_url': settings.COURSE_AUTHORING_MICROFRONTEND_URL,
'is_loading': context.get('is_loading', False),
'is_selected': context.get('is_selected', False),
'selectable': context.get('selectable', False),

View File

@@ -288,15 +288,9 @@ class GetItemTest(ItemTest):
self.assertEqual(resp.status_code, 200)
usage_key = self.response_usage_key(resp)
# Get the preview HTML without tags
mock_get_object_tag_counts.return_value = {}
html, __ = self._get_container_preview(root_usage_key)
self.assertIn("wrapper-xblock", html)
self.assertNotIn('data-testid="tag-count-button"', html)
# Get the preview HTML with tags
mock_get_object_tag_counts.return_value = {
str(usage_key): 13
str(usage_key): 13,
}
html, __ = self._get_container_preview(root_usage_key)
self.assertIn("wrapper-xblock", html)

View File

@@ -1206,6 +1206,7 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements
xblock_info["tags"] = tags
if use_tagging_taxonomy_list_page():
xblock_info["taxonomy_tags_widget_url"] = get_taxonomy_tags_widget_url()
xblock_info["course_authoring_url"] = settings.COURSE_AUTHORING_MICROFRONTEND_URL
if course_outline:
if xblock_info["has_explicit_staff_lock"]:

View File

@@ -0,0 +1,13 @@
import * as TagCountView from 'js/views/tag_count';
import * as TagCountModel from 'js/models/tag_count';
// eslint-disable-next-line no-unused-expressions
'use strict';
export default function TagCountFactory(TagCountJson, el) {
var model = new TagCountModel(TagCountJson, {parse: true});
var tagCountView = new TagCountView({el, model});
tagCountView.setupMessageListener();
tagCountView.render();
}
export {TagCountFactory};

View File

@@ -332,7 +332,7 @@ define(
* @return {JSON} the data of the previous import
*/
storedImport: function() {
return JSON.parse($.cookie(COOKIE_NAME));
return JSON.parse($.cookie(COOKIE_NAME) || null);
}
};

View File

@@ -0,0 +1,13 @@
define(['backbone', 'underscore'], function(Backbone, _) {
/**
* Model for Tag count view
*/
var TagCountModel = Backbone.Model.extend({
defaults: {
content_id: null,
tags_count: 0,
course_authoring_url: null,
},
});
return TagCountModel;
});

View File

@@ -12,11 +12,11 @@ define(['jquery', 'underscore', 'js/views/xblock_outline', 'edx-ui-toolkit/js/ut
'common/js/components/utils/view_utils', 'js/views/utils/xblock_utils',
'js/models/xblock_outline_info', 'js/views/modals/course_outline_modals', 'js/utils/drag_and_drop',
'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt',
'js/views/utils/tagging_drawer_utils',],
'js/views/utils/tagging_drawer_utils', 'js/views/tag_count', 'js/models/tag_count'],
function(
$, _, XBlockOutlineView, StringUtils, ViewUtils, XBlockViewUtils,
XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger, NotificationView, PromptView,
TaggingDrawerUtils
TaggingDrawerUtils, TagCountView, TagCountModel
) {
var CourseOutlineView = XBlockOutlineView.extend({
// takes XBlockOutlineInfo as a model
@@ -28,9 +28,28 @@ function(
this.makeContentDraggable(this.el);
// Show/hide the paste button
this.initializePasteButton(this.el);
this.renderTagCount();
return renderResult;
},
renderTagCount: function() {
const contentId = this.model.get('id');
const tagCountsByUnit = this.model.get('tag_counts_by_unit')
const tagsCount = tagCountsByUnit !== undefined ? tagCountsByUnit[contentId] : 0
var countModel = new TagCountModel({
content_id: contentId,
tags_count: tagsCount,
course_authoring_url: this.model.get('course_authoring_url'),
}, {parse: true});
var tagCountView = new TagCountView({el: this.$('.tag-count'), model: countModel});
tagCountView.setupMessageListener();
tagCountView.render();
this.$('.tag-count').click((event) => {
event.preventDefault();
this.openManageTagsDrawer();
});
},
shouldExpandChildren: function() {
return this.expandedLocators.contains(this.model.get('id'));
},
@@ -461,10 +480,8 @@ function(
},
openManageTagsDrawer() {
const article = document.querySelector('[data-taxonomy-tags-widget-url]');
const taxonomyTagsWidgetUrl = $(article).attr('data-taxonomy-tags-widget-url');
const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url');
const contentId = this.model.get('id');
TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId);
},

View File

@@ -191,7 +191,7 @@ define([
* @return {JSON} the data of the previous export
*/
storedExport: function(contentHomeUrl) {
var storedData = JSON.parse($.cookie(COOKIE_NAME));
var storedData = JSON.parse($.cookie(COOKIE_NAME) || null);
if (storedData) {
successUnixDate = storedData.date;
}

View File

@@ -111,6 +111,7 @@ function($, _, Backbone, gettext, BasePage,
el: this.$('.unit-tags'),
model: this.model
});
this.tagListView.setupMessageListener();
this.tagListView.render();
this.unitOutlineView = new UnitOutlineView({

View File

@@ -370,6 +370,83 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, H
}
},
setupMessageListener: function () {
window.addEventListener(
"message", (event) => {
// Listen any message from Manage tags drawer.
var data = event.data;
var courseAuthoringUrl = this.model.get("course_authoring_url")
if (event.origin == courseAuthoringUrl
&& data.includes('[Manage tags drawer] Tags updated:')) {
// This message arrives when there is a change in the tag list.
// The message contains the new list of tags.
let jsonData = data.replace(/\[Manage tags drawer\] Tags updated: /g, "");
jsonData = JSON.parse(jsonData);
if (jsonData.contentId == this.model.id) {
this.model.set('tags', this.buildTaxonomyTree(jsonData));
this.render();
}
}
},
);
},
buildTaxonomyTree: function(data) {
// TODO We can use this function for the initial request of tags
// and avoid to use two functions (see get_unit_tags on contentstore/views/component.py)
var taxonomyList = [];
var totalCount = 0;
var actualId = 0;
data.taxonomies.forEach((taxonomy) => {
// Build a tag tree for each taxonomy
var rootTagsValues = [];
var tags = {};
taxonomy.tags.forEach((tag) => {
// Creates the tags for all the lineage of this tag
for (let i = tag.lineage.length - 1; i >= 0; i--){
var tagValue = tag.lineage[i]
var tagProcessedBefore = tags.hasOwnProperty(tagValue);
if (!tagProcessedBefore) {
tags[tagValue] = {
id: actualId,
value: tagValue,
children: [],
}
actualId++;
if (i == 0) {
rootTagsValues.push(tagValue);
}
}
if (i !== tag.lineage.length - 1) {
// Add a child into the children list
tags[tagValue].children.push(tags[tag.lineage[i + 1]])
}
if (tagProcessedBefore) {
// Break this loop if the tag has been processed before,
// we don't need to process lineage again to avoid duplicates.
break;
}
}
})
var tagCount = Object.keys(tags).length;
// Add the tree to the taxonomy list
taxonomyList.push({
id: taxonomy.taxonomyId,
value: taxonomy.name,
tags: rootTagsValues.map(rootValue => tags[rootValue]),
count: tagCount,
});
totalCount += tagCount;
});
return {
count: totalCount,
taxonomies: taxonomyList,
};
},
handleKeyDownOnHeader: function(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();

View File

@@ -0,0 +1,54 @@
define(['jquery', 'underscore', 'js/views/baseview', 'edx-ui-toolkit/js/utils/html-utils'],
function($, _, BaseView, HtmlUtils) {
'use strict';
/**
* TagCountView displays the tag count of a unit/component
*
* This component is being rendered in this way to allow receiving
* messages from the Manage tags drawer and being able to update the count.
*/
var TagCountView = BaseView.extend({
// takes TagCountModel as a model
initialize: function() {
BaseView.prototype.initialize.call(this);
this.template = this.loadTemplate('tag-count');
},
setupMessageListener: function () {
window.addEventListener(
'message', (event) => {
// Listen any message from Manage tags drawer.
var data = event.data;
var courseAuthoringUrl = this.model.get("course_authoring_url")
if (event.origin == courseAuthoringUrl
&& data.includes('[Manage tags drawer] Count updated:')) {
// This message arrives when there is a change in the tag list.
// The message contains the new count of tags.
let jsonData = data.replace(/\[Manage tags drawer\] Count updated: /g, "");
jsonData = JSON.parse(jsonData);
if (jsonData.contentId == this.model.get("content_id")) {
this.model.set('tags_count', jsonData.count);
this.render();
}
}
}
);
},
render: function() {
HtmlUtils.setHtml(
this.$el,
HtmlUtils.HTML(
this.template({
tags_count: this.model.get("tags_count"),
})
)
);
return this;
}
});
return TagCountView;
});

View File

@@ -13,6 +13,10 @@
background: rgba(0, 0, 0, 0.8);
}
.drawer-cover.gray-cover {
background: rgba(112, 112, 112, 0.8);
}
.drawer {
@extend %ui-depth4;

View File

@@ -281,5 +281,5 @@ from openedx.core.djangolib.markup import HTML, Text
</div>
<div id="manage-tags-drawer" class="drawer"></div>
<div class="drawer-cover"></div>
<div class="drawer-cover gray-cover"></div>
</%block>

View File

@@ -29,7 +29,7 @@ from django.urls import reverse
<%block name="header_extras">
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'self-paced-due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'unit-access-editor', 'discussion-editor', 'content-visibility-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'access-editor', 'settings-modal-tabs', 'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor', 'course-highlights-enable', 'course-video-sharing-enable', 'summary-configuration-editor']:
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'self-paced-due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'unit-access-editor', 'discussion-editor', 'content-visibility-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'access-editor', 'settings-modal-tabs', 'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor', 'course-highlights-enable', 'course-video-sharing-enable', 'summary-configuration-editor', 'tag-count']:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
@@ -281,7 +281,7 @@ from django.urls import reverse
assets_url = reverse('assets_handler', kwargs={'course_key_string': str(course_locator.course_key)})
%>
<h2 class="sr">${_("Course Outline")}</h2>
<article class="outline outline-complex outline-course" data-locator="${course_locator}" data-course-key="${course_locator.course_key}" data-course-assets="${assets_url}" data-taxonomy-tags-widget-url="${taxonomy_tags_widget_url}">
<article class="outline outline-complex outline-course" data-locator="${course_locator}" data-course-key="${course_locator.course_key}" data-course-assets="${assets_url}">
</article>
</div>
<div class="ui-loading">
@@ -323,5 +323,5 @@ from django.urls import reverse
</div>
<div id="manage-tags-drawer" class="drawer"></div>
<div class="drawer-cover"></div>
<div class="drawer-cover gray-cover"></div>
</%block>

View File

@@ -7,7 +7,8 @@ var hasPartitionGroups = xblockInfo.get('has_partition_group_components');
var userPartitionInfo = xblockInfo.get('user_partition_info');
var selectedGroupsLabel = userPartitionInfo['selected_groups_label'];
var selectedPartitionIndex = userPartitionInfo['selected_partition_index'];
var tagsCount = (xblockInfo.get('tag_counts_by_unit') || {})[xblockInfo.get('id')] || 0;
var xblockId = xblockInfo.get('id')
var tagsCount = (xblockInfo.get('tag_counts_by_unit') || {})[xblockId] || 0;
var statusMessages = [];
var messageType;
@@ -171,14 +172,8 @@ if (is_proctored_exam) {
</li>
<% } %>
<% if (xblockInfo.isVertical() && typeof useTaggingTaxonomyListPage !== "undefined" && useTaggingTaxonomyListPage && tagsCount > 0) { %>
<li class="action-item">
<a href="#" data-tooltip="<%- gettext('Manage Tags') %>" class="manage-tags-button action-button">
<span class="icon fa fa-tag" aria-hidden="true"></span>
<span><%- tagsCount %></span>
<span class="sr action-button-text"><%- gettext('Manage Tags') %></span>
</a>
</li>
<% if (xblockInfo.isVertical() && typeof useTaggingTaxonomyListPage !== "undefined" && useTaggingTaxonomyListPage) { %>
<li class="action-item tag-count" data-locator="<%- xblockId %>"></li>
<% } %>
<% if (typeof enableCopyPasteUnits !== "undefined" && enableCopyPasteUnits) { %>

View File

@@ -0,0 +1,7 @@
<% if (tags_count && tags_count > 0) { %>
<button data-tooltip="<%- gettext("Manage Tags") %>" class="btn-default action-button manage-tags-button" data-testid="tag-count-button">
<span class="icon fa fa-tag" aria-hidden="true"></span>
<span><%- tags_count %></span>
<span class="sr action-button-text"><%- gettext("Manage Tags") %></span>
</button>
<% } %>

View File

@@ -29,6 +29,9 @@ block_is_unit = is_unit(xblock)
<script type="text/template" id="xblock-validation-messages-tpl">
<%static:include path="js/xblock-validation-messages.underscore" />
</script>
<script type="text/template" id="tag-count-tpl">
<%static:include path="js/tag-count.underscore" />
</script>
</%block>
<script type="text/javascript">
@@ -41,6 +44,16 @@ block_is_unit = is_unit(xblock)
);
</script>
<%static:webpack entry="js/factories/tag_count">
TagCountFactory({
tags_count: "${tags_count | n, js_escaped_string}",
content_id: "${xblock.location | n, js_escaped_string}",
course_authoring_url: "${course_authoring_url | n, js_escaped_string}",
},
$('li.tag-count[data-locator="${xblock.location | n, js_escaped_string}"]')
);
</%static:webpack>
% if not is_root:
% if is_reorderable:
<li class="studio-xblock-wrapper is-draggable" id="${xblock.location}" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}">
@@ -99,14 +112,8 @@ block_is_unit = is_unit(xblock)
<ul class="actions-list nav-dd ui-right">
% if not is_root:
% if can_edit:
% if use_tagging and tags_count:
<li class="action-item">
<button data-tooltip="${_("Manage Tags")}" class="btn-default action-button manage-tags-button" data-testid="tag-count-button">
<span class="icon fa fa-tag" aria-hidden="true"></span>
<span>${tags_count}</span>
<span class="sr action-button-text">${_("Manage Tags")}</span>
</button>
</li>
% if use_tagging:
<li class="action-item tag-count" data-locator="${xblock.location}"></li>
% endif
% if not show_inline:
<li class="action-item action-edit">

View File

@@ -23,6 +23,7 @@ from common.djangoapps.student.models import (
)
from common.djangoapps.student.models_api import confirm_name_change
from common.djangoapps.student.signals import USER_EMAIL_CHANGED
from openedx.core.djangoapps.safe_sessions.middleware import EmailChangeMiddleware
from openedx.features.name_affirmation_api.utils import is_name_affirmation_installed
logger = logging.getLogger(__name__)
@@ -105,8 +106,12 @@ if is_name_affirmation_installed():
@receiver(USER_EMAIL_CHANGED)
def _listen_for_user_email_changed(sender, user, **kwargs):
""" If user has changed their email, update that in email Braze. """
def _listen_for_user_email_changed(sender, user, request, **kwargs):
""" If user has changed their email, update that in session and Braze profile. """
# Store the user's email for session consistency (used by EmailChangeMiddleware)
EmailChangeMiddleware.register_email_change(request, user.email)
email = user.email
user_id = user.id
attributes = [{'email': email, 'external_id': user_id}]

View File

@@ -9,7 +9,7 @@ from common.djangoapps.student.models import CourseEnrollmentCelebration, Pendin
from common.djangoapps.student.signals.signals import USER_EMAIL_CHANGED
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory, UserProfileFactory
from lms.djangoapps.courseware.toggles import COURSEWARE_MICROFRONTEND_PROGRESS_MILESTONES
from openedx.core.djangolib.testing.utils import skip_unless_lms
from openedx.core.djangolib.testing.utils import skip_unless_lms, get_mock_request
from openedx.features.name_affirmation_api.utils import is_name_affirmation_installed
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
@@ -75,10 +75,18 @@ class ReceiversTest(SharedModuleStoreTestCase):
@patch('common.djangoapps.student.signals.receivers.get_braze_client')
def test_listen_for_user_email_changed(self, mock_get_braze_client):
"""
Ensure that USER_EMAIL_CHANGED signal triggers correct calls to get_braze_client.
Ensure that USER_EMAIL_CHANGED signal triggers correct calls to
get_braze_client and update email in session.
"""
user = UserFactory(email='email@test.com', username='jdoe')
request = get_mock_request(user=user)
request.session = self.client.session
USER_EMAIL_CHANGED.send(sender=None, user=user)
# simulating email change
user.email = 'new_email@test.com'
user.save()
USER_EMAIL_CHANGED.send(sender=None, user=user, request=request)
assert mock_get_braze_client.called
assert request.session.get('email', None) == user.email

View File

@@ -910,7 +910,7 @@ def confirm_email_change(request, key):
response = render_to_response("email_change_successful.html", address_context)
USER_EMAIL_CHANGED.send(sender=None, user=user)
USER_EMAIL_CHANGED.send(sender=None, user=user, request=request)
return response

View File

@@ -309,7 +309,7 @@ class CertificatesListRestApiTest(AuthAndScopesTestMixin, SharedModuleStoreTestC
def test_query_counts(self):
# Test student with no certificates
student_no_cert = UserFactory.create(password=self.user_password)
with self.assertNumQueries(17, table_ignorelist=WAFFLE_TABLES):
with self.assertNumQueries(21, table_ignorelist=WAFFLE_TABLES):
resp = self.get_response(
AuthType.jwt,
requesting_user=self.global_staff,
@@ -319,7 +319,7 @@ class CertificatesListRestApiTest(AuthAndScopesTestMixin, SharedModuleStoreTestC
assert len(resp.data) == 0
# Test student with 1 certificate
with self.assertNumQueries(12, table_ignorelist=WAFFLE_TABLES):
with self.assertNumQueries(13, table_ignorelist=WAFFLE_TABLES):
resp = self.get_response(
AuthType.jwt,
requesting_user=self.global_staff,
@@ -359,7 +359,7 @@ class CertificatesListRestApiTest(AuthAndScopesTestMixin, SharedModuleStoreTestC
download_url='www.google.com',
grade="0.88",
)
with self.assertNumQueries(12, table_ignorelist=WAFFLE_TABLES):
with self.assertNumQueries(13, table_ignorelist=WAFFLE_TABLES):
resp = self.get_response(
AuthType.jwt,
requesting_user=self.global_staff,

View File

@@ -0,0 +1,32 @@
# Generated by Django 3.2.23 on 2024-01-25 21:56
from django.db import migrations
from lms.djangoapps.certificates.data import CertificateStatuses
class Migration(migrations.Migration):
"""
If any certificates exist with an invalidation record that are not marked as unavailable,
change their status. Irreversible.
"""
dependencies = [
("certificates", "0036_modifiedcertificatetemplatecommandconfiguration"),
]
def make_invalid_certificates_unavailable(apps, schema_editor):
GeneratedCertificate = apps.get_model("certificates", "GeneratedCertificate")
GeneratedCertificate.objects.filter(
certificateinvalidation__active=True
).exclude(status=CertificateStatuses.unavailable).update(
status=CertificateStatuses.unavailable
)
operations = [
migrations.RunPython(
make_invalid_certificates_unavailable,
reverse_code=migrations.RunPython.noop,
)
]

View File

@@ -434,7 +434,7 @@ class CourseListSearchViewTest(CourseApiTestViewMixin, ModuleStoreTestCase, Sear
self.setup_user(self.audit_user)
# These query counts were found empirically
query_counts = [50, 46, 46, 46, 46, 46, 46, 46, 46, 46, 16]
query_counts = [53, 46, 46, 46, 46, 46, 46, 46, 46, 46, 16]
ordered_course_ids = sorted([str(cid) for cid in (course_ids + [c.id for c in self.courses])])
self.clear_caches()

View File

@@ -86,6 +86,7 @@ class CourseHomeMetadataTests(BaseCourseHomeTests):
assert self.client.get(self.url).data['username'] == self.user.username
def test_get_unknown_course(self):
self.client.logout()
url = reverse('course-home:course-metadata', args=['course-v1:unknown+course+2T2020'])
# Django TestCase wraps every test in a transaction, so we must specifically wrap this when we expect an error
with transaction.atomic():

View File

@@ -426,6 +426,8 @@ class ViewsQueryCountTestCase(
@ddt.ddt
@disable_signal(views, 'comment_flagged')
@disable_signal(views, 'thread_flagged')
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
class ViewsTestCase(
ForumsEnableMixin,
@@ -1714,7 +1716,13 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto
commentable_id = getattr(self, commentable_id)
self._setup_mock(
user, mock_request,
{"closed": False, "commentable_id": commentable_id, "thread_id": "dummy_thread", "body": 'dummy body'},
{
"closed": False,
"commentable_id": commentable_id,
"thread_id": "dummy_thread",
"body": 'dummy body',
"course_id": str(self.course.id)
},
)
for action in ["upvote_comment", "downvote_comment", "un_flag_abuse_for_comment", "flag_abuse_for_comment"]:
response = self.client.post(
@@ -1735,7 +1743,7 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto
commentable_id = getattr(self, commentable_id)
self._setup_mock(
user, mock_request,
{"closed": False, "commentable_id": commentable_id, "body": "dummy body"},
{"closed": False, "commentable_id": commentable_id, "body": "dummy body", "course_id": str(self.course.id)}
)
for action in ["upvote_thread", "downvote_thread", "un_flag_abuse_for_thread", "flag_abuse_for_thread",
"follow_thread", "unfollow_thread"]:

View File

@@ -1,7 +1,7 @@
"""
Discussion notifications sender util.
"""
import logging
import re
from django.conf import settings
from lms.djangoapps.discussion.django_comment_client.permissions import get_team
@@ -22,9 +22,6 @@ from openedx.core.djangoapps.django_comment_common.models import (
)
log = logging.getLogger(__name__)
class DiscussionNotificationSender:
"""
Class to send notifications to users who are subscribed to the thread.
@@ -75,7 +72,7 @@ class DiscussionNotificationSender:
course_key=self.course.id,
content_context={
"replier_name": self.creator.username,
"post_title": self.thread.title,
"post_title": getattr(self.thread, 'title', ''),
"course_name": self.course.display_name,
"sender_id": self.creator.id,
**extra_context,
@@ -206,16 +203,20 @@ class DiscussionNotificationSender:
discussion_cohorted = is_discussion_cohorted(course_key_str)
# Retrieves cohort divided discussion
discussion_settings = CourseDiscussionSettings.objects.get(course_id=course_key_str)
try:
discussion_settings = CourseDiscussionSettings.objects.get(course_id=course_key_str)
except CourseDiscussionSettings.DoesNotExist:
return {}
divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions(
self.course,
discussion_settings
)
# Checks if post has any cohort assigned
group_id = self.thread.attributes['group_id']
if group_id is not None:
group_id = int(group_id)
group_id = self.thread.attributes.get('group_id')
if group_id is None:
return {}
group_id = int(group_id)
# Course wide topics
all_topics = divided_inline_discussions + divided_course_wide_discussions
@@ -262,15 +263,52 @@ class DiscussionNotificationSender:
'username': self.creator.username,
'post_title': self.thread.title
}
log.info(f"Temp: Audience filter for course-wide notification is {audience_filters}")
self._send_course_wide_notification(notification_type, audience_filters, context)
def send_reported_content_notification(self):
"""
Send notification to users who are subscribed to the thread.
"""
thread_body = self.thread.body if self.thread.body else ''
thread_body = remove_html_tags(thread_body)
thread_types = {
# numeric key is the depth of the thread in the discussion
'comment': {
1: 'comment',
0: 'response'
},
'thread': {
0: 'thread'
}
}
content_type = thread_types[self.thread.type][getattr(self.thread, 'depth', 0)]
context = {
'username': self.creator.username,
'content_type': content_type,
'content': thread_body
}
audience_filters = self._create_cohort_course_audience()
audience_filters['discussion_roles'] = [
FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA
]
self._send_course_wide_notification("content_reported", audience_filters, context)
def is_discussion_cohorted(course_key_str):
"""
Returns if the discussion is divided by cohorts
"""
cohort_settings = CourseCohortsSettings.objects.get(course_id=course_key_str)
discussion_settings = CourseDiscussionSettings.objects.get(course_id=course_key_str)
try:
cohort_settings = CourseCohortsSettings.objects.get(course_id=course_key_str)
discussion_settings = CourseDiscussionSettings.objects.get(course_id=course_key_str)
except (CourseCohortsSettings.DoesNotExist, CourseDiscussionSettings.DoesNotExist):
return False
return cohort_settings.is_cohorted and discussion_settings.always_divide_inline_discussions
def remove_html_tags(text):
clean = re.compile('<.*?>')
return re.sub(clean, '', text)

View File

@@ -0,0 +1,91 @@
"""
Unit tests for the DiscussionNotificationSender class
"""
import unittest
from unittest.mock import MagicMock, patch
import pytest
from edx_toggles.toggles.testutils import override_waffle_flag
from lms.djangoapps.discussion.rest_api.discussions_notifications import DiscussionNotificationSender
from lms.djangoapps.discussion.toggles import ENABLE_REPORTED_CONTENT_NOTIFICATIONS
@patch('lms.djangoapps.discussion.rest_api.discussions_notifications.DiscussionNotificationSender'
'._create_cohort_course_audience', return_value={})
@patch('lms.djangoapps.discussion.rest_api.discussions_notifications.DiscussionNotificationSender'
'._send_course_wide_notification')
@pytest.mark.django_db
class TestDiscussionNotificationSender(unittest.TestCase):
"""
Tests for the DiscussionNotificationSender class
"""
@override_waffle_flag(ENABLE_REPORTED_CONTENT_NOTIFICATIONS, True)
def setUp(self):
self.thread = MagicMock()
self.course = MagicMock()
self.creator = MagicMock()
self.notification_sender = DiscussionNotificationSender(self.thread, self.course, self.creator)
def _setup_thread(self, thread_type, body, depth):
"""
Helper to set up the thread object
"""
self.thread.type = thread_type
self.thread.body = body
self.thread.depth = depth
self.creator.username = 'test_user'
def _assert_send_notification_called_with(self, mock_send_notification, expected_content_type):
"""
Helper to assert that the send_notification method was called with the correct arguments
"""
notification_type, audience_filters, context = mock_send_notification.call_args[0]
mock_send_notification.assert_called_once()
self.assertEqual(notification_type, "content_reported")
self.assertEqual(context, {
'username': 'test_user',
'content_type': expected_content_type,
'content': 'Thread body'
})
self.assertEqual(audience_filters, {
'discussion_roles': ['Administrator', 'Moderator', 'Community TA']
})
def test_send_reported_content_notification_for_response(self, mock_send_notification, mock_create_audience):
"""
Test that the send_reported_content_notification method calls the send_notification method with the correct
arguments for a comment with depth 0
"""
self._setup_thread('comment', '<p>Thread body</p>', 0)
mock_create_audience.return_value = {}
self.notification_sender.send_reported_content_notification()
self._assert_send_notification_called_with(mock_send_notification, 'response')
def test_send_reported_content_notification_for_comment(self, mock_send_notification, mock_create_audience):
"""
Test that the send_reported_content_notification method calls the send_notification method with the correct
arguments for a comment with depth 1
"""
self._setup_thread('comment', '<p>Thread body</p>', 1)
mock_create_audience.return_value = {}
self.notification_sender.send_reported_content_notification()
self._assert_send_notification_called_with(mock_send_notification, 'comment')
def test_send_reported_content_notification_for_thread(self, mock_send_notification, mock_create_audience):
"""
Test that the send_reported_content_notification method calls the send_notification method with the correct
"""
self._setup_thread('thread', '<p>Thread body</p>', 0)
mock_create_audience.return_value = {}
self.notification_sender.send_reported_content_notification()
self._assert_send_notification_called_with(mock_send_notification, 'thread')

View File

@@ -2,14 +2,17 @@
Signal handlers related to discussions.
"""
import logging
from django.conf import settings
from django.dispatch import receiver
from django.utils.html import strip_tags
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import LibraryLocator
from xmodule.modulestore.django import SignalHandler
from lms.djangoapps.discussion.rest_api.discussions_notifications import DiscussionNotificationSender
from lms.djangoapps.discussion.toggles import ENABLE_REPORTED_CONTENT_NOTIFICATIONS
from xmodule.modulestore.django import SignalHandler, modulestore
from lms.djangoapps.discussion import tasks
from lms.djangoapps.discussion.rest_api.tasks import send_response_notifications, send_thread_created_notification
@@ -19,7 +22,6 @@ from openedx.core.djangoapps.theming.helpers import get_current_site
log = logging.getLogger(__name__)
ENABLE_FORUM_NOTIFICATIONS_FOR_SITE_KEY = 'enable_forum_notifications'
@@ -43,7 +45,8 @@ def update_discussions_on_course_publish(sender, course_key, **kwargs): # pylin
@receiver(signals.comment_created)
def send_discussion_email_notification(sender, user, post, **kwargs): # lint-amnesty, pylint: disable=missing-function-docstring, unused-argument
def send_discussion_email_notification(sender, user, post,
**kwargs): # lint-amnesty, pylint: disable=missing-function-docstring, unused-argument
current_site = get_current_site()
if current_site is None:
log.info('Discussion: No current site, not sending notification about post: %s.', post.id)
@@ -64,7 +67,10 @@ def send_discussion_email_notification(sender, user, post, **kwargs): # lint-am
@receiver(signals.comment_flagged)
@receiver(signals.thread_flagged)
def send_reported_content_email_notification(sender, user, post, **kwargs): # lint-amnesty, pylint: disable=missing-function-docstring, unused-argument
def send_reported_content_email_notification(sender, user, post, **kwargs):
"""
Sends email notification for reported content.
"""
current_site = get_current_site()
if current_site is None:
log.info('Discussion: No current site, not sending notification about post: %s.', post.id)
@@ -84,6 +90,19 @@ def send_reported_content_email_notification(sender, user, post, **kwargs): # l
send_message_for_reported_content(user, post, current_site, sender)
@receiver(signals.comment_flagged)
@receiver(signals.thread_flagged)
def send_reported_content_notification(sender, user, post, **kwargs):
"""
Sends notification for reported content.
"""
course_key = CourseKey.from_string(post.course_id)
if not ENABLE_REPORTED_CONTENT_NOTIFICATIONS.is_enabled(course_key):
return
course = modulestore().get_course(course_key)
DiscussionNotificationSender(post, course, user).send_reported_content_notification()
def create_message_context(comment, site):
thread = comment.thread
return {
@@ -105,6 +124,7 @@ def create_message_context_for_reported_content(user, post, site, sender):
"""
Create message context for reported content.
"""
def get_comment_type(comment):
"""
Returns type of comment.
@@ -131,7 +151,8 @@ def send_message(comment, site): # lint-amnesty, pylint: disable=missing-functi
tasks.send_ace_message.apply_async(args=[context])
def send_message_for_reported_content(user, post, site, sender): # lint-amnesty, pylint: disable=missing-function-docstring
def send_message_for_reported_content(user, post, site,
sender): # lint-amnesty, pylint: disable=missing-function-docstring
context = create_message_context_for_reported_content(user, post, site, sender)
tasks.send_ace_message_for_reported_content.apply_async(args=[context], countdown=120)

View File

@@ -12,3 +12,15 @@ from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
# .. toggle_creation_date: 2021-11-05
# .. toggle_target_removal_date: 2022-12-05
ENABLE_DISCUSSIONS_MFE = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.enable_discussions_mfe', __name__)
# .. toggle_name: discussions.enable_reported_content_notifications
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: Waffle flag to enable reported content notifications.
# .. toggle_use_cases: temporary, open_edx
# .. toggle_creation_date: 18-Jan-2024
# .. toggle_target_removal_date: 18-Feb-2024
ENABLE_REPORTED_CONTENT_NOTIFICATIONS = CourseWaffleFlag(
f'{WAFFLE_FLAG_NAMESPACE}.enable_reported_content_notifications',
__name__
)

View File

@@ -2238,6 +2238,9 @@ MIDDLEWARE = [
#'django.contrib.auth.middleware.AuthenticationMiddleware',
'openedx.core.djangoapps.cache_toolbox.middleware.CacheBackedAuthenticationMiddleware',
# Middleware to flush user's session in other browsers when their email is changed.
'openedx.core.djangoapps.safe_sessions.middleware.EmailChangeMiddleware',
'common.djangoapps.student.middleware.UserStandingMiddleware',
'openedx.core.djangoapps.contentserver.middleware.StaticContentServer',
@@ -5041,6 +5044,20 @@ HIBP_LOGIN_BLOCK_PASSWORD_FREQUENCY_THRESHOLD = 5
# .. toggle_tickets: https://openedx.atlassian.net/browse/VAN-838
ENABLE_DYNAMIC_REGISTRATION_FIELDS = False
############## Settings for EmailChangeMiddleware ###############
# .. toggle_name: ENFORCE_SESSION_EMAIL_MATCH
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
# .. toggle_description: When enabled, this setting invalidates sessions in other browsers
# upon email change, while preserving the session validity in the browser where the
# email change occurs. This toggle is just being used for rollout.
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2023-12-07
# .. toggle_target_removal_date: 2024-04-01
# .. toggle_tickets: https://2u-internal.atlassian.net/browse/VAN-1797
ENFORCE_SESSION_EMAIL_MATCH = False
LEARNER_HOME_MFE_REDIRECT_PERCENTAGE = 0
############### Settings for the ace_common plugin #################

View File

@@ -3,8 +3,6 @@ Content Tagging APIs
"""
from __future__ import annotations
from typing import Iterator
import openedx_tagging.core.tagging.api as oel_tagging
from django.db.models import Q, QuerySet, Exists, OuterRef
from openedx_tagging.core.tagging.models import Taxonomy
@@ -101,7 +99,7 @@ def get_taxonomies_for_org(
return oel_tagging.get_taxonomies(enabled=enabled).filter(
Exists(
TaxonomyOrg.get_relationships(
taxonomy=OuterRef("pk"),
taxonomy=OuterRef("pk"), # type: ignore
rel_type=TaxonomyOrg.RelType.OWNER,
org_short_name=org_short_name,
)
@@ -130,7 +128,7 @@ def get_unassigned_taxonomies(enabled=True) -> QuerySet:
def get_content_tags(
object_key: ContentKey,
taxonomy_id: int | None = None,
) -> Iterator[ContentObjectTag]:
) -> QuerySet:
"""
Generates a list of content tags for a given object.
@@ -147,7 +145,7 @@ def tag_content_object(
object_key: ContentKey,
taxonomy: Taxonomy,
tags: list,
) -> Iterator[ContentObjectTag]:
) -> QuerySet:
"""
This is the main API to use when you want to add/update/delete tags from a content object (e.g. an XBlock or
course).

View File

@@ -6,6 +6,7 @@ from __future__ import annotations
from urllib.parse import parse_qs, urlparse
import json
from unittest.mock import MagicMock
import abc
import ddt
@@ -33,6 +34,7 @@ from openedx.core.djangoapps.content_libraries.api import (
create_library,
set_library_user_permissions,
)
from openedx.core.djangoapps.content_tagging import api as tagging_api
from openedx.core.djangoapps.content_tagging.models import TaxonomyOrg
from openedx.core.djangolib.testing.utils import skip_unless_cms
from openedx.core.lib import blockstore_api
@@ -192,7 +194,7 @@ class TestTaxonomyObjectsMixin:
rel_type=TaxonomyOrg.RelType.OWNER,
)
# Global taxonomy
# Global taxonomy, which contains tags
self.t1 = Taxonomy.objects.create(name="t1", enabled=True)
TaxonomyOrg.objects.create(
taxonomy=self.t1,
@@ -203,6 +205,12 @@ class TestTaxonomyObjectsMixin:
taxonomy=self.t2,
rel_type=TaxonomyOrg.RelType.OWNER,
)
root1 = Tag.objects.create(taxonomy=self.t1, value="ALPHABET")
Tag.objects.create(taxonomy=self.t1, value="android", parent=root1)
Tag.objects.create(taxonomy=self.t1, value="abacus", parent=root1)
Tag.objects.create(taxonomy=self.t1, value="azure", parent=root1)
Tag.objects.create(taxonomy=self.t1, value="aardvark", parent=root1)
Tag.objects.create(taxonomy=self.t1, value="anvil", parent=root1)
# OrgA taxonomy
self.tA1 = Taxonomy.objects.create(name="tA1", enabled=True)
@@ -278,7 +286,8 @@ class TestTaxonomyListCreateViewSet(TestTaxonomyObjectsMixin, APITestCase):
expected_taxonomies: list[str],
enabled_parameter: bool | None = None,
org_parameter: str | None = None,
unassigned_parameter: bool | None = None
unassigned_parameter: bool | None = None,
page_size: int | None = None,
) -> None:
"""
Helper function to call the list endpoint and check the response
@@ -293,6 +302,7 @@ class TestTaxonomyListCreateViewSet(TestTaxonomyObjectsMixin, APITestCase):
"enabled": enabled_parameter,
"org": org_parameter,
"unassigned": unassigned_parameter,
"page_size": page_size,
}.items() if v is not None}
response = self.client.get(url, query_params, format="json")
@@ -304,11 +314,12 @@ class TestTaxonomyListCreateViewSet(TestTaxonomyObjectsMixin, APITestCase):
"""
Tests that staff users see all taxonomies
"""
# Default page_size=10, and so "tBA1" and "tBA2" appear on the second page
# page_size=10, and so "tBA1" and "tBA2" appear on the second page
expected_taxonomies = ["ot1", "ot2", "st1", "st2", "t1", "t2", "tA1", "tA2", "tB1", "tB2"]
self._test_list_taxonomy(
user_attr="staff",
expected_taxonomies=expected_taxonomies,
page_size=10,
)
@ddt.data(
@@ -476,6 +487,29 @@ class TestTaxonomyListCreateViewSet(TestTaxonomyObjectsMixin, APITestCase):
if user_attr == "staffA":
assert response.data["orgs"] == [self.orgA.short_name]
def test_list_taxonomy_query_count(self):
"""
Test how many queries are used when retrieving taxonomies and permissions
"""
url = TAXONOMY_ORG_LIST_URL + f'?org=${self.orgA.short_name}&enabled=true'
self.client.force_authenticate(user=self.staff)
with self.assertNumQueries(16): # TODO Why so many queries?
response = self.client.get(url)
assert response.status_code == 200
assert response.data["can_add_taxonomy"]
assert len(response.data["results"]) == 2
for taxonomy in response.data["results"]:
if taxonomy["system_defined"]:
assert not taxonomy["can_change_taxonomy"]
assert not taxonomy["can_delete_taxonomy"]
assert taxonomy["can_tag_object"]
else:
assert taxonomy["can_change_taxonomy"]
assert taxonomy["can_delete_taxonomy"]
assert taxonomy["can_tag_object"]
@ddt.ddt
class TestTaxonomyDetailExportMixin(TestTaxonomyObjectsMixin):
@@ -787,7 +821,14 @@ class TestTaxonomyDetailViewSet(TestTaxonomyDetailExportMixin, APITestCase):
assert response.status_code == expected_status, reason
if status.is_success(expected_status):
check_taxonomy(response.data, taxonomy.pk, **(TaxonomySerializer(taxonomy.cast()).data))
request = MagicMock()
request.user = user
context = {"request": request}
check_taxonomy(
response.data,
taxonomy.pk,
**(TaxonomySerializer(taxonomy.cast(), context=context)).data,
)
@skip_unless_cms
@@ -1538,12 +1579,12 @@ class TestObjectTagViewSet(TestObjectTagMixin, APITestCase):
# Fetch this object's tags for a single taxonomy
expected_tags = [{
'editable': True,
'name': 'Multiple Taxonomy',
'taxonomy_id': taxonomy.pk,
'can_tag_object': True,
'tags': [
{'value': 'Tag 1', 'lineage': ['Tag 1']},
{'value': 'Tag 2', 'lineage': ['Tag 2']},
{'value': 'Tag 1', 'lineage': ['Tag 1'], 'can_delete_objecttag': True},
{'value': 'Tag 2', 'lineage': ['Tag 2'], 'can_delete_objecttag': True},
],
}]
@@ -1560,6 +1601,28 @@ class TestObjectTagViewSet(TestObjectTagMixin, APITestCase):
assert status.is_success(response3.status_code)
assert response3.data[str(self.courseA)]["taxonomies"] == expected_tags
def test_object_tags_query_count(self):
"""
Test how many queries are used when retrieving object tags and permissions
"""
object_key = self.courseA
object_id = str(object_key)
tagging_api.tag_content_object(object_key=object_key, taxonomy=self.t1, tags=["anvil", "android"])
expected_tags = [
{"value": "android", "lineage": ["ALPHABET", "android"], "can_delete_objecttag": True},
{"value": "anvil", "lineage": ["ALPHABET", "anvil"], "can_delete_objecttag": True},
]
url = OBJECT_TAGS_URL.format(object_id=object_id)
self.client.force_authenticate(user=self.staff)
with self.assertNumQueries(7): # TODO Why so many queries?
response = self.client.get(url)
assert response.status_code == 200
assert len(response.data[object_id]["taxonomies"]) == 1
assert response.data[object_id]["taxonomies"][0]["can_tag_object"]
assert response.data[object_id]["taxonomies"][0]["tags"] == expected_tags
@skip_unless_cms
@ddt.ddt
@@ -2029,3 +2092,27 @@ class TestImportTagsView(ImportTaxonomyMixin, APITestCase):
assert len(tags) == len(self.old_tags)
for i, tag in enumerate(tags):
assert tag["value"] == self.old_tags[i].value
@skip_unless_cms
@ddt.ddt
class TestTaxonomyTagsViewSet(TestTaxonomyObjectsMixin, APITestCase):
"""
Test cases for TaxonomyTagsViewSet retrive action.
"""
def test_taxonomy_tags_query_count(self):
"""
Test how many queries are used when retrieving small taxonomies+tags and permissions
"""
url = f"{TAXONOMY_TAGS_URL}?search_term=an&parent_tag=ALPHABET".format(pk=self.t1.id)
self.client.force_authenticate(user=self.staff)
with self.assertNumQueries(13): # TODO Why so many queries?
response = self.client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.data["can_add_tag"]
assert len(response.data["results"]) == 2
for taxonomy in response.data["results"]:
assert taxonomy["can_change_tag"]
assert taxonomy["can_delete_tag"]

View File

@@ -3,6 +3,7 @@ Taxonomies API v1 URLs.
"""
from rest_framework.routers import DefaultRouter
from openedx_tagging.core.tagging.rest_api.v1.views import ObjectTagCountsView
from django.urls.conf import path, include
@@ -16,6 +17,7 @@ from . import views
router = DefaultRouter()
router.register("taxonomies", views.TaxonomyOrgView, basename="taxonomy")
router.register("object_tags", views.ObjectTagOrgView, basename="object_tag")
router.register("object_tag_counts", ObjectTagCountsView, basename="object_tag_counts")
urlpatterns = [
path(

View File

@@ -81,11 +81,11 @@ class TaxonomyOrgView(TaxonomyView):
serializer.instance = create_taxonomy(**serializer.validated_data, orgs=user_admin_orgs)
@action(detail=False, url_path="import", methods=["post"])
def create_import(self, request: Request, **kwargs) -> Response:
def create_import(self, request: Request, **kwargs) -> Response: # type: ignore
"""
Creates a new taxonomy with the given orgs and imports the tags from the uploaded file.
"""
response = super().create_import(request, **kwargs)
response = super().create_import(request=request, **kwargs) # type: ignore
# If creation was successful, set the orgs for the new taxonomy
if status.is_success(response.status_code):

View File

@@ -219,7 +219,7 @@ def can_change_object_tag_objectid(user: UserType, object_id: str) -> bool:
Everyone that has permission to edit the object should be able to tag it.
"""
if not object_id:
raise ValueError("object_id must be provided")
return True
try:
usage_key = UsageKey.from_string(object_id)
if not usage_key.course_key.is_course:
@@ -274,7 +274,7 @@ def can_change_taxonomy_tag(user: UserType, tag: oel_tagging.Tag | None = None)
return oel_tagging.is_taxonomy_admin(user) and (
not tag
or not taxonomy
or (taxonomy and not taxonomy.allow_free_text and not taxonomy.system_defined)
or (bool(taxonomy) and not taxonomy.allow_free_text and not taxonomy.system_defined)
)

View File

@@ -1,7 +1,6 @@
"""
Audience based filters for notifications
"""
import logging
from abc import abstractmethod
@@ -22,9 +21,6 @@ from openedx.core.djangoapps.django_comment_common.models import (
)
log = logging.getLogger(__name__)
class NotificationAudienceFilterBase:
"""
Base class for notification audience filters
@@ -84,12 +80,10 @@ class CourseRoleAudienceFilter(NotificationAudienceFilterBase):
if 'staff' in course_roles:
staff_users = CourseStaffRole(course_key).users_with_role().values_list('id', flat=True)
log.info(f'Temp: Course wide notification, staff users calculated are {staff_users}')
user_ids.extend(staff_users)
if 'instructor' in course_roles:
instructor_users = CourseInstructorRole(course_key).users_with_role().values_list('id', flat=True)
log.info(f'Temp: Course wide notification, instructor users calculated are {instructor_users}')
user_ids.extend(instructor_users)
return user_ids

View File

@@ -113,6 +113,25 @@ COURSE_NOTIFICATION_TYPES = {
'email_template': '',
'filters': [FILTER_AUDIT_EXPIRED_USERS_WITH_NO_ROLE]
},
'content_reported': {
'notification_app': 'discussion',
'name': 'content_reported',
'is_core': False,
'info': '',
'web': True,
'email': True,
'push': True,
'non_editable': [],
'content_template': _('<p><strong>{username}s </strong> {content_type} has been reported <strong> {'
'content}</strong></p>'),
'content_context': {
'post_title': 'Post title',
'author_name': 'author name',
'replier_name': 'replier name',
},
'email_template': '',
},
}
COURSE_NOTIFICATION_APPS = {

View File

@@ -96,13 +96,10 @@ def calculate_course_wide_notification_audience(course_key, audience_filters):
if filter_class:
filter_instance = filter_class(course_key)
filtered_users = filter_instance.filter(filter_values)
log.info(f'Temp: Course-wide notification filtered users are '
f'{filtered_users} for filter type {filter_type}')
audience_user_ids.extend(filtered_users)
else:
raise ValueError(f"Invalid audience filter type: {filter_type}")
log.info(f'Temp: Course-wide notification after audience filter is applied, users: {list(set(audience_user_ids))}')
return list(set(audience_user_ids))
@@ -131,5 +128,4 @@ def generate_course_notifications(signal, sender, course_notification_data, meta
'content_url': course_notification_data.get('content_url'),
}
log.info(f"Temp: Course-wide notification, user_ids to sent notifications to {notification_data.get('user_ids')}")
send_notifications.delay(**notification_data)

View File

@@ -21,7 +21,7 @@ log = logging.getLogger(__name__)
NOTIFICATION_CHANNELS = ['web', 'push', 'email']
# Update this version when there is a change to any course specific notification type or app.
COURSE_NOTIFICATION_CONFIG_VERSION = 4
COURSE_NOTIFICATION_CONFIG_VERSION = 5
def get_course_notification_preference_config():

View File

@@ -18,6 +18,7 @@ from rest_framework.test import APIClient, APITestCase
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory
from lms.djangoapps.discussion.toggles import ENABLE_REPORTED_CONTENT_NOTIFICATIONS
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.django_comment_common.models import (
FORUM_ROLE_ADMINISTRATOR,
@@ -169,6 +170,7 @@ class CourseEnrollmentPostSaveTest(ModuleStoreTestCase):
@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True)
@override_waffle_flag(ENABLE_REPORTED_CONTENT_NOTIFICATIONS, active=True)
@ddt.ddt
class UserNotificationPreferenceAPITest(ModuleStoreTestCase):
"""
@@ -246,6 +248,7 @@ class UserNotificationPreferenceAPITest(ModuleStoreTestCase):
},
'new_discussion_post': {'web': False, 'email': False, 'push': False, 'info': ''},
'new_question_post': {'web': False, 'email': False, 'push': False, 'info': ''},
'content_reported': {'web': True, 'email': True, 'push': True, 'info': ''},
},
'non_editable': {
'core': ['web']

View File

@@ -4,6 +4,7 @@ Utils function for notifications app
from typing import Dict, List
from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.discussion.toggles import ENABLE_REPORTED_CONTENT_NOTIFICATIONS
from openedx.core.djangoapps.django_comment_common.models import Role
from openedx.core.lib.cache_utils import request_cached
@@ -65,6 +66,10 @@ def filter_course_wide_preferences(course_key, preferences):
if ENABLE_COURSEWIDE_NOTIFICATIONS.is_enabled(course_key):
return preferences
course_wide_notification_types = ['new_discussion_post', 'new_question_post']
if not ENABLE_REPORTED_CONTENT_NOTIFICATIONS.is_enabled(course_key):
course_wide_notification_types.append('content_reported')
config = preferences['notification_preference_config']
for app_prefs in config.values():
notification_types = app_prefs['notification_types']

View File

@@ -95,7 +95,7 @@ from edx_django_utils.logging import encrypt_for_log
from edx_django_utils.monitoring import set_custom_attribute
from edx_toggles.toggles import SettingToggle
from openedx.core.djangoapps.user_authn.cookies import delete_logged_in_cookies
from openedx.core.djangoapps.user_authn.cookies import delete_logged_in_cookies, set_logged_in_cookies
from openedx.core.lib.mobile_utils import is_request_from_mobile_app
# .. toggle_name: LOG_REQUEST_USER_CHANGES
@@ -768,6 +768,92 @@ class SafeSessionMiddleware(SessionMiddleware, MiddlewareMixin):
return encrypt_for_log(str(request.headers), getattr(settings, 'SAFE_SESSIONS_DEBUG_PUBLIC_KEY', None))
class EmailChangeMiddleware(MiddlewareMixin):
"""
Middleware responsible for performing the following
jobs on detecting an email change
1) It will update the session's email and update the JWT cookie
to match the new email.
2) It will invalidate any future session on other browsers where
the user's email does not match its session email.
This middleware ensures that the sessions in other browsers
are invalidated when a user changes their email in one browser.
The active session in which the email change is made will remain valid.
The user's email is stored in their session and JWT cookies during login
and gets updated when the user changes their email.
This middleware checks for any mismatch between the stored email
and the current user's email in each request, and if found,
it invalidates/flushes the session and mark cookies for deletion in request
which are then deleted in the process_response of SafeSessionMiddleware.
"""
def process_request(self, request):
"""
Invalidate the user session if there's a mismatch
between the email in the user's session and request.user.email.
"""
if request.user.is_authenticated:
user_session_email = request.session.get('email', None)
are_emails_mismatched = user_session_email is not None and request.user.email != user_session_email
EmailChangeMiddleware._set_session_email_match_custom_attributes(are_emails_mismatched)
if settings.ENFORCE_SESSION_EMAIL_MATCH and are_emails_mismatched:
# Flush the session and mark cookies for deletion.
log.info(
f'EmailChangeMiddleware invalidating session for user: {request.user.id} due to email mismatch.'
)
request.session.flush()
request.user = AnonymousUser()
_mark_cookie_for_deletion(request)
def process_response(self, request, response):
"""
1. Update the logged-in cookies if the email change was requested
2. Store user's email in session if not already
"""
if request.user.is_authenticated:
if request.session.get('email', None) is None:
# .. custom_attribute_name: session_with_no_email_found
# .. custom_attribute_description: Indicates that user's email was not
# yet stored in the user's session.
set_custom_attribute('session_with_no_email_found', True)
request.session['email'] = request.user.email
if request_cache.get_cached_response('email_change_requested').is_found:
# Update the JWT cookies with new user email
response = set_logged_in_cookies(request, response, request.user)
return response
@staticmethod
def register_email_change(request, email):
"""
Stores the fact that an email change happened.
1. Sets the email in session for later comparison.
2. Sets a request level variable to mark that the user email change was requested.
"""
request.session['email'] = email
request_cache.set('email_change_requested', True)
@staticmethod
def _set_session_email_match_custom_attributes(are_emails_mismatched):
"""
Sets custom attributes of session_email_match
"""
# .. custom_attribute_name: session_email_match
# .. custom_attribute_description: Indicates whether there is a match between the
# email in the user's session and the current user's email in the request.
set_custom_attribute('session_email_mismatch', are_emails_mismatched)
# .. custom_attribute_name: is_enforce_session_email_match_enabled
# .. custom_attribute_description: Indicates whether session email match was enforced.
# When enforced/enabled, it invalidates sessions in other browsers upon email change,
# while preserving the session validity in the browser where the email change occurs.
set_custom_attribute('is_enforce_session_email_match_enabled', settings.ENFORCE_SESSION_EMAIL_MATCH)
def obscure_token(value: Union[str, None]) -> Union[str, None]:
"""
Return a short string that can be used to detect other occurrences

View File

@@ -1,22 +1,29 @@
"""
Unit tests for SafeSessionMiddleware
"""
import uuid
from unittest.mock import call, patch, MagicMock
import ddt
from crum import set_current_request
from django.conf import settings
from django.contrib.auth import SESSION_KEY
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.models import AnonymousUser, User # lint-amnesty, pylint: disable=imported-auth-user
from django.http import HttpResponse, HttpResponseRedirect, SimpleCookie
from django.test import TestCase
from django.test.utils import override_settings
from django.urls import reverse
from edx_django_utils.cache import RequestCache
from edx_rest_framework_extensions.auth.jwt import cookies as jwt_cookies
from openedx.core.djangolib.testing.utils import get_mock_request, CacheIsolationTestCase
from common.djangoapps.student.models import PendingEmailChange
from openedx.core.djangolib.testing.utils import get_mock_request, CacheIsolationTestCase, skip_unless_lms
from openedx.core.djangoapps.user_authn.tests.utils import setup_login_oauth_client
from openedx.core.djangoapps.user_authn.cookies import ALL_LOGGED_IN_COOKIE_NAMES
from common.djangoapps.student.tests.factories import UserFactory
from ..middleware import (
EmailChangeMiddleware,
SafeCookieData,
SafeSessionMiddleware,
mark_user_change_as_expected,
@@ -615,3 +622,748 @@ class TestTrackRequestUserChanges(TestCase):
request.user = object()
assert len(request.debug_user_changes) == 2
assert "Changing request user but user has no id." in request.debug_user_changes[1]
@skip_unless_lms
class TestEmailChangeMiddleware(TestSafeSessionsLogMixin, TestCase):
"""
Test class for EmailChangeMiddleware
"""
def setUp(self):
super().setUp()
self.EMAIL = 'test@example.com'
self.PASSWORD = 'Password1234'
self.user = UserFactory.create(email=self.EMAIL, password=self.PASSWORD)
self.addCleanup(set_current_request, None)
self.request = get_mock_request(self.user)
self.request.session = {}
self.client.response = HttpResponse()
self.client.response.cookies = SimpleCookie()
self.addCleanup(RequestCache.clear_all_namespaces)
self.login_url = reverse("user_api_login_session", kwargs={'api_version': 'v2'})
self.register_url = reverse("user_api_registration_v2")
self.dashboard_url = reverse('dashboard')
@override_settings(ENFORCE_SESSION_EMAIL_MATCH=False)
@patch('openedx.core.djangoapps.safe_sessions.middleware._mark_cookie_for_deletion')
def test_process_request_user_not_authenticated_with_toggle_disabled(self, mock_mark_cookie_for_deletion):
"""
Calls EmailChangeMiddleware.process_request when no user is authenticated
and ENFORCE_SESSION_EMAIL_MATCH toggle is disabled.
Verifies that session and cookies are not affected.
"""
# Unauthenticated User
self.request.user = AnonymousUser()
# Call process_request without authenticating a user
EmailChangeMiddleware(get_response=lambda request: None).process_request(self.request)
# Assert that session and cookies are not affected
# Assert that _mark_cookie_for_deletion not called
mock_mark_cookie_for_deletion.assert_not_called()
@override_settings(ENFORCE_SESSION_EMAIL_MATCH=True)
@patch('openedx.core.djangoapps.safe_sessions.middleware._mark_cookie_for_deletion')
def test_process_request_user_not_authenticated_with_toggle_enabled(self, mock_mark_cookie_for_deletion):
"""
Calls EmailChangeMiddleware.process_request when no user is authenticated
and ENFORCE_SESSION_EMAIL_MATCH toggle is enabled.
Verifies that session and cookies are not affected.
"""
# Unauthenticated User
self.request.user = AnonymousUser()
# Call process_request without authenticating a user
EmailChangeMiddleware(get_response=lambda request: None).process_request(self.request)
# Assert that session and cookies are not affected
# Assert that _mark_cookie_for_deletion not called
mock_mark_cookie_for_deletion.assert_not_called()
@override_settings(ENFORCE_SESSION_EMAIL_MATCH=True)
@patch('openedx.core.djangoapps.safe_sessions.middleware._mark_cookie_for_deletion')
@patch("openedx.core.djangoapps.safe_sessions.middleware.set_custom_attribute")
def test_process_request_emails_match_with_toggle_enabled(
self, mock_set_custom_attribute, mock_mark_cookie_for_deletion
):
"""
Calls EmailChangeMiddleware.process_request when user is authenticated,
ENFORCE_SESSION_EMAIL_MATCH is enabled and user session and request email also match.
Verifies that session and cookies are not affected.
"""
# Log in the user
self.client.login(email=self.user.email, password=self.PASSWORD)
self.request.session = self.client.session
self.client.response.set_cookie(settings.SESSION_COOKIE_NAME, 'authenticated') # Add some logged-in cookie
# Registering email change (store user's email in session for later comparison by
# process_request function of middleware)
EmailChangeMiddleware.register_email_change(request=self.request, email=self.user.email)
# Ensure email is set in the session
self.assertEqual(self.request.session.get('email'), self.user.email)
# Ensure session cookie exist
self.assertEqual(len(self.client.response.cookies), 1)
# No email change occurred in any browser
# Call process_request
EmailChangeMiddleware(get_response=lambda request: None).process_request(self.request)
# Verify that session_email_mismatch and is_enforce_session_email_match_enabled
# custom attributes are set
mock_set_custom_attribute.assert_has_calls([call('session_email_mismatch', False)])
mock_set_custom_attribute.assert_has_calls([call('is_enforce_session_email_match_enabled', True)])
# Assert that the session and cookies are not affected
self.assertEqual(self.request.session.get('email'), self.user.email)
self.assertEqual(len(self.client.response.cookies), 1)
self.assertEqual(self.client.response.cookies[settings.SESSION_COOKIE_NAME].value, 'authenticated')
# Assert that _mark_cookie_for_deletion not called
mock_mark_cookie_for_deletion.assert_not_called()
@override_settings(ENFORCE_SESSION_EMAIL_MATCH=False)
@patch('openedx.core.djangoapps.safe_sessions.middleware._mark_cookie_for_deletion')
@patch("openedx.core.djangoapps.safe_sessions.middleware.set_custom_attribute")
def test_process_request_emails_match_with_toggle_disabled(
self, mock_set_custom_attribute, mock_mark_cookie_for_deletion
):
"""
Calls EmailChangeMiddleware.process_request when user is authenticated,
ENFORCE_SESSION_EMAIL_MATCH is disabled and user session and request email match.
Verifies that session and cookies are not affected.
"""
# Log in the user
self.client.login(email=self.user.email, password=self.PASSWORD)
self.request.session = self.client.session
self.client.response.set_cookie(settings.SESSION_COOKIE_NAME, 'authenticated') # Add some logged-in cookie
# Registering email change (store user's email in session for later comparison by
# process_request function of middleware)
EmailChangeMiddleware.register_email_change(request=self.request, email=self.user.email)
# Ensure email is set in the session
self.assertEqual(self.request.session.get('email'), self.user.email)
# Ensure session cookie exist
self.assertEqual(len(self.client.response.cookies), 1)
# No email change occurred in any browser
# Call process_request
EmailChangeMiddleware(get_response=lambda request: None).process_request(self.request)
# Verify that session_email_mismatch and is_enforce_session_email_match_enabled
# custom attributes are set
mock_set_custom_attribute.assert_has_calls([call('session_email_mismatch', False)])
mock_set_custom_attribute.assert_has_calls([call('is_enforce_session_email_match_enabled', False)])
# Assert that the session and cookies are not affected
self.assertEqual(self.request.session.get('email'), self.user.email)
self.assertEqual(len(self.client.response.cookies), 1)
self.assertEqual(self.client.response.cookies[settings.SESSION_COOKIE_NAME].value, 'authenticated')
# Assert that _mark_cookie_for_deletion not called
mock_mark_cookie_for_deletion.assert_not_called()
@override_settings(ENFORCE_SESSION_EMAIL_MATCH=True)
@patch('openedx.core.djangoapps.safe_sessions.middleware._mark_cookie_for_deletion')
@patch("openedx.core.djangoapps.safe_sessions.middleware.set_custom_attribute")
def test_process_request_emails_mismatch_with_toggle_enabled(
self, mock_set_custom_attribute, mock_mark_cookie_for_deletion
):
"""
Calls EmailChangeMiddleware.process_request when user is authenticated,
ENFORCE_SESSION_EMAIL_MATCH is enabled and user session and request
email mismatch. (Email was changed in some other browser)
Verifies that session is flushed and cookies are marked for deletion.
"""
# Log in the user
self.client.login(email=self.user.email, password=self.PASSWORD)
self.request.session = self.client.session
self.client.response.set_cookie(settings.SESSION_COOKIE_NAME, 'authenticated') # Add some logged-in cookie
# Registering email change (store user's email in session for later comparison by
# process_request function of middleware)
EmailChangeMiddleware.register_email_change(request=self.request, email=self.user.email)
# Ensure email is set in the session
self.assertEqual(self.request.session.get('email'), self.user.email)
# Ensure session cookie exist
self.assertEqual(len(self.client.response.cookies), 1)
# simulating email changed in some other browser
self.user.email = 'new_email@test.com'
self.user.save()
# Call process_request
EmailChangeMiddleware(get_response=lambda request: None).process_request(self.request)
# Verify that session_email_mismatch and is_enforce_session_email_match_enabled
# custom attributes are set
mock_set_custom_attribute.assert_has_calls([call('session_email_mismatch', True)])
mock_set_custom_attribute.assert_has_calls([call('is_enforce_session_email_match_enabled', True)])
# Assert that the session is flushed and cookies marked for deletion
mock_mark_cookie_for_deletion.assert_called()
assert self.request.session.get(SESSION_KEY) is None
assert self.request.user == AnonymousUser()
@override_settings(ENFORCE_SESSION_EMAIL_MATCH=False)
@patch('openedx.core.djangoapps.safe_sessions.middleware._mark_cookie_for_deletion')
@patch("openedx.core.djangoapps.safe_sessions.middleware.set_custom_attribute")
def test_process_request_emails_mismatch_with_toggle_disabled(
self, mock_set_custom_attribute, mock_mark_cookie_for_deletion
):
"""
Calls EmailChangeMiddleware.process_request when user is authenticated,
ENFORCE_SESSION_EMAIL_MATCH is disabled and user session and request
email mismatch. (Email was changed in some other browser)
Verifies that session and cookies are not affected.
"""
# Log in the user
self.client.login(email=self.user.email, password=self.PASSWORD)
self.request.session = self.client.session
self.client.response.set_cookie(settings.SESSION_COOKIE_NAME, 'authenticated') # Add some logged-in cookie
# Registering email change (store user's email in session for later comparison by
# process_request function of middleware)
EmailChangeMiddleware.register_email_change(request=self.request, email=self.user.email)
# Ensure email is set in the session
self.assertEqual(self.request.session.get('email'), self.user.email)
# Ensure session cookie exist
self.assertEqual(len(self.client.response.cookies), 1)
# simulating email changed in some other browser
self.user.email = 'new_email@test.com'
self.user.save()
# Call process_request
EmailChangeMiddleware(get_response=lambda request: None).process_request(self.request)
# Verify that session_email_mismatch and is_enforce_session_email_match_enabled
# custom attributes are set
mock_set_custom_attribute.assert_has_calls([call('session_email_mismatch', True)])
mock_set_custom_attribute.assert_has_calls([call('is_enforce_session_email_match_enabled', False)])
# Assert that the session and cookies are not affected
self.assertNotEqual(self.request.session.get('email'), self.user.email)
self.assertEqual(len(self.client.response.cookies), 1)
self.assertEqual(self.client.response.cookies[settings.SESSION_COOKIE_NAME].value, 'authenticated')
# Assert that _mark_cookie_for_deletion not called
mock_mark_cookie_for_deletion.assert_not_called()
@override_settings(ENFORCE_SESSION_EMAIL_MATCH=True)
@patch('openedx.core.djangoapps.safe_sessions.middleware._mark_cookie_for_deletion')
def test_process_request_no_email_change_history_with_toggle_enabled(
self, mock_mark_cookie_for_deletion
):
"""
Calls EmailChangeMiddleware.process_request when there is no previous
history of an email change and ENFORCE_SESSION_EMAIL_MATCH is enabled
Verifies that existing sessions are not affected.
Test that sessions predating this code are not affected.
"""
# Log in the user (Simulating user logged-in before this code and email was not set in session)
self.client.login(email=self.user.email, password=self.PASSWORD)
self.request.session = self.client.session
self.client.response.set_cookie(settings.SESSION_COOKIE_NAME, 'authenticated') # Add some logged-in cookie
# Ensure there is no email in the session denoting no previous history of email change
self.assertEqual(self.request.session.get('email'), None)
# Ensure session cookie exist
self.assertEqual(len(self.client.response.cookies), 1)
# simulating email changed in some other browser
self.user.email = 'new_email@test.com'
self.user.save()
# Call process_request
EmailChangeMiddleware(get_response=lambda request: None).process_request(self.request)
# Assert that the session and cookies are not affected
self.assertEqual(len(self.client.response.cookies), 1)
self.assertEqual(self.client.response.cookies[settings.SESSION_COOKIE_NAME].value, 'authenticated')
# Assert that _mark_cookie_for_deletion not called
mock_mark_cookie_for_deletion.assert_not_called()
@override_settings(ENFORCE_SESSION_EMAIL_MATCH=False)
@patch('openedx.core.djangoapps.safe_sessions.middleware._mark_cookie_for_deletion')
def test_process_request_no_email_change_history_with_toggle_disabled(
self, mock_mark_cookie_for_deletion
):
"""
Calls EmailChangeMiddleware.process_request when there is no previous
history of an email change and ENFORCE_SESSION_EMAIL_MATCH is disabled
Verifies that existing sessions are not affected.
Test that sessions predating this code are not affected.
"""
# Log in the user (Simulating user logged-in before this code and email was not set in session)
self.client.login(email=self.user.email, password=self.PASSWORD)
self.request.session = self.client.session
self.client.response.set_cookie(settings.SESSION_COOKIE_NAME, 'authenticated') # Add some logged-in cookie
# Ensure there is no email in the session denoting no previous history of email change
self.assertEqual(self.request.session.get('email'), None)
# Ensure session cookie exist
self.assertEqual(len(self.client.response.cookies), 1)
# simulating email changed in some other browser
self.user.email = 'new_email@test.com'
self.user.save()
# Call process_request
EmailChangeMiddleware(get_response=lambda request: None).process_request(self.request)
# Assert that the session and cookies are not affected
self.assertEqual(len(self.client.response.cookies), 1)
self.assertEqual(self.client.response.cookies[settings.SESSION_COOKIE_NAME].value, 'authenticated')
# Assert that _mark_cookie_for_deletion not called
mock_mark_cookie_for_deletion.assert_not_called()
@patch("openedx.core.djangoapps.safe_sessions.middleware.set_logged_in_cookies")
def test_process_response_user_not_authenticated(self, mock_set_logged_in_cookies):
"""
Calls EmailChangeMiddleware.process_response when user is not authenticated.
Verify that the logged-in cookies are not updated
"""
# return value of mock
mock_set_logged_in_cookies.return_value = self.client.response
# Unauthenticated User
self.request.user = AnonymousUser()
# Call process_response without authenticating a user
response = EmailChangeMiddleware(get_response=lambda request: None).process_response(
self.request, self.client.response
)
assert response.status_code == 200
# Assert that cookies are not updated
# Assert that mock_set_logged_in_cookies not called
mock_set_logged_in_cookies.assert_not_called()
@patch("openedx.core.djangoapps.safe_sessions.middleware.set_logged_in_cookies")
def test_process_response_user_authenticated_but_email_change_not_requested(self, mock_set_logged_in_cookies):
"""
Calls EmailChangeMiddleware.process_response when user is authenticated but email
change was not requested.
Verify that the logged-in cookies are not updated
"""
# return value of mock
mock_set_logged_in_cookies.return_value = self.client.response
# Log in the user
self.client.login(email=self.user.email, password=self.PASSWORD)
self.request.session = self.client.session
self.client.response.set_cookie(settings.SESSION_COOKIE_NAME, 'authenticated') # Add some logged-in cookie
# No call to register_email_change to indicate email was not changed
# Call process_response
response = EmailChangeMiddleware(get_response=lambda request: None).process_response(
self.request, self.client.response
)
assert response.status_code == 200
# Assert that cookies are not updated
# Assert that mock_set_logged_in_cookies not called
mock_set_logged_in_cookies.assert_not_called()
@patch("openedx.core.djangoapps.safe_sessions.middleware.set_logged_in_cookies")
def test_process_response_user_authenticated_and_email_change_requested(self, mock_set_logged_in_cookies):
"""
Calls EmailChangeMiddleware.process_response when user is authenticated and email
change was requested.
Verify that the logged-in cookies are updated
"""
# return value of mock
mock_set_logged_in_cookies.return_value = self.client.response
# Log in the user
self.client.login(email=self.user.email, password=self.PASSWORD)
self.request.session = self.client.session
self.client.response.set_cookie(settings.SESSION_COOKIE_NAME, 'authenticated') # Add some logged-in cookie
# Registering email change (setting a variable `email_change_requested` to indicate email was changed)
# so that process_response can update cookies
EmailChangeMiddleware.register_email_change(request=self.request, email=self.user.email)
# Call process_response
response = EmailChangeMiddleware(get_response=lambda request: None).process_response(
self.request, self.client.response
)
assert response.status_code == 200
# Assert that cookies are updated
# Assert that mock_set_logged_in_cookies is called
mock_set_logged_in_cookies.assert_called()
def test_process_response_no_email_in_session(self):
"""
Calls EmailChangeMiddleware.process_response when user is authenticated and
user's email was not stored in user's session.
Verify that the user's email is stored in session
"""
# Log in the user
self.client.login(email=self.user.email, password=self.PASSWORD)
self.request.session = self.client.session
self.client.response.set_cookie(settings.SESSION_COOKIE_NAME, 'authenticated') # Add some logged-in cookie
# Ensure there is no email in the session
self.assertEqual(self.request.session.get('email'), None)
# Call process_response
response = EmailChangeMiddleware(get_response=lambda request: None).process_response(
self.request, self.client.response
)
assert response.status_code == 200
# Verify that email is set in the session
self.assertEqual(self.request.session.get('email'), self.user.email)
@patch.dict("django.conf.settings.FEATURES", {"DISABLE_SET_JWT_COOKIES_FOR_TESTS": False})
def test_user_remain_authenticated_on_email_change_in_other_browser_with_toggle_disabled(self):
"""
Integration Test: test that a user remains authenticated upon email change
in other browser when ENFORCE_SESSION_EMAIL_MATCH toggle is disabled
Verify that the session and cookies are not affected in current browser and
user remains authenticated
"""
setup_login_oauth_client()
# Login the user with 'test@example.com` email and test password in current browser
response = self.client.post(self.login_url, {
"email_or_username": self.EMAIL,
"password": self.PASSWORD,
})
# Verify that the user is logged in successfully in current browser
assert response.status_code == 200
# Verify that the logged-in cookies are set in current browser
self._assert_logged_in_cookies_present(response)
# Verify that the authenticated user can access the dashboard in current browser
response = self.client.get(self.dashboard_url)
assert response.status_code == 200
# simulating email changed in some other browser (Email is changed in DB)
self.user.email = 'new_email@test.com'
self.user.save()
# Verify that the user remains authenticated in current browser and can access the dashboard
response = self.client.get(self.dashboard_url)
assert response.status_code == 200
@patch.dict("django.conf.settings.FEATURES", {"DISABLE_SET_JWT_COOKIES_FOR_TESTS": False})
@override_settings(ENFORCE_SESSION_EMAIL_MATCH=True)
def test_cookies_are_updated_with_new_email_on_email_change_with_toggle_enabled(self):
"""
Integration Test: test that cookies are updated with new email upon email change
in current browser regardless of toggle setting
Verify that the cookies are updated in current browser and
user remains authenticated
"""
setup_login_oauth_client()
# Login the user with 'test@example.com` email and test password in current browser
login_response = self.client.post(self.login_url, {
"email_or_username": self.EMAIL,
"password": self.PASSWORD,
})
# Verify that the user is logged in successfully in current browser
assert login_response.status_code == 200
# Verify that the logged-in cookies are set in current browser
self._assert_logged_in_cookies_present(login_response)
# Verify that the authenticated user can access the dashboard in current browser
response = self.client.get(self.dashboard_url)
assert response.status_code == 200
# simulating email change in current browser
activation_key = uuid.uuid4().hex
PendingEmailChange.objects.update_or_create(
user=self.user,
defaults={
'new_email': 'new_email@test.com',
'activation_key': activation_key,
}
)
email_change_response = self.client.get(
reverse('confirm_email_change', kwargs={'key': activation_key}),
)
# Verify that email change is successful
assert email_change_response.status_code == 200
self._assert_logged_in_cookies_present(email_change_response)
# Verify that jwt cookies are updated with new email and
# not equal to old logged-in cookies in current browser
self.assertNotEqual(
login_response.cookies[jwt_cookies.jwt_cookie_header_payload_name()].value,
email_change_response.cookies[jwt_cookies.jwt_cookie_header_payload_name()].value
)
self.assertNotEqual(
login_response.cookies[jwt_cookies.jwt_cookie_signature_name()].value,
email_change_response.cookies[jwt_cookies.jwt_cookie_signature_name()].value
)
@patch.dict("django.conf.settings.FEATURES", {"DISABLE_SET_JWT_COOKIES_FOR_TESTS": False})
@override_settings(ENFORCE_SESSION_EMAIL_MATCH=False)
def test_cookies_are_updated_with_new_email_on_email_change_with_toggle_disabled(self):
"""
Integration Test: test that cookies are updated with new email upon email change
in current browser regardless of toggle setting
Verify that the cookies are updated in current browser and
user remains authenticated
"""
setup_login_oauth_client()
# Login the user with 'test@example.com` email and test password in current browser
login_response = self.client.post(self.login_url, {
"email_or_username": self.EMAIL,
"password": self.PASSWORD,
})
# Verify that the user is logged in successfully in current browser
assert login_response.status_code == 200
# Verify that the logged-in cookies are set in current browser
self._assert_logged_in_cookies_present(login_response)
# Verify that the authenticated user can access the dashboard in current browser
response = self.client.get(self.dashboard_url)
assert response.status_code == 200
# simulating email change in current browser
activation_key = uuid.uuid4().hex
PendingEmailChange.objects.update_or_create(
user=self.user,
defaults={
'new_email': 'new_email@test.com',
'activation_key': activation_key,
}
)
email_change_response = self.client.get(
reverse('confirm_email_change', kwargs={'key': activation_key}),
)
# Verify that email change is successful
assert email_change_response.status_code == 200
self._assert_logged_in_cookies_present(email_change_response)
# Verify that jwt cookies are updated with new email and
# not equal to old logged-in cookies in current browser
self.assertNotEqual(
login_response.cookies[jwt_cookies.jwt_cookie_header_payload_name()].value,
email_change_response.cookies[jwt_cookies.jwt_cookie_header_payload_name()].value
)
self.assertNotEqual(
login_response.cookies[jwt_cookies.jwt_cookie_signature_name()].value,
email_change_response.cookies[jwt_cookies.jwt_cookie_signature_name()].value
)
@patch.dict("django.conf.settings.FEATURES", {"DISABLE_SET_JWT_COOKIES_FOR_TESTS": False})
@override_settings(ENFORCE_SESSION_EMAIL_MATCH=True)
def test_logged_in_user_unauthenticated_on_email_change_in_other_browser(self):
"""
Integration Test: Test that a user logged-in in one browser gets unauthenticated
when the email is changed in some other browser and the request and session emails mismatch.
Verify that the session is invalidated and cookies are deleted in current browser
and user gets unauthenticated.
"""
setup_login_oauth_client()
# Login the user with 'test@example.com` email and test password in current browser
response = self.client.post(self.login_url, {
"email_or_username": self.EMAIL,
"password": self.PASSWORD,
})
# Verify that the user is logged in successfully in current browser
assert response.status_code == 200
# Verify that the logged-in cookies are set in current browser
self._assert_logged_in_cookies_present(response)
# Verify that the authenticated user can access the dashboard in current browser
response = self.client.get(self.dashboard_url)
assert response.status_code == 200
# simulating email changed in some other browser (Email is changed in DB)
self.user.email = 'new_email@test.com'
self.user.save()
# Verify that the user gets unauthenticated in current browser and cannot access the dashboard
response = self.client.get(self.dashboard_url)
assert response.status_code == 302
self._assert_logged_in_cookies_not_present(response)
@patch.dict("django.conf.settings.FEATURES", {"DISABLE_SET_JWT_COOKIES_FOR_TESTS": False})
@override_settings(ENFORCE_SESSION_EMAIL_MATCH=True)
def test_logged_in_user_remains_authenticated_on_email_change_in_same_browser(self):
"""
Integration Test: test that a user logged-in in some browser remains authenticated
when the email is changed in same browser.
Verify that the session and cookies are updated in current browser and
user remains authenticated
"""
setup_login_oauth_client()
# Login the user with 'test@example.com` email and test password in current browser
response = self.client.post(self.login_url, {
"email_or_username": self.EMAIL,
"password": self.PASSWORD,
})
# Verify that the user is logged in successfully in current browser
assert response.status_code == 200
# Verify that the logged-in cookies are set in current browser
self._assert_logged_in_cookies_present(response)
# Verify that the authenticated user can access the dashboard in current browser
response = self.client.get(self.dashboard_url)
assert response.status_code == 200
# simulating email change in current browser
activation_key = uuid.uuid4().hex
PendingEmailChange.objects.update_or_create(
user=self.user,
defaults={
'new_email': 'new_email@test.com',
'activation_key': activation_key,
}
)
email_change_response = self.client.get(
reverse('confirm_email_change', kwargs={'key': activation_key}),
)
# Verify that email change is successful and all logged-in
# cookies are set in current browser
assert email_change_response.status_code == 200
self._assert_logged_in_cookies_present(email_change_response)
# Verify that the user remains authenticated in current browser and can access the dashboard
response = self.client.get(self.dashboard_url)
assert response.status_code == 200
@patch.dict("django.conf.settings.FEATURES", {"DISABLE_SET_JWT_COOKIES_FOR_TESTS": False})
@override_settings(ENFORCE_SESSION_EMAIL_MATCH=True)
def test_registered_user_unauthenticated_on_email_change_in_other_browser(self):
"""
Integration Test: Test that a user registered in one browser gets unauthenticated
when the email is changed in some other browser and the request and session emails mismatch.
Verify that the session is invalidated and cookies are deleted in current browser
and user gets unauthenticated
"""
setup_login_oauth_client()
# Register the user with 'john_doe@example.com` email and test password in current browser
response = self.client.post(self.register_url, {
"email": 'john_doe@example.com',
"name": 'John Doe',
"username": 'john_doe',
"password": 'password',
"honor_code": "true",
})
# Verify that the user is logged in successfully in current browser
assert response.status_code == 200
# Verify that the logged-in cookies are set in current browser
self._assert_logged_in_cookies_present(response)
# Verify that the authenticated user can access the dashboard in current browser
response = self.client.get(self.dashboard_url)
assert response.status_code == 200
# simulating email changed in some other browser (Email is changed in DB)
registered_user = User.objects.get(email='john_doe@example.com')
registered_user.email = 'new_email@test.com'
registered_user.save()
# Verify that the user get unauthenticated in current browser and cannot access the dashboard
response = self.client.get(self.dashboard_url)
assert response.status_code == 302
self._assert_logged_in_cookies_not_present(response)
@patch.dict("django.conf.settings.FEATURES", {"DISABLE_SET_JWT_COOKIES_FOR_TESTS": False})
@override_settings(ENFORCE_SESSION_EMAIL_MATCH=True)
def test_registered_user_remain_authenticated_on_email_change_in_same_browser(self):
"""
Integration Test: test that a user registered in one browser remains
authenticated in current browser when the email is changed in same browser.
Verify that the session and cookies updated and user remains
authenticated in current browser
"""
setup_login_oauth_client()
# Register the user with 'john_doe@example.com` email and test password in current browser
response = self.client.post(self.register_url, {
"email": 'john_doe@example.com',
"name": 'John Doe',
"username": 'john_doe',
"password": 'password',
"honor_code": "true",
})
# Verify that the user is logged in successfully in current browser
assert response.status_code == 200
# Verify that the logged-in cookies are set in current browser
self._assert_logged_in_cookies_present(response)
# Verify that the authenticated user can access the dashboard in current browser
response = self.client.get(self.dashboard_url)
assert response.status_code == 200
# getting newly created user
registered_user = User.objects.get(email='john_doe@example.com')
# simulating email change in current browser
activation_key = uuid.uuid4().hex
PendingEmailChange.objects.update_or_create(
user=registered_user,
defaults={
'new_email': 'new_email@test.com',
'activation_key': activation_key,
}
)
email_change_response = self.client.get(
reverse('confirm_email_change', kwargs={'key': activation_key}),
)
# Verify that email change is successful and all logged-in
# cookies are updated with new email in current browser
assert email_change_response.status_code == 200
self._assert_logged_in_cookies_present(email_change_response)
# Verify that the user remains authenticated in current browser and can access the dashboard
response = self.client.get(self.dashboard_url)
assert response.status_code == 200
def _assert_logged_in_cookies_present(self, response):
"""
Helper function to verify that all logged-in cookies are available
and have valid values (not empty strings)
"""
all_cookies = ALL_LOGGED_IN_COOKIE_NAMES + (settings.SESSION_COOKIE_NAME,)
for cookie in all_cookies:
# Check if the cookie is present in response.cookies.keys()
self.assertIn(cookie, response.cookies.keys())
# Assert that the value is not an empty string
self.assertNotEqual(response.cookies[cookie].value, "")
def _assert_logged_in_cookies_not_present(self, response):
"""
Helper function to verify that all logged-in cookies are cleared
and have empty values
"""
all_cookies = ALL_LOGGED_IN_COOKIE_NAMES + (settings.SESSION_COOKIE_NAME,)
for cookie in all_cookies:
# Check if the cookie is present in response.cookies.keys()
self.assertIn(cookie, response.cookies.keys())
# Assert that the value is not an empty string
self.assertEqual(response.cookies[cookie].value, "")

View File

@@ -232,7 +232,7 @@ class TestOwnUsernameAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAP
Test that a client (logged in) can get her own username.
"""
self.client.login(username=self.user.username, password=TEST_PASSWORD)
self._verify_get_own_username(16)
self._verify_get_own_username(19)
def test_get_username_inactive(self):
"""
@@ -242,7 +242,7 @@ class TestOwnUsernameAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAP
self.client.login(username=self.user.username, password=TEST_PASSWORD)
self.user.is_active = False
self.user.save()
self._verify_get_own_username(16)
self._verify_get_own_username(19)
def test_get_username_not_logged_in(self):
"""
@@ -358,7 +358,7 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe
"""
ENABLED_CACHES = ['default']
TOTAL_QUERY_COUNT = 24
TOTAL_QUERY_COUNT = 27
FULL_RESPONSE_FIELD_COUNT = 29
def setUp(self):
@@ -811,7 +811,7 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe
assert data['time_zone'] is None
self.client.login(username=self.user.username, password=TEST_PASSWORD)
verify_get_own_information(self._get_num_queries(22))
verify_get_own_information(self._get_num_queries(25))
# Now make sure that the user can get the same information, even if not active
self.user.is_active = False
@@ -831,7 +831,7 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe
legacy_profile.save()
self.client.login(username=self.user.username, password=TEST_PASSWORD)
with self.assertNumQueries(self._get_num_queries(22), table_ignorelist=WAFFLE_TABLES):
with self.assertNumQueries(self._get_num_queries(25), table_ignorelist=WAFFLE_TABLES):
response = self.send_get(self.client)
for empty_field in ("level_of_education", "gender", "country", "state", "bio",):
assert response.data[empty_field] is None

View File

@@ -23,7 +23,7 @@ click>=8.0,<9.0
# 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.10.9
edx-enterprise==4.10.11
# Stay on LTS version, remove once this is added to common constraint
Django<5.0
@@ -108,7 +108,17 @@ libsass==0.10.0
click==8.1.6
# pinning this version to avoid updates while the library is being developed
openedx-learning==0.4.2
openedx-learning==0.4.4
# Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise.
openai<=0.28.1
# optimizely-sdk 5.0.0 is breaking following test with segmentation fault
# common/djangoapps/third_party_auth/tests/test_views.py::SAMLMetadataTest::test_secure_key_configuration
# needs to be fixed in the follow up issue
# https://github.com/openedx/edx-platform/issues/34103
optimizely-sdk<5.0
# lxml 5.1.0 introduced a breaking change in unit test shards
# This constraint can probably be removed once lxml==5.1.1 is released on PyPI
lxml<5.0

View File

@@ -22,7 +22,7 @@ cryptography==38.0.4
# -r requirements/edx-sandbox/py38.in
cycler==0.12.1
# via matplotlib
fonttools==4.46.0
fonttools==4.47.2
# via matplotlib
importlib-resources==6.1.1
# via matplotlib
@@ -30,11 +30,12 @@ joblib==1.3.2
# via nltk
kiwisolver==1.4.5
# via matplotlib
lxml==4.9.3
lxml==4.9.4
# via
# -c requirements/edx-sandbox/../constraints.txt
# -r requirements/edx-sandbox/py38.in
# openedx-calc
markupsafe==2.1.3
markupsafe==2.1.4
# via
# chem
# openedx-calc
@@ -59,7 +60,7 @@ openedx-calc==3.0.1
# via -r requirements/edx-sandbox/py38.in
packaging==23.2
# via matplotlib
pillow==10.1.0
pillow==10.2.0
# via matplotlib
pycparser==2.21
# via cffi
@@ -71,9 +72,9 @@ pyparsing==3.1.1
# openedx-calc
python-dateutil==2.8.2
# via matplotlib
random2==1.0.1
random2==1.0.2
# via -r requirements/edx-sandbox/py38.in
regex==2023.10.3
regex==2023.12.25
# via nltk
scipy==1.7.3
# via

View File

@@ -35,7 +35,7 @@ async-timeout==4.0.3
# via
# aiohttp
# redis
attrs==23.1.0
attrs==23.2.0
# via
# -r requirements/edx/kernel.in
# aiohttp
@@ -59,7 +59,7 @@ backports-zoneinfo[tzdata]==0.2.1
# django
# icalendar
# kombu
beautifulsoup4==4.12.2
beautifulsoup4==4.12.3
# via pynliner
billiard==4.2.0
# via celery
@@ -74,13 +74,13 @@ bleach[css]==6.1.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/kernel.in
boto3==1.33.12
boto3==1.34.28
# via
# -r requirements/edx/kernel.in
# django-ses
# fs-s3fs
# ora2
botocore==1.33.12
botocore==1.34.28
# via
# -r requirements/edx/kernel.in
# boto3
@@ -244,6 +244,7 @@ django==4.2.9
# openedx-learning
# ora2
# super-csv
# xblock-google-drive
# xss-utils
django-appconf==1.0.6
# via django-statici18n
@@ -284,12 +285,12 @@ django-filter==23.5
# edx-enterprise
# lti-consumer-xblock
# openedx-blockstore
django-ipware==6.0.2
django-ipware==6.0.3
# via
# -r requirements/edx/kernel.in
# edx-enterprise
# edx-proctoring
django-js-asset==2.1.0
django-js-asset==2.2.0
# via django-mptt
django-method-override==1.0.4
# via -r requirements/edx/kernel.in
@@ -326,7 +327,7 @@ django-oauth-toolkit==1.7.1
# edx-enterprise
django-object-actions==4.2.0
# via edx-enterprise
django-pipeline==2.1.0
django-pipeline==3.0.0
# via -r requirements/edx/kernel.in
django-ratelimit==4.1.0
# via -r requirements/edx/kernel.in
@@ -400,9 +401,9 @@ done-xblock==2.2.0
# via -r requirements/edx/bundled.in
drf-jwt==1.19.2
# via edx-drf-extensions
drf-nested-routers==0.93.4
drf-nested-routers==0.93.5
# via openedx-blockstore
drf-spectacular==0.27.0
drf-spectacular==0.27.1
# via -r requirements/edx/kernel.in
drf-yasg==1.21.5
# via
@@ -420,7 +421,7 @@ edx-auth-backends==4.2.0
# via
# -r requirements/edx/kernel.in
# openedx-blockstore
edx-braze-client==0.1.8
edx-braze-client==0.2.1
# via
# -r requirements/edx/bundled.in
# edx-enterprise
@@ -448,7 +449,7 @@ edx-django-release-util==1.3.0
# openedx-blockstore
edx-django-sites-extensions==4.0.2
# via -r requirements/edx/kernel.in
edx-django-utils==5.9.0
edx-django-utils==5.10.1
# via
# -r requirements/edx/kernel.in
# django-config-models
@@ -464,7 +465,7 @@ edx-django-utils==5.9.0
# openedx-blockstore
# ora2
# super-csv
edx-drf-extensions==9.1.2
edx-drf-extensions==10.1.0
# via
# -r requirements/edx/kernel.in
# edx-completion
@@ -476,11 +477,11 @@ edx-drf-extensions==9.1.2
# edx-when
# edxval
# openedx-learning
edx-enterprise==4.10.9
edx-enterprise==4.10.11
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
edx-event-bus-kafka==5.5.0
edx-event-bus-kafka==5.6.0
# via -r requirements/edx/kernel.in
edx-event-bus-redis==0.3.2
# via -r requirements/edx/kernel.in
@@ -521,7 +522,7 @@ edx-rest-api-client==5.6.1
# edx-proctoring
edx-search==3.8.2
# via -r requirements/edx/kernel.in
edx-sga==0.23.0
edx-sga==0.23.1
# via -r requirements/edx/bundled.in
edx-submissions==3.6.0
# via
@@ -562,11 +563,11 @@ event-tracking==2.2.0
# edx-completion
# edx-proctoring
# edx-search
fastavro==1.9.1
fastavro==1.9.3
# via openedx-events
filelock==3.13.1
# via snowflake-connector-python
frozenlist==1.4.0
frozenlist==1.4.1
# via
# aiohttp
# aiosignal
@@ -603,7 +604,7 @@ idna==3.6
# requests
# snowflake-connector-python
# yarl
importlib-metadata==7.0.0
importlib-metadata==7.0.1
# via markdown
importlib-resources==5.13.0
# via
@@ -622,7 +623,7 @@ isodate==0.6.1
# via python3-saml
itypes==1.2.0
# via coreapi
jinja2==3.1.2
jinja2==3.1.3
# via
# code-annotations
# coreschema
@@ -643,17 +644,17 @@ jsonfield==3.1.0
# edx-submissions
# lti-consumer-xblock
# ora2
jsonschema==4.20.0
jsonschema==4.21.1
# via
# drf-spectacular
# optimizely-sdk
jsonschema-specifications==2023.11.2
jsonschema-specifications==2023.12.1
# via jsonschema
jwcrypto==1.5.0
jwcrypto==1.5.1
# via
# django-oauth-toolkit
# pylti1p3
kombu==5.3.4
kombu==5.3.5
# via celery
laboratory==1.0.2
# via -r requirements/edx/kernel.in
@@ -670,10 +671,11 @@ libsass==0.10.0
# -r requirements/edx/paver.txt
loremipsum==1.0.5
# via ora2
lti-consumer-xblock==9.8.1
lti-consumer-xblock==9.8.3
# via -r requirements/edx/kernel.in
lxml==4.9.3
lxml==4.9.4
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
# edx-i18n-tools
# edxval
@@ -692,7 +694,6 @@ mako==1.3.0
# acid-xblock
# lti-consumer-xblock
# xblock
# xblock-google-drive
# xblock-utils
markdown==3.3.7
# via
@@ -703,7 +704,7 @@ markdown==3.3.7
# xblock-poll
markey==0.8
# via enmerkar-underscore
markupsafe==2.1.3
markupsafe==2.1.4
# via
# -r requirements/edx/paver.txt
# chem
@@ -711,7 +712,7 @@ markupsafe==2.1.3
# mako
# openedx-calc
# xblock
maxminddb==2.5.1
maxminddb==2.5.2
# via geoip2
mock==5.1.0
# via -r requirements/edx/paver.txt
@@ -727,11 +728,11 @@ multidict==6.0.4
# via
# aiohttp
# yarl
mysqlclient==2.2.0
mysqlclient==2.2.1
# via
# -r requirements/edx/kernel.in
# openedx-blockstore
newrelic==9.3.0
newrelic==9.6.0
# via
# -r requirements/edx/bundled.in
# edx-django-utils
@@ -758,13 +759,13 @@ openai==0.28.1
# via
# -c requirements/edx/../constraints.txt
# edx-enterprise
openedx-atlas==0.5.0
openedx-atlas==0.6.0
# via -r requirements/edx/kernel.in
openedx-blockstore==1.4.0
# via -r requirements/edx/kernel.in
openedx-calc==3.0.1
# via -r requirements/edx/kernel.in
openedx-django-pyfs==3.4.0
openedx-django-pyfs==3.4.1
# via
# lti-consumer-xblock
# xblock
@@ -772,7 +773,7 @@ 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==9.2.0
openedx-events==9.3.0
# via
# -r requirements/edx/kernel.in
# edx-event-bus-kafka
@@ -781,15 +782,17 @@ openedx-filters==1.6.0
# via
# -r requirements/edx/kernel.in
# lti-consumer-xblock
openedx-learning==0.4.2
openedx-learning==0.4.4
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
openedx-mongodbproxy==0.2.0
# via -r requirements/edx/kernel.in
optimizely-sdk==4.1.1
# via -r requirements/edx/bundled.in
ora2==6.0.29
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/bundled.in
ora2==6.0.30
# via -r requirements/edx/bundled.in
packaging==23.2
# via
@@ -820,7 +823,7 @@ pgpy==0.6.0
# via edx-enterprise
piexif==1.1.3
# via -r requirements/edx/kernel.in
pillow==10.1.0
pillow==10.2.0
# via
# -r requirements/edx/kernel.in
# edx-enterprise
@@ -832,9 +835,9 @@ platformdirs==3.11.0
# via snowflake-connector-python
polib==1.2.0
# via edx-i18n-tools
prompt-toolkit==3.0.42
prompt-toolkit==3.0.43
# via click-repl
psutil==5.9.6
psutil==5.9.8
# via
# -r requirements/edx/paver.txt
# edx-django-utils
@@ -848,7 +851,7 @@ pycountry==23.12.11
# via -r requirements/edx/kernel.in
pycparser==2.21
# via cffi
pycryptodomex==3.19.0
pycryptodomex==3.20.0
# via
# -r requirements/edx/kernel.in
# edx-proctoring
@@ -921,11 +924,11 @@ python-dateutil==2.8.2
# olxcleaner
# ora2
# xblock
python-ipware==2.0.0
python-ipware==2.0.1
# via django-ipware
python-memcached==1.59
python-memcached==1.62
# via -r requirements/edx/paver.txt
python-slugify==8.0.1
python-slugify==8.0.2
# via code-annotations
python-swiftclient==4.4.0
# via ora2
@@ -966,19 +969,19 @@ pyyaml==6.0.1
# edx-django-release-util
# edx-i18n-tools
# xblock
random2==1.0.1
random2==1.0.2
# via -r requirements/edx/kernel.in
recommender-xblock==2.0.1
recommender-xblock==2.1.1
# via -r requirements/edx/bundled.in
redis==5.0.1
# via
# -r requirements/edx/kernel.in
# walrus
referencing==0.32.0
referencing==0.32.1
# via
# jsonschema
# jsonschema-specifications
regex==2023.10.3
regex==2023.12.25
# via nltk
requests==2.31.0
# via
@@ -1003,11 +1006,12 @@ requests==2.31.0
# slumber
# snowflake-connector-python
# social-auth-core
# xblock-google-drive
requests-oauthlib==1.3.1
# via
# -r requirements/edx/kernel.in
# social-auth-core
rpds-py==0.13.2
rpds-py==0.17.1
# via
# jsonschema
# referencing
@@ -1021,7 +1025,7 @@ rules==3.3
# edx-enterprise
# edx-proctoring
# openedx-learning
s3transfer==0.8.2
s3transfer==0.10.0
# via boto3
sailthru-client==2.2.3
# via edx-ace
@@ -1070,14 +1074,13 @@ six==1.16.0
# py2neo
# pyjwkest
# python-dateutil
# python-memcached
slumber==0.7.1
# via
# -r requirements/edx/kernel.in
# edx-bulk-grades
# edx-enterprise
# edx-rest-api-client
snowflake-connector-python==3.6.0
snowflake-connector-python==3.7.0
# via edx-enterprise
social-auth-app-django==5.0.0
# via
@@ -1142,7 +1145,7 @@ typing-extensions==4.9.0
# kombu
# pylti1p3
# snowflake-connector-python
tzdata==2023.3
tzdata==2023.4
# via
# backports-zoneinfo
# celery
@@ -1177,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.12
wcwidth==0.2.13
# via prompt-toolkit
web-fragments==2.1.0
# via
@@ -1200,7 +1203,7 @@ wrapt==1.16.0
# via
# -r requirements/edx/paver.txt
# deprecated
xblock[django]==1.9.0
xblock[django]==1.10.0
# via
# -r requirements/edx/kernel.in
# acid-xblock
@@ -1216,16 +1219,14 @@ xblock[django]==1.9.0
# xblock-google-drive
# xblock-poll
# xblock-utils
xblock-drag-and-drop-v2==3.3.0
xblock-drag-and-drop-v2==3.4.0
# via -r requirements/edx/bundled.in
xblock-google-drive==0.5.0
xblock-google-drive==0.6.1
# via -r requirements/edx/bundled.in
xblock-poll==1.13.0
# via -r requirements/edx/bundled.in
xblock-utils==4.0.0
# via
# edx-sga
# xblock-google-drive
# via edx-sga
xmlsec==1.3.13
# via python3-saml
xss-utils==0.5.0

View File

@@ -6,15 +6,15 @@
#
chardet==5.2.0
# via diff-cover
coverage==7.3.2
coverage==7.4.0
# via -r requirements/edx/coverage.in
diff-cover==8.0.1
diff-cover==8.0.3
# via -r requirements/edx/coverage.in
jinja2==3.1.2
jinja2==3.1.3
# via diff-cover
markupsafe==2.1.3
markupsafe==2.1.4
# via jinja2
pluggy==1.3.0
pluggy==1.4.0
# via diff-cover
pygments==2.17.2
# via diff-cover

View File

@@ -53,10 +53,9 @@ annotated-types==0.6.0
# via
# -r requirements/edx/testing.txt
# pydantic
anyio==3.7.1
anyio==4.2.0
# via
# -r requirements/edx/testing.txt
# fastapi
# starlette
appdirs==1.4.4
# via
@@ -86,7 +85,7 @@ async-timeout==4.0.3
# -r requirements/edx/testing.txt
# aiohttp
# redis
attrs==23.1.0
attrs==23.2.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -120,7 +119,7 @@ backports-zoneinfo[tzdata]==0.2.1
# django
# icalendar
# kombu
beautifulsoup4==4.12.2
beautifulsoup4==4.12.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -146,14 +145,14 @@ boto==2.49.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
boto3==1.33.12
boto3==1.34.28
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# django-ses
# fs-s3fs
# ora2
botocore==1.33.12
botocore==1.34.28
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -282,7 +281,7 @@ coreschema==0.0.4
# -r requirements/edx/testing.txt
# coreapi
# drf-yasg
coverage[toml]==7.3.2
coverage[toml]==7.4.0
# via
# -r requirements/edx/testing.txt
# coverage
@@ -314,9 +313,9 @@ cssutils==2.9.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# pynliner
ddt==1.7.0
ddt==1.7.1
# via -r requirements/edx/testing.txt
deepmerge==1.1.0
deepmerge==1.1.1
# via
# -r requirements/edx/doc.txt
# sphinxcontrib-openapi
@@ -333,7 +332,7 @@ deprecated==1.2.14
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# jwcrypto
diff-cover==8.0.1
diff-cover==8.0.3
# via -r requirements/edx/testing.txt
dill==0.3.7
# via
@@ -417,6 +416,7 @@ django==4.2.9
# openedx-learning
# ora2
# super-csv
# xblock-google-drive
# xss-utils
django-appconf==1.0.6
# via
@@ -482,13 +482,13 @@ django-filter==23.5
# edx-enterprise
# lti-consumer-xblock
# openedx-blockstore
django-ipware==6.0.2
django-ipware==6.0.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-enterprise
# edx-proctoring
django-js-asset==2.1.0
django-js-asset==2.2.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -541,7 +541,7 @@ django-object-actions==4.2.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-enterprise
django-pipeline==2.1.0
django-pipeline==3.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -657,12 +657,12 @@ drf-jwt==1.19.2
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-drf-extensions
drf-nested-routers==0.93.4
drf-nested-routers==0.93.5
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# openedx-blockstore
drf-spectacular==0.27.0
drf-spectacular==0.27.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -688,7 +688,7 @@ edx-auth-backends==4.2.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# openedx-blockstore
edx-braze-client==0.1.8
edx-braze-client==0.2.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -727,7 +727,7 @@ edx-django-sites-extensions==4.0.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
edx-django-utils==5.9.0
edx-django-utils==5.10.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -744,7 +744,7 @@ edx-django-utils==5.9.0
# openedx-blockstore
# ora2
# super-csv
edx-drf-extensions==9.1.2
edx-drf-extensions==10.1.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -757,12 +757,12 @@ edx-drf-extensions==9.1.2
# edx-when
# edxval
# openedx-learning
edx-enterprise==4.10.9
edx-enterprise==4.10.11
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
edx-event-bus-kafka==5.5.0
edx-event-bus-kafka==5.6.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -826,7 +826,7 @@ edx-search==3.8.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
edx-sga==0.23.0
edx-sga==0.23.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -898,15 +898,15 @@ execnet==2.0.2
# pytest-xdist
factory-boy==3.3.0
# via -r requirements/edx/testing.txt
faker==20.1.0
faker==22.5.1
# via
# -r requirements/edx/testing.txt
# factory-boy
fastapi==0.105.0
fastapi==0.109.0
# via
# -r requirements/edx/testing.txt
# pact-python
fastavro==1.9.1
fastavro==1.9.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -918,9 +918,9 @@ filelock==3.13.1
# snowflake-connector-python
# tox
# virtualenv
freezegun==1.3.1
freezegun==1.4.0
# via -r requirements/edx/testing.txt
frozenlist==1.4.0
frozenlist==1.4.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -951,13 +951,13 @@ gitdb==4.0.11
# via
# -r requirements/edx/doc.txt
# gitpython
gitpython==3.1.40
gitpython==3.1.41
# via -r requirements/edx/doc.txt
glob2==0.7
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
grimp==3.1
grimp==3.2
# via
# -r requirements/edx/testing.txt
# import-linter
@@ -997,9 +997,9 @@ imagesize==1.4.1
# via
# -r requirements/edx/doc.txt
# sphinx
import-linter==1.12.1
import-linter==2.0
# via -r requirements/edx/testing.txt
importlib-metadata==7.0.0
importlib-metadata==7.0.1
# via
# -r requirements/edx/../pip-tools.txt
# -r requirements/edx/doc.txt
@@ -1039,7 +1039,7 @@ isodate==0.6.1
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# python3-saml
isort==5.13.1
isort==5.13.2
# via
# -r requirements/edx/testing.txt
# pylint
@@ -1048,7 +1048,7 @@ itypes==1.2.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# coreapi
jinja2==3.1.2
jinja2==3.1.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1082,25 +1082,25 @@ jsonfield==3.1.0
# edx-submissions
# lti-consumer-xblock
# ora2
jsonschema==4.20.0
jsonschema==4.21.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# drf-spectacular
# optimizely-sdk
# sphinxcontrib-openapi
jsonschema-specifications==2023.11.2
jsonschema-specifications==2023.12.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# jsonschema
jwcrypto==1.5.0
jwcrypto==1.5.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# django-oauth-toolkit
# pylti1p3
kombu==5.3.4
kombu==5.3.5
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1117,7 +1117,7 @@ lazy==1.6
# lti-consumer-xblock
# ora2
# xblock
lazy-object-proxy==1.9.0
lazy-object-proxy==1.10.0
# via
# -r requirements/edx/testing.txt
# astroid
@@ -1132,12 +1132,13 @@ loremipsum==1.0.5
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# ora2
lti-consumer-xblock==9.8.1
lti-consumer-xblock==9.8.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
lxml==4.9.3
lxml==4.9.4
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-i18n-tools
@@ -1161,7 +1162,6 @@ mako==1.3.0
# acid-xblock
# lti-consumer-xblock
# xblock
# xblock-google-drive
# xblock-utils
markdown==3.3.7
# via
@@ -1176,7 +1176,7 @@ markey==0.8
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# enmerkar-underscore
markupsafe==2.1.3
markupsafe==2.1.4
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1185,7 +1185,7 @@ markupsafe==2.1.3
# mako
# openedx-calc
# xblock
maxminddb==2.5.1
maxminddb==2.5.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1223,19 +1223,19 @@ multidict==6.0.4
# -r requirements/edx/testing.txt
# aiohttp
# yarl
mypy==1.7.1
mypy==1.8.0
# via
# -r requirements/edx/development.in
# django-stubs
# djangorestframework-stubs
mypy-extensions==1.0.0
# via mypy
mysqlclient==2.2.0
mysqlclient==2.2.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# openedx-blockstore
newrelic==9.3.0
newrelic==9.6.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1276,7 +1276,7 @@ openai==0.28.1
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-enterprise
openedx-atlas==0.5.0
openedx-atlas==0.6.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1288,7 +1288,7 @@ openedx-calc==3.0.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
openedx-django-pyfs==3.4.0
openedx-django-pyfs==3.4.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1302,7 +1302,7 @@ openedx-django-wiki==2.0.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
openedx-events==9.2.0
openedx-events==9.3.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1313,7 +1313,7 @@ openedx-filters==1.6.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# lti-consumer-xblock
openedx-learning==0.4.2
openedx-learning==0.4.4
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
@@ -1324,9 +1324,10 @@ openedx-mongodbproxy==0.2.0
# -r requirements/edx/testing.txt
optimizely-sdk==4.1.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
ora2==6.0.29
ora2==6.0.30
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1387,7 +1388,7 @@ piexif==1.1.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
pillow==10.1.0
pillow==10.2.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1409,7 +1410,7 @@ platformdirs==3.11.0
# snowflake-connector-python
# tox
# virtualenv
pluggy==1.3.0
pluggy==1.4.0
# via
# -r requirements/edx/testing.txt
# diff-cover
@@ -1420,12 +1421,12 @@ polib==1.2.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-i18n-tools
prompt-toolkit==3.0.42
prompt-toolkit==3.0.43
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# click-repl
psutil==5.9.6
psutil==5.9.8
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1457,18 +1458,18 @@ pycparser==2.21
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# cffi
pycryptodomex==3.19.0
pycryptodomex==3.20.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-proctoring
# lti-consumer-xblock
# pyjwkest
pydantic==2.5.2
pydantic==2.5.3
# via
# -r requirements/edx/testing.txt
# fastapi
pydantic-core==2.14.5
pydantic-core==2.14.6
# via
# -r requirements/edx/testing.txt
# pydantic
@@ -1593,7 +1594,7 @@ pysrt==1.1.2
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edxval
pytest==7.4.3
pytest==7.4.4
# via
# -r requirements/edx/testing.txt
# pylint-pytest
@@ -1638,16 +1639,16 @@ python-dateutil==2.8.2
# olxcleaner
# ora2
# xblock
python-ipware==2.0.0
python-ipware==2.0.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# django-ipware
python-memcached==1.59
python-memcached==1.62
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
python-slugify==8.0.1
python-slugify==8.0.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1704,11 +1705,11 @@ pyyaml==6.0.1
# edx-i18n-tools
# sphinxcontrib-openapi
# xblock
random2==1.0.1
random2==1.0.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
recommender-xblock==2.0.1
recommender-xblock==2.1.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1717,13 +1718,13 @@ redis==5.0.1
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# walrus
referencing==0.32.0
referencing==0.32.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# jsonschema
# jsonschema-specifications
regex==2023.10.3
regex==2023.12.25
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1755,12 +1756,13 @@ requests==2.31.0
# snowflake-connector-python
# social-auth-core
# sphinx
# xblock-google-drive
requests-oauthlib==1.3.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# social-auth-core
rpds-py==0.13.2
rpds-py==0.17.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1783,7 +1785,7 @@ rules==3.3
# edx-enterprise
# edx-proctoring
# openedx-learning
s3transfer==0.8.2
s3transfer==0.10.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1851,7 +1853,6 @@ six==1.16.0
# py2neo
# pyjwkest
# python-dateutil
# python-memcached
# sphinxcontrib-httpdomain
slumber==0.7.1
# via
@@ -1872,7 +1873,7 @@ snowballstemmer==2.2.0
# via
# -r requirements/edx/doc.txt
# sphinx
snowflake-connector-python==3.6.0
snowflake-connector-python==3.7.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1970,7 +1971,7 @@ staff-graded-xblock==2.2.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
starlette==0.27.0
starlette==0.35.1
# via
# -r requirements/edx/testing.txt
# fastapi
@@ -2008,8 +2009,6 @@ tinycss2==1.2.1
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# bleach
toml==0.10.2
# via vulture
tomli==2.0.1
# via
# -r requirements/edx/../pip-tools.txt
@@ -2025,6 +2024,7 @@ tomli==2.0.1
# pyproject-hooks
# pytest
# tox
# vulture
tomlkit==0.12.3
# via
# -r requirements/edx/doc.txt
@@ -2054,6 +2054,7 @@ typing-extensions==4.9.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# annotated-types
# anyio
# asgiref
# astroid
# django-countries
@@ -2076,7 +2077,7 @@ typing-extensions==4.9.0
# snowflake-connector-python
# starlette
# uvicorn
tzdata==2023.3
tzdata==2023.4
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2110,7 +2111,7 @@ user-util==1.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
uvicorn==0.24.0.post1
uvicorn==0.27.0
# via
# -r requirements/edx/testing.txt
# pact-python
@@ -2130,7 +2131,7 @@ voluptuous==0.14.1
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# ora2
vulture==2.10
vulture==2.11
# via -r requirements/edx/development.in
walrus==0.9.3
# via
@@ -2142,7 +2143,7 @@ watchdog==3.0.0
# -r requirements/edx/development.in
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
wcwidth==0.2.12
wcwidth==0.2.13
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2178,7 +2179,7 @@ wrapt==1.16.0
# -r requirements/edx/testing.txt
# astroid
# deprecated
xblock[django]==1.9.0
xblock[django]==1.10.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2196,11 +2197,11 @@ xblock[django]==1.9.0
# xblock-google-drive
# xblock-poll
# xblock-utils
xblock-drag-and-drop-v2==3.3.0
xblock-drag-and-drop-v2==3.4.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
xblock-google-drive==0.5.0
xblock-google-drive==0.6.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2213,7 +2214,6 @@ xblock-utils==4.0.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-sga
# xblock-google-drive
xmlsec==1.3.13
# via
# -r requirements/edx/doc.txt

View File

@@ -52,7 +52,7 @@ async-timeout==4.0.3
# -r requirements/edx/base.txt
# aiohttp
# redis
attrs==23.1.0
attrs==23.2.0
# via
# -r requirements/edx/base.txt
# aiohttp
@@ -82,7 +82,7 @@ backports-zoneinfo[tzdata]==0.2.1
# django
# icalendar
# kombu
beautifulsoup4==4.12.2
beautifulsoup4==4.12.3
# via
# -r requirements/edx/base.txt
# pydata-sphinx-theme
@@ -103,13 +103,13 @@ bleach[css]==6.1.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/base.txt
boto3==1.33.12
boto3==1.34.28
# via
# -r requirements/edx/base.txt
# django-ses
# fs-s3fs
# ora2
botocore==1.33.12
botocore==1.34.28
# via
# -r requirements/edx/base.txt
# boto3
@@ -211,7 +211,7 @@ cssutils==2.9.0
# via
# -r requirements/edx/base.txt
# pynliner
deepmerge==1.1.0
deepmerge==1.1.1
# via sphinxcontrib-openapi
defusedxml==0.7.1
# via
@@ -294,6 +294,7 @@ django==4.2.9
# openedx-learning
# ora2
# super-csv
# xblock-google-drive
# xss-utils
django-appconf==1.0.6
# via
@@ -344,12 +345,12 @@ django-filter==23.5
# edx-enterprise
# lti-consumer-xblock
# openedx-blockstore
django-ipware==6.0.2
django-ipware==6.0.3
# via
# -r requirements/edx/base.txt
# edx-enterprise
# edx-proctoring
django-js-asset==2.1.0
django-js-asset==2.2.0
# via
# -r requirements/edx/base.txt
# django-mptt
@@ -392,7 +393,7 @@ django-object-actions==4.2.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
django-pipeline==2.1.0
django-pipeline==3.0.0
# via -r requirements/edx/base.txt
django-ratelimit==4.1.0
# via -r requirements/edx/base.txt
@@ -475,11 +476,11 @@ drf-jwt==1.19.2
# via
# -r requirements/edx/base.txt
# edx-drf-extensions
drf-nested-routers==0.93.4
drf-nested-routers==0.93.5
# via
# -r requirements/edx/base.txt
# openedx-blockstore
drf-spectacular==0.27.0
drf-spectacular==0.27.1
# via -r requirements/edx/base.txt
drf-yasg==1.21.5
# via
@@ -498,7 +499,7 @@ edx-auth-backends==4.2.0
# via
# -r requirements/edx/base.txt
# openedx-blockstore
edx-braze-client==0.1.8
edx-braze-client==0.2.1
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -526,7 +527,7 @@ edx-django-release-util==1.3.0
# openedx-blockstore
edx-django-sites-extensions==4.0.2
# via -r requirements/edx/base.txt
edx-django-utils==5.9.0
edx-django-utils==5.10.1
# via
# -r requirements/edx/base.txt
# django-config-models
@@ -542,7 +543,7 @@ edx-django-utils==5.9.0
# openedx-blockstore
# ora2
# super-csv
edx-drf-extensions==9.1.2
edx-drf-extensions==10.1.0
# via
# -r requirements/edx/base.txt
# edx-completion
@@ -554,11 +555,11 @@ edx-drf-extensions==9.1.2
# edx-when
# edxval
# openedx-learning
edx-enterprise==4.10.9
edx-enterprise==4.10.11
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
edx-event-bus-kafka==5.5.0
edx-event-bus-kafka==5.6.0
# via -r requirements/edx/base.txt
edx-event-bus-redis==0.3.2
# via -r requirements/edx/base.txt
@@ -603,7 +604,7 @@ edx-rest-api-client==5.6.1
# edx-proctoring
edx-search==3.8.2
# via -r requirements/edx/base.txt
edx-sga==0.23.0
edx-sga==0.23.1
# via -r requirements/edx/base.txt
edx-submissions==3.6.0
# via
@@ -649,7 +650,7 @@ event-tracking==2.2.0
# edx-completion
# edx-proctoring
# edx-search
fastavro==1.9.1
fastavro==1.9.3
# via
# -r requirements/edx/base.txt
# openedx-events
@@ -657,7 +658,7 @@ filelock==3.13.1
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
frozenlist==1.4.0
frozenlist==1.4.1
# via
# -r requirements/edx/base.txt
# aiohttp
@@ -680,7 +681,7 @@ geoip2==4.8.0
# via -r requirements/edx/base.txt
gitdb==4.0.11
# via gitpython
gitpython==3.1.40
gitpython==3.1.41
# via -r requirements/edx/doc.in
glob2==0.7
# via -r requirements/edx/base.txt
@@ -703,7 +704,7 @@ idna==3.6
# yarl
imagesize==1.4.1
# via sphinx
importlib-metadata==7.0.0
importlib-metadata==7.0.1
# via
# -r requirements/edx/base.txt
# markdown
@@ -733,7 +734,7 @@ itypes==1.2.0
# via
# -r requirements/edx/base.txt
# coreapi
jinja2==3.1.2
jinja2==3.1.3
# via
# -r requirements/edx/base.txt
# code-annotations
@@ -761,22 +762,22 @@ jsonfield==3.1.0
# edx-submissions
# lti-consumer-xblock
# ora2
jsonschema==4.20.0
jsonschema==4.21.1
# via
# -r requirements/edx/base.txt
# drf-spectacular
# optimizely-sdk
# sphinxcontrib-openapi
jsonschema-specifications==2023.11.2
jsonschema-specifications==2023.12.1
# via
# -r requirements/edx/base.txt
# jsonschema
jwcrypto==1.5.0
jwcrypto==1.5.1
# via
# -r requirements/edx/base.txt
# django-oauth-toolkit
# pylti1p3
kombu==5.3.4
kombu==5.3.5
# via
# -r requirements/edx/base.txt
# celery
@@ -797,10 +798,11 @@ loremipsum==1.0.5
# via
# -r requirements/edx/base.txt
# ora2
lti-consumer-xblock==9.8.1
lti-consumer-xblock==9.8.3
# via -r requirements/edx/base.txt
lxml==4.9.3
lxml==4.9.4
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# edx-i18n-tools
# edxval
@@ -819,7 +821,6 @@ mako==1.3.0
# acid-xblock
# lti-consumer-xblock
# xblock
# xblock-google-drive
# xblock-utils
markdown==3.3.7
# via
@@ -832,7 +833,7 @@ markey==0.8
# via
# -r requirements/edx/base.txt
# enmerkar-underscore
markupsafe==2.1.3
markupsafe==2.1.4
# via
# -r requirements/edx/base.txt
# chem
@@ -840,7 +841,7 @@ markupsafe==2.1.3
# mako
# openedx-calc
# xblock
maxminddb==2.5.1
maxminddb==2.5.2
# via
# -r requirements/edx/base.txt
# geoip2
@@ -864,11 +865,11 @@ multidict==6.0.4
# -r requirements/edx/base.txt
# aiohttp
# yarl
mysqlclient==2.2.0
mysqlclient==2.2.1
# via
# -r requirements/edx/base.txt
# openedx-blockstore
newrelic==9.3.0
newrelic==9.6.0
# via
# -r requirements/edx/base.txt
# edx-django-utils
@@ -899,13 +900,13 @@ openai==0.28.1
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# edx-enterprise
openedx-atlas==0.5.0
openedx-atlas==0.6.0
# via -r requirements/edx/base.txt
openedx-blockstore==1.4.0
# via -r requirements/edx/base.txt
openedx-calc==3.0.1
# via -r requirements/edx/base.txt
openedx-django-pyfs==3.4.0
openedx-django-pyfs==3.4.1
# via
# -r requirements/edx/base.txt
# lti-consumer-xblock
@@ -914,7 +915,7 @@ 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==9.2.0
openedx-events==9.3.0
# via
# -r requirements/edx/base.txt
# edx-event-bus-kafka
@@ -923,15 +924,17 @@ openedx-filters==1.6.0
# via
# -r requirements/edx/base.txt
# lti-consumer-xblock
openedx-learning==0.4.2
openedx-learning==0.4.4
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
openedx-mongodbproxy==0.2.0
# via -r requirements/edx/base.txt
optimizely-sdk==4.1.1
# via -r requirements/edx/base.txt
ora2==6.0.29
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
ora2==6.0.30
# via -r requirements/edx/base.txt
packaging==23.2
# via
@@ -971,7 +974,7 @@ picobox==4.0.0
# via sphinxcontrib-openapi
piexif==1.1.3
# via -r requirements/edx/base.txt
pillow==10.1.0
pillow==10.2.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -989,11 +992,11 @@ polib==1.2.0
# via
# -r requirements/edx/base.txt
# edx-i18n-tools
prompt-toolkit==3.0.42
prompt-toolkit==3.0.43
# via
# -r requirements/edx/base.txt
# click-repl
psutil==5.9.6
psutil==5.9.8
# via
# -r requirements/edx/base.txt
# edx-django-utils
@@ -1011,7 +1014,7 @@ pycparser==2.21
# via
# -r requirements/edx/base.txt
# cffi
pycryptodomex==3.19.0
pycryptodomex==3.20.0
# via
# -r requirements/edx/base.txt
# edx-proctoring
@@ -1098,13 +1101,13 @@ python-dateutil==2.8.2
# olxcleaner
# ora2
# xblock
python-ipware==2.0.0
python-ipware==2.0.1
# via
# -r requirements/edx/base.txt
# django-ipware
python-memcached==1.59
python-memcached==1.62
# via -r requirements/edx/base.txt
python-slugify==8.0.1
python-slugify==8.0.2
# via
# -r requirements/edx/base.txt
# code-annotations
@@ -1150,20 +1153,20 @@ pyyaml==6.0.1
# edx-i18n-tools
# sphinxcontrib-openapi
# xblock
random2==1.0.1
random2==1.0.2
# via -r requirements/edx/base.txt
recommender-xblock==2.0.1
recommender-xblock==2.1.1
# via -r requirements/edx/base.txt
redis==5.0.1
# via
# -r requirements/edx/base.txt
# walrus
referencing==0.32.0
referencing==0.32.1
# via
# -r requirements/edx/base.txt
# jsonschema
# jsonschema-specifications
regex==2023.10.3
regex==2023.12.25
# via
# -r requirements/edx/base.txt
# nltk
@@ -1191,11 +1194,12 @@ requests==2.31.0
# snowflake-connector-python
# social-auth-core
# sphinx
# xblock-google-drive
requests-oauthlib==1.3.1
# via
# -r requirements/edx/base.txt
# social-auth-core
rpds-py==0.13.2
rpds-py==0.17.1
# via
# -r requirements/edx/base.txt
# jsonschema
@@ -1214,7 +1218,7 @@ rules==3.3
# edx-enterprise
# edx-proctoring
# openedx-learning
s3transfer==0.8.2
s3transfer==0.10.0
# via
# -r requirements/edx/base.txt
# boto3
@@ -1269,7 +1273,6 @@ six==1.16.0
# py2neo
# pyjwkest
# python-dateutil
# python-memcached
# sphinxcontrib-httpdomain
slumber==0.7.1
# via
@@ -1281,7 +1284,7 @@ smmap==5.0.1
# via gitdb
snowballstemmer==2.2.0
# via sphinx
snowflake-connector-python==3.6.0
snowflake-connector-python==3.7.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -1399,7 +1402,7 @@ typing-extensions==4.9.0
# pydata-sphinx-theme
# pylti1p3
# snowflake-connector-python
tzdata==2023.3
tzdata==2023.4
# via
# -r requirements/edx/base.txt
# backports-zoneinfo
@@ -1441,7 +1444,7 @@ walrus==0.9.3
# edx-event-bus-redis
watchdog==3.0.0
# via -r requirements/edx/base.txt
wcwidth==0.2.12
wcwidth==0.2.13
# via
# -r requirements/edx/base.txt
# prompt-toolkit
@@ -1467,7 +1470,7 @@ wrapt==1.16.0
# via
# -r requirements/edx/base.txt
# deprecated
xblock[django]==1.9.0
xblock[django]==1.10.0
# via
# -r requirements/edx/base.txt
# acid-xblock
@@ -1484,9 +1487,9 @@ xblock[django]==1.9.0
# xblock-google-drive
# xblock-poll
# xblock-utils
xblock-drag-and-drop-v2==3.3.0
xblock-drag-and-drop-v2==3.4.0
# via -r requirements/edx/base.txt
xblock-google-drive==0.5.0
xblock-google-drive==0.6.1
# via -r requirements/edx/base.txt
xblock-poll==1.13.0
# via -r requirements/edx/base.txt
@@ -1494,7 +1497,6 @@ xblock-utils==4.0.0
# via
# -r requirements/edx/base.txt
# edx-sga
# xblock-google-drive
xmlsec==1.3.13
# via
# -r requirements/edx/base.txt

View File

@@ -73,8 +73,8 @@ edx-codejail
edx-django-utils>=5.4.0 # Utilities for cache, monitoring, and plugins
edx-drf-extensions
edx-enterprise
# edx-event-bus-kafka 4.0.0 adds support for configurable consumer API
edx-event-bus-kafka>=4.0.1 # Kafka implementation of event bus
# edx-event-bus-kafka 5.6.0 adds support for putting client ids on event producers/consumers
edx-event-bus-kafka>=5.6.0 # Kafka implementation of event bus
edx-event-bus-redis
edx-milestones
edx-name-affirmation

View File

@@ -20,7 +20,7 @@ libsass==0.10.0
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/paver.in
markupsafe==2.1.3
markupsafe==2.1.4
# via -r requirements/edx/paver.in
mock==5.1.0
# via -r requirements/edx/paver.in
@@ -30,7 +30,7 @@ paver==1.3.4
# via -r requirements/edx/paver.in
pbr==6.0.0
# via stevedore
psutil==5.9.6
psutil==5.9.8
# via -r requirements/edx/paver.in
pymemcache==4.0.0
# via -r requirements/edx/paver.in
@@ -39,7 +39,7 @@ pymongo==3.13.0
# -c requirements/edx/../constraints.txt
# -r requirements/edx/paver.in
# edx-opaque-keys
python-memcached==1.59
python-memcached==1.62
# via -r requirements/edx/paver.in
requests==2.31.0
# via -r requirements/edx/paver.in
@@ -47,7 +47,6 @@ six==1.16.0
# via
# libsass
# paver
# python-memcached
stevedore==5.1.0
# via
# -r requirements/edx/paver.in

View File

@@ -4,7 +4,7 @@
#
# make upgrade
#
attrs==23.1.0
attrs==23.2.0
# via
# glom
# jsonschema
@@ -44,9 +44,9 @@ importlib-resources==6.1.1
# via
# jsonschema
# jsonschema-specifications
jsonschema==4.20.0
jsonschema==4.21.1
# via semgrep
jsonschema-specifications==2023.11.2
jsonschema-specifications==2023.12.1
# via jsonschema
markdown-it-py==3.0.0
# via rich
@@ -60,7 +60,7 @@ pkgutil-resolve-name==1.3.10
# via jsonschema
pygments==2.17.2
# via rich
referencing==0.32.0
referencing==0.32.1
# via
# jsonschema
# jsonschema-specifications
@@ -68,7 +68,7 @@ requests==2.31.0
# via semgrep
rich==13.7.0
# via semgrep
rpds-py==0.13.2
rpds-py==0.17.1
# via
# jsonschema
# referencing

View File

@@ -31,10 +31,8 @@ aniso8601==9.0.1
# edx-tincan-py35
annotated-types==0.6.0
# via pydantic
anyio==3.7.1
# via
# fastapi
# starlette
anyio==4.2.0
# via starlette
appdirs==1.4.4
# via
# -r requirements/edx/base.txt
@@ -58,7 +56,7 @@ async-timeout==4.0.3
# -r requirements/edx/base.txt
# aiohttp
# redis
attrs==23.1.0
attrs==23.2.0
# via
# -r requirements/edx/base.txt
# aiohttp
@@ -86,7 +84,7 @@ backports-zoneinfo[tzdata]==0.2.1
# django
# icalendar
# kombu
beautifulsoup4==4.12.2
beautifulsoup4==4.12.3
# via
# -r requirements/edx/base.txt
# -r requirements/edx/testing.in
@@ -107,13 +105,13 @@ bleach[css]==6.1.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/base.txt
boto3==1.33.12
boto3==1.34.28
# via
# -r requirements/edx/base.txt
# django-ses
# fs-s3fs
# ora2
botocore==1.33.12
botocore==1.34.28
# via
# -r requirements/edx/base.txt
# boto3
@@ -211,7 +209,7 @@ coreschema==0.0.4
# -r requirements/edx/base.txt
# coreapi
# drf-yasg
coverage[toml]==7.3.2
coverage[toml]==7.4.0
# via
# -r requirements/edx/coverage.txt
# pytest-cov
@@ -238,7 +236,7 @@ cssutils==2.9.0
# via
# -r requirements/edx/base.txt
# pynliner
ddt==1.7.0
ddt==1.7.1
# via -r requirements/edx/testing.in
defusedxml==0.7.1
# via
@@ -251,7 +249,7 @@ deprecated==1.2.14
# via
# -r requirements/edx/base.txt
# jwcrypto
diff-cover==8.0.1
diff-cover==8.0.3
# via -r requirements/edx/coverage.txt
dill==0.3.7
# via pylint
@@ -327,6 +325,7 @@ django==4.2.9
# openedx-learning
# ora2
# super-csv
# xblock-google-drive
# xss-utils
django-appconf==1.0.6
# via
@@ -377,12 +376,12 @@ django-filter==23.5
# edx-enterprise
# lti-consumer-xblock
# openedx-blockstore
django-ipware==6.0.2
django-ipware==6.0.3
# via
# -r requirements/edx/base.txt
# edx-enterprise
# edx-proctoring
django-js-asset==2.1.0
django-js-asset==2.2.0
# via
# -r requirements/edx/base.txt
# django-mptt
@@ -425,7 +424,7 @@ django-object-actions==4.2.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
django-pipeline==2.1.0
django-pipeline==3.0.0
# via -r requirements/edx/base.txt
django-ratelimit==4.1.0
# via -r requirements/edx/base.txt
@@ -503,11 +502,11 @@ drf-jwt==1.19.2
# via
# -r requirements/edx/base.txt
# edx-drf-extensions
drf-nested-routers==0.93.4
drf-nested-routers==0.93.5
# via
# -r requirements/edx/base.txt
# openedx-blockstore
drf-spectacular==0.27.0
drf-spectacular==0.27.1
# via -r requirements/edx/base.txt
drf-yasg==1.21.5
# via
@@ -526,7 +525,7 @@ edx-auth-backends==4.2.0
# via
# -r requirements/edx/base.txt
# openedx-blockstore
edx-braze-client==0.1.8
edx-braze-client==0.2.1
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -554,7 +553,7 @@ edx-django-release-util==1.3.0
# openedx-blockstore
edx-django-sites-extensions==4.0.2
# via -r requirements/edx/base.txt
edx-django-utils==5.9.0
edx-django-utils==5.10.1
# via
# -r requirements/edx/base.txt
# django-config-models
@@ -570,7 +569,7 @@ edx-django-utils==5.9.0
# openedx-blockstore
# ora2
# super-csv
edx-drf-extensions==9.1.2
edx-drf-extensions==10.1.0
# via
# -r requirements/edx/base.txt
# edx-completion
@@ -582,11 +581,11 @@ edx-drf-extensions==9.1.2
# edx-when
# edxval
# openedx-learning
edx-enterprise==4.10.9
edx-enterprise==4.10.11
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
edx-event-bus-kafka==5.5.0
edx-event-bus-kafka==5.6.0
# via -r requirements/edx/base.txt
edx-event-bus-redis==0.3.2
# via -r requirements/edx/base.txt
@@ -634,7 +633,7 @@ edx-rest-api-client==5.6.1
# edx-proctoring
edx-search==3.8.2
# via -r requirements/edx/base.txt
edx-sga==0.23.0
edx-sga==0.23.1
# via -r requirements/edx/base.txt
edx-submissions==3.6.0
# via
@@ -688,11 +687,11 @@ execnet==2.0.2
# via pytest-xdist
factory-boy==3.3.0
# via -r requirements/edx/testing.in
faker==20.1.0
faker==22.5.1
# via factory-boy
fastapi==0.105.0
fastapi==0.109.0
# via pact-python
fastavro==1.9.1
fastavro==1.9.3
# via
# -r requirements/edx/base.txt
# openedx-events
@@ -702,9 +701,9 @@ filelock==3.13.1
# snowflake-connector-python
# tox
# virtualenv
freezegun==1.3.1
freezegun==1.4.0
# via -r requirements/edx/testing.in
frozenlist==1.4.0
frozenlist==1.4.1
# via
# -r requirements/edx/base.txt
# aiohttp
@@ -727,7 +726,7 @@ geoip2==4.8.0
# via -r requirements/edx/base.txt
glob2==0.7
# via -r requirements/edx/base.txt
grimp==3.1
grimp==3.2
# via import-linter
gunicorn==21.2.0
# via -r requirements/edx/base.txt
@@ -751,9 +750,9 @@ idna==3.6
# requests
# snowflake-connector-python
# yarl
import-linter==1.12.1
import-linter==2.0
# via -r requirements/edx/testing.in
importlib-metadata==7.0.0
importlib-metadata==7.0.1
# via
# -r requirements/edx/base.txt
# markdown
@@ -781,7 +780,7 @@ isodate==0.6.1
# via
# -r requirements/edx/base.txt
# python3-saml
isort==5.13.1
isort==5.13.2
# via
# -r requirements/edx/testing.in
# pylint
@@ -789,7 +788,7 @@ itypes==1.2.0
# via
# -r requirements/edx/base.txt
# coreapi
jinja2==3.1.2
jinja2==3.1.3
# via
# -r requirements/edx/base.txt
# -r requirements/edx/coverage.txt
@@ -818,21 +817,21 @@ jsonfield==3.1.0
# edx-submissions
# lti-consumer-xblock
# ora2
jsonschema==4.20.0
jsonschema==4.21.1
# via
# -r requirements/edx/base.txt
# drf-spectacular
# optimizely-sdk
jsonschema-specifications==2023.11.2
jsonschema-specifications==2023.12.1
# via
# -r requirements/edx/base.txt
# jsonschema
jwcrypto==1.5.0
jwcrypto==1.5.1
# via
# -r requirements/edx/base.txt
# django-oauth-toolkit
# pylti1p3
kombu==5.3.4
kombu==5.3.5
# via
# -r requirements/edx/base.txt
# celery
@@ -845,7 +844,7 @@ lazy==1.6
# lti-consumer-xblock
# ora2
# xblock
lazy-object-proxy==1.9.0
lazy-object-proxy==1.10.0
# via astroid
libsass==0.10.0
# via
@@ -855,10 +854,11 @@ loremipsum==1.0.5
# via
# -r requirements/edx/base.txt
# ora2
lti-consumer-xblock==9.8.1
lti-consumer-xblock==9.8.3
# via -r requirements/edx/base.txt
lxml==4.9.3
lxml==4.9.4
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# edx-i18n-tools
# edxval
@@ -878,7 +878,6 @@ mako==1.3.0
# acid-xblock
# lti-consumer-xblock
# xblock
# xblock-google-drive
# xblock-utils
markdown==3.3.7
# via
@@ -891,7 +890,7 @@ markey==0.8
# via
# -r requirements/edx/base.txt
# enmerkar-underscore
markupsafe==2.1.3
markupsafe==2.1.4
# via
# -r requirements/edx/base.txt
# -r requirements/edx/coverage.txt
@@ -900,7 +899,7 @@ markupsafe==2.1.3
# mako
# openedx-calc
# xblock
maxminddb==2.5.1
maxminddb==2.5.2
# via
# -r requirements/edx/base.txt
# geoip2
@@ -924,11 +923,11 @@ multidict==6.0.4
# -r requirements/edx/base.txt
# aiohttp
# yarl
mysqlclient==2.2.0
mysqlclient==2.2.1
# via
# -r requirements/edx/base.txt
# openedx-blockstore
newrelic==9.3.0
newrelic==9.6.0
# via
# -r requirements/edx/base.txt
# edx-django-utils
@@ -959,13 +958,13 @@ openai==0.28.1
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# edx-enterprise
openedx-atlas==0.5.0
openedx-atlas==0.6.0
# via -r requirements/edx/base.txt
openedx-blockstore==1.4.0
# via -r requirements/edx/base.txt
openedx-calc==3.0.1
# via -r requirements/edx/base.txt
openedx-django-pyfs==3.4.0
openedx-django-pyfs==3.4.1
# via
# -r requirements/edx/base.txt
# lti-consumer-xblock
@@ -974,7 +973,7 @@ 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==9.2.0
openedx-events==9.3.0
# via
# -r requirements/edx/base.txt
# edx-event-bus-kafka
@@ -983,15 +982,17 @@ openedx-filters==1.6.0
# via
# -r requirements/edx/base.txt
# lti-consumer-xblock
openedx-learning==0.4.2
openedx-learning==0.4.4
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
openedx-mongodbproxy==0.2.0
# via -r requirements/edx/base.txt
optimizely-sdk==4.1.1
# via -r requirements/edx/base.txt
ora2==6.0.29
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
ora2==6.0.30
# via -r requirements/edx/base.txt
packaging==23.2
# via
@@ -1032,7 +1033,7 @@ pgpy==0.6.0
# edx-enterprise
piexif==1.1.3
# via -r requirements/edx/base.txt
pillow==10.1.0
pillow==10.2.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -1049,7 +1050,7 @@ platformdirs==3.11.0
# snowflake-connector-python
# tox
# virtualenv
pluggy==1.3.0
pluggy==1.4.0
# via
# -r requirements/edx/coverage.txt
# diff-cover
@@ -1060,11 +1061,11 @@ polib==1.2.0
# -r requirements/edx/base.txt
# -r requirements/edx/testing.in
# edx-i18n-tools
prompt-toolkit==3.0.42
prompt-toolkit==3.0.43
# via
# -r requirements/edx/base.txt
# click-repl
psutil==5.9.6
psutil==5.9.8
# via
# -r requirements/edx/base.txt
# edx-django-utils
@@ -1090,15 +1091,15 @@ pycparser==2.21
# via
# -r requirements/edx/base.txt
# cffi
pycryptodomex==3.19.0
pycryptodomex==3.20.0
# via
# -r requirements/edx/base.txt
# edx-proctoring
# lti-consumer-xblock
# pyjwkest
pydantic==2.5.2
pydantic==2.5.3
# via fastapi
pydantic-core==2.14.5
pydantic-core==2.14.6
# via pydantic
pygments==2.17.2
# via
@@ -1186,7 +1187,7 @@ pysrt==1.1.2
# via
# -r requirements/edx/base.txt
# edxval
pytest==7.4.3
pytest==7.4.4
# via
# -r requirements/edx/testing.in
# pylint-pytest
@@ -1228,13 +1229,13 @@ python-dateutil==2.8.2
# olxcleaner
# ora2
# xblock
python-ipware==2.0.0
python-ipware==2.0.1
# via
# -r requirements/edx/base.txt
# django-ipware
python-memcached==1.59
python-memcached==1.62
# via -r requirements/edx/base.txt
python-slugify==8.0.1
python-slugify==8.0.2
# via
# -r requirements/edx/base.txt
# code-annotations
@@ -1279,20 +1280,20 @@ pyyaml==6.0.1
# edx-django-release-util
# edx-i18n-tools
# xblock
random2==1.0.1
random2==1.0.2
# via -r requirements/edx/base.txt
recommender-xblock==2.0.1
recommender-xblock==2.1.1
# via -r requirements/edx/base.txt
redis==5.0.1
# via
# -r requirements/edx/base.txt
# walrus
referencing==0.32.0
referencing==0.32.1
# via
# -r requirements/edx/base.txt
# jsonschema
# jsonschema-specifications
regex==2023.10.3
regex==2023.12.25
# via
# -r requirements/edx/base.txt
# nltk
@@ -1320,11 +1321,12 @@ requests==2.31.0
# slumber
# snowflake-connector-python
# social-auth-core
# xblock-google-drive
requests-oauthlib==1.3.1
# via
# -r requirements/edx/base.txt
# social-auth-core
rpds-py==0.13.2
rpds-py==0.17.1
# via
# -r requirements/edx/base.txt
# jsonschema
@@ -1343,7 +1345,7 @@ rules==3.3
# edx-enterprise
# edx-proctoring
# openedx-learning
s3transfer==0.8.2
s3transfer==0.10.0
# via
# -r requirements/edx/base.txt
# boto3
@@ -1402,7 +1404,6 @@ six==1.16.0
# py2neo
# pyjwkest
# python-dateutil
# python-memcached
slumber==0.7.1
# via
# -r requirements/edx/base.txt
@@ -1411,7 +1412,7 @@ slumber==0.7.1
# edx-rest-api-client
sniffio==1.3.0
# via anyio
snowflake-connector-python==3.6.0
snowflake-connector-python==3.7.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -1445,7 +1446,7 @@ sqlparse==0.4.4
# openedx-blockstore
staff-graded-xblock==2.2.0
# via -r requirements/edx/base.txt
starlette==0.27.0
starlette==0.35.1
# via fastapi
stevedore==5.1.0
# via
@@ -1500,6 +1501,7 @@ typing-extensions==4.9.0
# via
# -r requirements/edx/base.txt
# annotated-types
# anyio
# asgiref
# astroid
# django-countries
@@ -1517,7 +1519,7 @@ typing-extensions==4.9.0
# snowflake-connector-python
# starlette
# uvicorn
tzdata==2023.3
tzdata==2023.4
# via
# -r requirements/edx/base.txt
# backports-zoneinfo
@@ -1545,7 +1547,7 @@ urllib3==1.26.18
# snowflake-connector-python
user-util==1.0.0
# via -r requirements/edx/base.txt
uvicorn==0.24.0.post1
uvicorn==0.27.0
# via pact-python
vine==5.1.0
# via
@@ -1565,7 +1567,7 @@ walrus==0.9.3
# edx-event-bus-redis
watchdog==3.0.0
# via -r requirements/edx/base.txt
wcwidth==0.2.12
wcwidth==0.2.13
# via
# -r requirements/edx/base.txt
# prompt-toolkit
@@ -1592,7 +1594,7 @@ wrapt==1.16.0
# -r requirements/edx/base.txt
# astroid
# deprecated
xblock[django]==1.9.0
xblock[django]==1.10.0
# via
# -r requirements/edx/base.txt
# acid-xblock
@@ -1609,9 +1611,9 @@ xblock[django]==1.9.0
# xblock-google-drive
# xblock-poll
# xblock-utils
xblock-drag-and-drop-v2==3.3.0
xblock-drag-and-drop-v2==3.4.0
# via -r requirements/edx/base.txt
xblock-google-drive==0.5.0
xblock-google-drive==0.6.1
# via -r requirements/edx/base.txt
xblock-poll==1.13.0
# via -r requirements/edx/base.txt
@@ -1619,7 +1621,6 @@ xblock-utils==4.0.0
# via
# -r requirements/edx/base.txt
# edx-sga
# xblock-google-drive
xmlsec==1.3.13
# via
# -r requirements/edx/base.txt

View File

@@ -10,7 +10,7 @@ click==8.1.6
# via
# -c requirements/constraints.txt
# pip-tools
importlib-metadata==7.0.0
importlib-metadata==7.0.1
# via build
packaging==23.2
# via build

View File

@@ -8,7 +8,7 @@ wheel==0.42.0
# via -r requirements/pip.in
# The following packages are considered to be unsafe in a requirements file:
pip==23.3.1
pip==23.3.2
# via -r requirements/pip.in
setuptools==69.0.2
setuptools==69.0.3
# via -r requirements/pip.in

View File

@@ -84,6 +84,7 @@ module.exports = Merge.smart({
'js/factories/xblock_validation': './cms/static/js/factories/xblock_validation.js',
'js/factories/edit_tabs': './cms/static/js/factories/edit_tabs.js',
'js/sock': './cms/static/js/sock.js',
'js/factories/tag_count': './cms/static/js/factories/tag_count.js',
// LMS
SingleSupportForm: './lms/static/support/jsx/single_support_form.jsx',