Merge branch 'master' into iamsobanjaved/django-42-lts
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
13
cms/static/js/factories/tag_count.js
Normal file
13
cms/static/js/factories/tag_count.js
Normal 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};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
13
cms/static/js/models/tag_count.js
Normal file
13
cms/static/js/models/tag_count.js
Normal 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;
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
|
||||
54
cms/static/js/views/tag_count.js
Normal file
54
cms/static/js/views/tag_count.js
Normal 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;
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) { %>
|
||||
|
||||
7
cms/templates/js/tag-count.underscore
Normal file
7
cms/templates/js/tag-count.underscore
Normal 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>
|
||||
<% } %>
|
||||
@@ -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">
|
||||
|
||||
@@ -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}]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
]
|
||||
@@ -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()
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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__
|
||||
)
|
||||
|
||||
@@ -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 #################
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, "")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user