diff --git a/openedx/core/djangoapps/notifications/tests/test_views.py b/openedx/core/djangoapps/notifications/tests/test_views.py
index d2968749efab..e40e52078989 100644
--- a/openedx/core/djangoapps/notifications/tests/test_views.py
+++ b/openedx/core/djangoapps/notifications/tests/test_views.py
@@ -28,9 +28,13 @@
)
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS
from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
-from openedx.core.djangoapps.notifications.models import CourseNotificationPreference, Notification
+from openedx.core.djangoapps.notifications.models import (
+ CourseNotificationPreference,
+ Notification,
+ get_course_notification_preference_config_version
+)
from openedx.core.djangoapps.notifications.serializers import NotificationCourseEnrollmentSerializer
-from openedx.core.djangoapps.notifications.email.utils import get_unsubscribe_link
+from openedx.core.djangoapps.notifications.email.utils import encrypt_object, encrypt_string
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@@ -910,7 +914,13 @@ def test_if_preference_is_updated(self, request_type):
"""
Tests if preference is updated when url is hit
"""
- url = get_unsubscribe_link(self.user.username, {'channel': 'email', 'value': False})
+ user_hash = encrypt_string(self.user.username)
+ patch_hash = encrypt_object({'channel': 'email', 'value': False})
+ url_params = {
+ "username": user_hash,
+ "patch": patch_hash
+ }
+ url = reverse("preference_update_from_encrypted_username_view", kwargs=url_params)
func = getattr(self.client, request_type)
response = func(url)
assert response.status_code == status.HTTP_200_OK
@@ -921,6 +931,24 @@ def test_if_preference_is_updated(self, request_type):
assert type_prefs['email'] is False
assert type_prefs['email_cadence'] == EmailCadence.NEVER
+ def test_if_config_version_is_updated(self):
+ """
+ Tests if preference version is updated before applying patch data
+ """
+ preference = CourseNotificationPreference.objects.get(user=self.user, course_id=self.course.id)
+ preference.config_version -= 1
+ preference.save()
+ user_hash = encrypt_string(self.user.username)
+ patch_hash = encrypt_object({'channel': 'email', 'value': False})
+ url_params = {
+ "username": user_hash,
+ "patch": patch_hash
+ }
+ url = reverse("preference_update_from_encrypted_username_view", kwargs=url_params)
+ self.client.get(url)
+ preference = CourseNotificationPreference.objects.get(user=self.user, course_id=self.course.id)
+ assert preference.config_version == get_course_notification_preference_config_version()
+
def remove_notifications_with_visibility_settings(expected_response):
"""
diff --git a/openedx/core/djangoapps/notifications/views.py b/openedx/core/djangoapps/notifications/views.py
index ee5e282d905e..fdc91c12a9e0 100644
--- a/openedx/core/djangoapps/notifications/views.py
+++ b/openedx/core/djangoapps/notifications/views.py
@@ -5,7 +5,7 @@
from django.conf import settings
from django.db.models import Count
-from django.shortcuts import get_object_or_404, render
+from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
from opaque_keys.edx.keys import CourseKey
from pytz import UTC
@@ -441,7 +441,4 @@ def preference_update_from_encrypted_username_view(request, username, patch):
username and patch must be string
"""
update_user_preferences_from_patch(username, patch)
- context = {
- "notification_preferences_url": f"{settings.ACCOUNT_MICROFRONTEND_URL}/notifications"
- }
- return render(request, "notifications/email_digest_preference_update.html", context=context)
+ return Response({"result": "success"}, status=status.HTTP_200_OK)
diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py
index 720b3ba96af7..103c5bf24f6c 100644
--- a/openedx/core/djangoapps/user_api/accounts/views.py
+++ b/openedx/core/djangoapps/user_api/accounts/views.py
@@ -21,7 +21,6 @@
from edx_ace import ace
from edx_ace.recipient import Recipient
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
-from openedx.core.lib.api.authentication import BearerAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from enterprise.models import EnterpriseCourseEnrollment, EnterpriseCustomerUser, PendingEnterpriseCustomerUser
from integrated_channels.degreed.models import DegreedLearnerDataTransmissionAudit
@@ -50,9 +49,10 @@
get_retired_email_by_email,
get_retired_username_by_username,
is_email_retired,
- is_username_retired
+ is_username_retired,
)
from common.djangoapps.student.models_api import confirm_name_change, do_name_change_request, get_pending_name_change
+from lms.djangoapps.certificates.api import clear_pii_from_certificate_records_for_user
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
from openedx.core.djangoapps.course_groups.models import UnregisteredLearnerCohortAssignments
@@ -64,9 +64,8 @@
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_names, set_has_profile_image
from openedx.core.djangoapps.user_api.accounts.utils import handle_retirement_cancellation
from openedx.core.djangoapps.user_authn.exceptions import AuthFailedError
-from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
+from openedx.core.lib.api.authentication import BearerAuthentication, BearerAuthenticationAllowInactiveUser
from openedx.core.lib.api.parsers import MergePatchParser
-from lms.djangoapps.certificates.api import clear_pii_from_certificate_records_for_user
from ..errors import AccountUpdateError, AccountValidationError, UserNotAuthorized, UserNotFound
from ..message_types import DeletionNotificationMessage
@@ -75,7 +74,7 @@
RetirementStateError,
UserOrgTag,
UserRetirementPartnerReportingStatus,
- UserRetirementStatus
+ UserRetirementStatus,
)
from .api import get_account_settings, update_account_settings
from .permissions import (
@@ -83,13 +82,13 @@
CanDeactivateUser,
CanGetAccountInfo,
CanReplaceUsername,
- CanRetireUser
+ CanRetireUser,
)
from .serializers import (
PendingNameChangeSerializer,
UserRetirementPartnerReportSerializer,
UserRetirementStatusSerializer,
- UserSearchEmailSerializer
+ UserSearchEmailSerializer,
)
from .signals import USER_RETIRE_LMS_CRITICAL, USER_RETIRE_LMS_MISC, USER_RETIRE_MAILINGS
from .utils import create_retirement_request_and_deactivate_account, username_suffix_generator
@@ -97,16 +96,16 @@
log = logging.getLogger(__name__)
USER_PROFILE_PII = {
- 'name': '',
- 'meta': '',
- 'location': '',
- 'year_of_birth': None,
- 'gender': None,
- 'mailing_address': None,
- 'city': None,
- 'country': None,
- 'bio': None,
- 'phone_number': None,
+ "name": "",
+ "meta": "",
+ "location": "",
+ "year_of_birth": None,
+ "gender": None,
+ "mailing_address": None,
+ "city": None,
+ "country": None,
+ "bio": None,
+ "phone_number": None,
}
@@ -118,12 +117,9 @@ def request_requires_username(function):
@wraps(function)
def wrapper(self, request): # pylint: disable=missing-docstring
- username = request.data.get('username', None)
+ username = request.data.get("username", None)
if not username:
- return Response(
- status=status.HTTP_404_NOT_FOUND,
- data={'message': 'The user was not specified.'}
- )
+ return Response(status=status.HTTP_404_NOT_FOUND, data={"message": "The user was not specified."})
return function(self, request)
return wrapper
@@ -131,177 +127,183 @@ def wrapper(self, request): # pylint: disable=missing-docstring
class AccountViewSet(ViewSet):
"""
- **Use Cases**
-
- Get or update a user's account information. Updates are supported
- only through merge patch.
-
- **Example Requests**
-
- GET /api/user/v1/me[?view=shared]
- GET /api/user/v1/accounts?usernames={username1,username2}[?view=shared]
- GET /api/user/v1/accounts?email={user_email}
- GET /api/user/v1/accounts/{username}/[?view=shared]
-
- PATCH /api/user/v1/accounts/{username}/{"key":"value"} "application/merge-patch+json"
-
- POST /api/user/v1/accounts/search_emails "application/json"
-
- **Notes for PATCH requests to /accounts endpoints**
- * Requested updates to social_links are automatically merged with
- previously set links. That is, any newly introduced platforms are
- add to the previous list. Updated links to pre-existing platforms
- replace their values in the previous list. Pre-existing platforms
- can be removed by setting the value of the social_link to an
- empty string ("").
-
- **Response Values for GET requests to the /me endpoint**
- If the user is not logged in, an HTTP 401 "Not Authorized" response
- is returned.
-
- Otherwise, an HTTP 200 "OK" response is returned. The response
- contains the following value:
-
- * username: The username associated with the account.
-
- **Response Values for GET requests to /accounts endpoints**
-
- If no user exists with the specified username, or email, an HTTP 404 "Not
- Found" response is returned.
-
- If the user makes the request for her own account, or makes a
- request for another account and has "is_staff" access, an HTTP 200
- "OK" response is returned. The response contains the following
- values.
-
- * id: numerical lms user id in db
- * activation_key: auto-genrated activation key when signed up via email
- * bio: null or textual representation of user biographical
- information ("about me").
- * country: An ISO 3166 country code or null.
- * date_joined: The date the account was created, in the string
- format provided by datetime. For example, "2014-08-26T17:52:11Z".
- * last_login: The latest date the user logged in, in the string datetime format.
- * email: Email address for the user. New email addresses must be confirmed
- via a confirmation email, so GET does not reflect the change until
- the address has been confirmed.
- * secondary_email: A secondary email address for the user. Unlike
- the email field, GET will reflect the latest update to this field
- even if changes have yet to be confirmed.
- * verified_name: Approved verified name of the learner present in name affirmation plugin
- * gender: One of the following values:
-
- * null
- * "f"
- * "m"
- * "o"
-
- * goals: The textual representation of the user's goals, or null.
- * is_active: Boolean representation of whether a user is active.
- * language: The user's preferred language, or null.
- * language_proficiencies: Array of language preferences. Each
- preference is a JSON object with the following keys:
-
- * "code": string ISO 639-1 language code e.g. "en".
-
- * level_of_education: One of the following values:
-
- * "p": PhD or Doctorate
- * "m": Master's or professional degree
- * "b": Bachelor's degree
- * "a": Associate's degree
- * "hs": Secondary/high school
- * "jhs": Junior secondary/junior high/middle school
- * "el": Elementary/primary school
- * "none": None
- * "o": Other
- * null: The user did not enter a value
-
- * mailing_address: The textual representation of the user's mailing
- address, or null.
- * name: The full name of the user.
- * profile_image: A JSON representation of a user's profile image
- information. This representation has the following keys.
-
- * "has_image": Boolean indicating whether the user has a profile
- image.
- * "image_url_*": Absolute URL to various sizes of a user's
- profile image, where '*' matches a representation of the
- corresponding image size, such as 'small', 'medium', 'large',
- and 'full'. These are configurable via PROFILE_IMAGE_SIZES_MAP.
-
- * requires_parental_consent: True if the user is a minor
- requiring parental consent.
- * social_links: Array of social links, sorted alphabetically by
- "platform". Each preference is a JSON object with the following keys:
-
- * "platform": A particular social platform, ex: 'facebook'
- * "social_link": The link to the user's profile on the particular platform
-
- * username: The username associated with the account.
- * year_of_birth: The year the user was born, as an integer, or null.
-
- * account_privacy: The user's setting for sharing her personal
- profile. Possible values are "all_users", "private", or "custom".
- If "custom", the user has selectively chosen a subset of shareable
- fields to make visible to others via the User Preferences API.
-
- * phone_number: The phone number for the user. String of numbers with
- an optional `+` sign at the start.
-
- * pending_name_change: If the user has an active name change request, returns the
- requested name.
-
- For all text fields, plain text instead of HTML is supported. The
- data is stored exactly as specified. Clients must HTML escape
- rendered values to avoid script injections.
-
- If a user who does not have "is_staff" access requests account
- information for a different user, only a subset of these fields is
- returned. The returned fields depend on the
- ACCOUNT_VISIBILITY_CONFIGURATION configuration setting and the
- visibility preference of the user for whom data is requested.
-
- Note that a user can view which account fields they have shared
- with other users by requesting their own username and providing
- the "view=shared" URL parameter.
-
- **Response Values for PATCH**
-
- Users can only modify their own account information. If the
- requesting user does not have the specified username and has staff
- access, the request returns an HTTP 403 "Forbidden" response. If
- the requesting user does not have staff access, the request
- returns an HTTP 404 "Not Found" response to avoid revealing the
- existence of the account.
-
- If no user exists with the specified username, an HTTP 404 "Not
- Found" response is returned.
-
- If "application/merge-patch+json" is not the specified content
- type, a 415 "Unsupported Media Type" response is returned.
-
- If validation errors prevent the update, this method returns a 400
- "Bad Request" response that includes a "field_errors" field that
- lists all error messages.
-
- If a failure at the time of the update prevents the update, a 400
- "Bad Request" error is returned. The JSON collection contains
- specific errors.
-
- If the update is successful, updated user account data is returned.
+ **Use Cases**
+
+ Get or update a user's account information. Updates are supported
+ only through merge patch.
+
+ **Example Requests**
+
+ GET /api/user/v1/me[?view=shared]
+ GET /api/user/v1/accounts?usernames={username1,username2}[?view=shared]
+ GET /api/user/v1/accounts?email={user_email}
+ GET /api/user/v1/accounts/{username}/[?view=shared]
+
+ PATCH /api/user/v1/accounts/{username}/{"key":"value"} "application/merge-patch+json"
+
+ POST /api/user/v1/accounts/search_emails "application/json"
+
+ **Notes for PATCH requests to /accounts endpoints**
+ * Requested updates to social_links are automatically merged with
+ previously set links. That is, any newly introduced platforms are
+ add to the previous list. Updated links to pre-existing platforms
+ replace their values in the previous list. Pre-existing platforms
+ can be removed by setting the value of the social_link to an
+ empty string ("").
+
+ **Response Values for GET requests to the /me endpoint**
+ If the user is not logged in, an HTTP 401 "Not Authorized" response
+ is returned.
+
+ Otherwise, an HTTP 200 "OK" response is returned. The response
+ contains the following value:
+
+ * username: The username associated with the account.
+
+ **Response Values for GET requests to /accounts endpoints**
+
+ If no user exists with the specified username, or email, an HTTP 404 "Not
+ Found" response is returned.
+
+ If the user makes the request for her own account, or makes a
+ request for another account and has "is_staff" access, an HTTP 200
+ "OK" response is returned. The response contains the following
+ values.
+
+ * id: numerical lms user id in db
+ * activation_key: auto-genrated activation key when signed up via email
+ * bio: null or textual representation of user biographical
+ information ("about me").
+ * country: An ISO 3166 country code or null.
+ * date_joined: The date the account was created, in the string
+ format provided by datetime. For example, "2014-08-26T17:52:11Z".
+ * last_login: The latest date the user logged in, in the string datetime format.
+ * email: Email address for the user. New email addresses must be confirmed
+ via a confirmation email, so GET does not reflect the change until
+ the address has been confirmed.
+ * secondary_email: A secondary email address for the user. Unlike
+ the email field, GET will reflect the latest update to this field
+ even if changes have yet to be confirmed.
+ * verified_name: Approved verified name of the learner present in name affirmation plugin
+ * gender: One of the following values:
+
+ * null
+ * "f"
+ * "m"
+ * "o"
+
+ * goals: The textual representation of the user's goals, or null.
+ * is_active: Boolean representation of whether a user is active.
+ * language: The user's preferred language, or null.
+ * language_proficiencies: Array of language preferences. Each
+ preference is a JSON object with the following keys:
+
+ * "code": string ISO 639-1 language code e.g. "en".
+
+ * level_of_education: One of the following values:
+
+ * "p": PhD or Doctorate
+ * "m": Master's or professional degree
+ * "b": Bachelor's degree
+ * "a": Associate's degree
+ * "hs": Secondary/high school
+ * "jhs": Junior secondary/junior high/middle school
+ * "el": Elementary/primary school
+ * "none": None
+ * "o": Other
+ * null: The user did not enter a value
+
+ * mailing_address: The textual representation of the user's mailing
+ address, or null.
+ * name: The full name of the user.
+ * profile_image: A JSON representation of a user's profile image
+ information. This representation has the following keys.
+
+ * "has_image": Boolean indicating whether the user has a profile
+ image.
+ * "image_url_*": Absolute URL to various sizes of a user's
+ profile image, where '*' matches a representation of the
+ corresponding image size, such as 'small', 'medium', 'large',
+ and 'full'. These are configurable via PROFILE_IMAGE_SIZES_MAP.
+
+ * requires_parental_consent: True if the user is a minor
+ requiring parental consent.
+ * social_links: Array of social links, sorted alphabetically by
+ "platform". Each preference is a JSON object with the following keys:
+
+ * "platform": A particular social platform, ex: 'facebook'
+ * "social_link": The link to the user's profile on the particular platform
+
+ * username: The username associated with the account.
+ * year_of_birth: The year the user was born, as an integer, or null.
+
+ * account_privacy: The user's setting for sharing her personal
+ profile. Possible values are "all_users", "private", or "custom".
+ If "custom", the user has selectively chosen a subset of shareable
+ fields to make visible to others via the User Preferences API.
+
+ * phone_number: The phone number for the user. String of numbers with
+ an optional `+` sign at the start.
+
+ * pending_name_change: If the user has an active name change request, returns the
+ requested name.
+
+ For all text fields, plain text instead of HTML is supported. The
+ data is stored exactly as specified. Clients must HTML escape
+ rendered values to avoid script injections.
+
+ If a user who does not have "is_staff" access requests account
+ information for a different user, only a subset of these fields is
+ returned. The returned fields depend on the
+ ACCOUNT_VISIBILITY_CONFIGURATION configuration setting and the
+ visibility preference of the user for whom data is requested.
+
+ Note that a user can view which account fields they have shared
+ with other users by requesting their own username and providing
+ the "view=shared" URL parameter.
+
+ **Response Values for PATCH**
+
+ Users can only modify their own account information. If the
+ requesting user does not have the specified username and has staff
+ access, the request returns an HTTP 403 "Forbidden" response. If
+ the requesting user does not have staff access, the request
+ returns an HTTP 404 "Not Found" response to avoid revealing the
+ existence of the account.
+
+ If no user exists with the specified username, an HTTP 404 "Not
+ Found" response is returned.
+
+ If "application/merge-patch+json" is not the specified content
+ type, a 415 "Unsupported Media Type" response is returned.
+
+ If validation errors prevent the update, this method returns a 400
+ "Bad Request" response that includes a "field_errors" field that
+ lists all error messages.
+
+ If a failure at the time of the update prevents the update, a 400
+ "Bad Request" error is returned. The JSON collection contains
+ specific errors.
+
+ If the update is successful, updated user account data is returned.
"""
+
authentication_classes = (
- JwtAuthentication, BearerAuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser
+ JwtAuthentication,
+ BearerAuthenticationAllowInactiveUser,
+ SessionAuthenticationAllowInactiveUser,
)
permission_classes = (permissions.IsAuthenticated, CanGetAccountInfo)
- parser_classes = (JSONParser, MergePatchParser,)
+ parser_classes = (
+ JSONParser,
+ MergePatchParser,
+ )
def get(self, request):
"""
GET /api/user/v1/me
"""
- return Response({'username': request.user.username})
+ return Response({"username": request.user.username})
def list(self, request):
"""
@@ -309,13 +311,13 @@ def list(self, request):
GET /api/user/v1/accounts?email={user_email} (Staff Only)
GET /api/user/v1/accounts?lms_user_id={lms_user_id} (Staff Only)
"""
- usernames = request.GET.get('username')
- user_email = request.GET.get('email')
- lms_user_id = request.GET.get('lms_user_id')
+ usernames = request.GET.get("username")
+ user_email = request.GET.get("email")
+ lms_user_id = request.GET.get("lms_user_id")
search_usernames = []
if usernames:
- search_usernames = usernames.strip(',').split(',')
+ search_usernames = usernames.strip(",").split(",")
elif user_email:
if is_email_retired(user_email):
can_cancel_retirement = True
@@ -325,22 +327,20 @@ def list(self, request):
retirement_status = UserRetirementStatus.objects.get(
created__gt=earliest_datetime,
created__lt=datetime.datetime.now(pytz.UTC),
- original_email=user_email
+ original_email=user_email,
)
retirement_id = retirement_status.id
except UserRetirementStatus.DoesNotExist:
can_cancel_retirement = False
context = {
- 'error_msg': accounts.RETIRED_EMAIL_MSG,
- 'can_cancel_retirement': can_cancel_retirement,
- 'retirement_id': retirement_id
+ "error_msg": accounts.RETIRED_EMAIL_MSG,
+ "can_cancel_retirement": can_cancel_retirement,
+ "retirement_id": retirement_id,
}
- return Response(
- context, status=status.HTTP_404_NOT_FOUND
- )
- user_email = user_email.strip('')
+ return Response(context, status=status.HTTP_404_NOT_FOUND)
+ user_email = user_email.strip("")
try:
user = User.objects.get(email=user_email)
except (UserNotFound, User.DoesNotExist):
@@ -355,9 +355,7 @@ def list(self, request):
return Response(status=status.HTTP_400_BAD_REQUEST)
search_usernames = [user.username]
try:
- account_settings = get_account_settings(
- request, search_usernames, view=request.query_params.get('view')
- )
+ account_settings = get_account_settings(request, search_usernames, view=request.query_params.get("view"))
except UserNotFound:
return Response(status=status.HTTP_404_NOT_FOUND)
@@ -386,23 +384,15 @@ def search_emails(self, request):
"""
if not request.user.is_staff:
return Response(
- {
- 'developer_message': 'not_found',
- 'user_message': 'Not Found'
- },
- status=status.HTTP_404_NOT_FOUND
+ {"developer_message": "not_found", "user_message": "Not Found"}, status=status.HTTP_404_NOT_FOUND
)
try:
- user_emails = request.data['emails']
+ user_emails = request.data["emails"]
except KeyError as error:
- error_message = f'{error} field is required'
+ error_message = f"{error} field is required"
return Response(
- {
- 'developer_message': error_message,
- 'user_message': error_message
- },
- status=status.HTTP_400_BAD_REQUEST
+ {"developer_message": error_message, "user_message": error_message}, status=status.HTTP_400_BAD_REQUEST
)
users = User.objects.filter(email__in=user_emails)
data = UserSearchEmailSerializer(users, many=True).data
@@ -413,8 +403,7 @@ def retrieve(self, request, username):
GET /api/user/v1/accounts/{username}/
"""
try:
- account_settings = get_account_settings(
- request, [username], view=request.query_params.get('view'))
+ account_settings = get_account_settings(request, [username], view=request.query_params.get("view"))
except UserNotFound:
return Response(status=status.HTTP_404_NOT_FOUND)
@@ -443,11 +432,8 @@ def partial_update(self, request, username):
return Response({"field_errors": err.field_errors}, status=status.HTTP_400_BAD_REQUEST)
except AccountUpdateError as err:
return Response(
- {
- "developer_message": err.developer_message,
- "user_message": err.user_message
- },
- status=status.HTTP_400_BAD_REQUEST
+ {"developer_message": err.developer_message, "user_message": err.user_message},
+ status=status.HTTP_400_BAD_REQUEST,
)
return Response(account_settings)
@@ -457,6 +443,7 @@ class NameChangeView(ViewSet):
"""
Viewset to manage profile name change requests.
"""
+
permission_classes = (permissions.IsAuthenticated,)
def create(self, request):
@@ -472,10 +459,10 @@ def create(self, request):
}
"""
user = request.user
- new_name = request.data.get('name', None)
- rationale = f'Name change requested through account API by {user.username}'
+ new_name = request.data.get("name", None)
+ rationale = f"Name change requested through account API by {user.username}"
- serializer = PendingNameChangeSerializer(data={'new_name': new_name})
+ serializer = PendingNameChangeSerializer(data={"new_name": new_name})
if serializer.is_valid():
pending_name_change = do_name_change_request(user, new_name, rationale)[0]
@@ -483,8 +470,8 @@ def create(self, request):
return Response(status=status.HTTP_201_CREATED)
else:
return Response(
- {'new_name': 'The profile name given was identical to the current name.'},
- status=status.HTTP_400_BAD_REQUEST
+ {"new_name": "The profile name given was identical to the current name."},
+ status=status.HTTP_400_BAD_REQUEST,
)
return Response(status=status.HTTP_400_BAD_REQUEST, data=serializer.errors)
@@ -514,6 +501,7 @@ class AccountDeactivationView(APIView):
Account deactivation viewset. Currently only supports POST requests.
Only admins can deactivate accounts.
"""
+
permission_classes = (permissions.IsAuthenticated, CanDeactivateUser)
def post(self, request, username):
@@ -559,6 +547,7 @@ class DeactivateLogoutView(APIView):
- Log the user out
- Create a row in the retirement table for that user
"""
+
# BearerAuthentication is added here to support account deletion
# from the mobile app until it moves to JWT Auth.
# See mobile roadmap issue https://github.com/openedx/edx-platform/issues/33307.
@@ -575,7 +564,7 @@ def post(self, request):
# Ensure the account deletion is not disable
enable_account_deletion = configuration_helpers.get_value(
- 'ENABLE_ACCOUNT_DELETION', settings.FEATURES.get('ENABLE_ACCOUNT_DELETION', False)
+ "ENABLE_ACCOUNT_DELETION", settings.FEATURES.get("ENABLE_ACCOUNT_DELETION", False)
)
if not enable_account_deletion:
@@ -595,11 +584,9 @@ def post(self, request):
# Send notification email to user
site = Site.objects.get_current()
notification_context = get_base_template_context(site)
- notification_context.update({'full_name': request.user.profile.name})
+ notification_context.update({"full_name": request.user.profile.name})
language_code = request.user.preferences.model.get_value(
- request.user,
- LANGUAGE_KEY,
- default=settings.LANGUAGE_CODE
+ request.user, LANGUAGE_KEY, default=settings.LANGUAGE_CODE
)
notification = DeletionNotificationMessage().personalize(
recipient=Recipient(lms_user_id=0, email_address=user_email),
@@ -608,22 +595,20 @@ def post(self, request):
)
ace.send(notification)
except Exception as exc:
- log.exception('Error sending out deletion notification email')
+ log.exception("Error sending out deletion notification email")
raise exc
# Log the user out.
logout(request)
return Response(status=status.HTTP_204_NO_CONTENT)
except KeyError:
- log.exception(f'Username not specified {request.user}')
- return Response('Username not specified.', status=status.HTTP_404_NOT_FOUND)
+ log.exception(f"Username not specified {request.user}")
+ return Response("Username not specified.", status=status.HTTP_404_NOT_FOUND)
except user_model.DoesNotExist:
log.exception(f'The user "{request.user.username}" does not exist.')
- return Response(
- f'The user "{request.user.username}" does not exist.', status=status.HTTP_404_NOT_FOUND
- )
+ return Response(f'The user "{request.user.username}" does not exist.', status=status.HTTP_404_NOT_FOUND)
except Exception as exc: # pylint: disable=broad-except
- log.exception(f'500 error deactivating account {exc}')
+ log.exception(f"500 error deactivating account {exc}")
return Response(str(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def _verify_user_password(self, request):
@@ -636,7 +621,7 @@ def _verify_user_password(self, request):
"""
try:
self._check_excessive_login_attempts(request.user)
- user = authenticate(username=request.user.username, password=request.POST['password'], request=request)
+ user = authenticate(username=request.user.username, password=request.POST["password"], request=request)
if user:
if LoginFailures.is_feature_enabled():
LoginFailures.clear_lockout_counter(user)
@@ -644,9 +629,7 @@ def _verify_user_password(self, request):
else:
self._handle_failed_authentication(request.user)
except AuthFailedError as err:
- log.exception(
- f"The user password to deactivate was incorrect. {request.user.username}"
- )
+ log.exception(f"The user password to deactivate was incorrect. {request.user.username}")
return Response(str(err), status=status.HTTP_403_FORBIDDEN)
except Exception as err: # pylint: disable=broad-except
return Response(f"Could not verify user password: {err}", status=status.HTTP_400_BAD_REQUEST)
@@ -657,8 +640,9 @@ def _check_excessive_login_attempts(self, user):
"""
if user and LoginFailures.is_feature_enabled():
if LoginFailures.is_user_locked_out(user):
- raise AuthFailedError(_('This account has been temporarily locked due '
- 'to excessive login failures. Try again later.'))
+ raise AuthFailedError(
+ _("This account has been temporarily locked due to excessive login failures. Try again later.")
+ )
def _handle_failed_authentication(self, user):
"""
@@ -667,7 +651,7 @@ def _handle_failed_authentication(self, user):
if user and LoginFailures.is_feature_enabled():
LoginFailures.increment_lockout_counter(user)
- raise AuthFailedError(_('Email or password is incorrect.'))
+ raise AuthFailedError(_("Email or password is incorrect."))
def _set_unusable_password(user):
@@ -684,15 +668,19 @@ class AccountRetirementPartnerReportView(ViewSet):
Provides API endpoints for managing partner reporting of retired
users.
"""
- DELETION_COMPLETED_KEY = 'deletion_completed'
- ORGS_CONFIG_KEY = 'orgs_config'
- ORGS_CONFIG_ORG_KEY = 'org'
- ORGS_CONFIG_FIELD_HEADINGS_KEY = 'field_headings'
- ORIGINAL_EMAIL_KEY = 'original_email'
- ORIGINAL_NAME_KEY = 'original_name'
- STUDENT_ID_KEY = 'student_id'
-
- permission_classes = (permissions.IsAuthenticated, CanRetireUser,)
+
+ DELETION_COMPLETED_KEY = "deletion_completed"
+ ORGS_CONFIG_KEY = "orgs_config"
+ ORGS_CONFIG_ORG_KEY = "org"
+ ORGS_CONFIG_FIELD_HEADINGS_KEY = "field_headings"
+ ORIGINAL_EMAIL_KEY = "original_email"
+ ORIGINAL_NAME_KEY = "original_name"
+ STUDENT_ID_KEY = "student_id"
+
+ permission_classes = (
+ permissions.IsAuthenticated,
+ CanRetireUser,
+ )
parser_classes = (JSONParser,)
serializer_class = UserRetirementStatusSerializer
@@ -706,7 +694,7 @@ def _get_orgs_for_user(user):
org = enrollment.course_id.org
# Org can conceivably be blank or this bogus default value
- if org and org != 'outdated_entry':
+ if org and org != "outdated_entry":
orgs.add(org)
return orgs
@@ -718,9 +706,9 @@ def retirement_partner_report(self, request): # pylint: disable=unused-argument
that are not already being processed and updates their status
to indicate they are currently being processed.
"""
- retirement_statuses = UserRetirementPartnerReportingStatus.objects.filter(
- is_being_processed=False
- ).order_by('id')
+ retirement_statuses = UserRetirementPartnerReportingStatus.objects.filter(is_being_processed=False).order_by(
+ "id"
+ )
retirements = []
for retirement_status in retirement_statuses:
@@ -737,12 +725,12 @@ def _get_retirement_for_partner_report(self, retirement_status):
Get the retirement for this retirement_status. The retirement info will be included in the partner report.
"""
retirement = {
- 'user_id': retirement_status.user.pk,
- 'original_username': retirement_status.original_username,
+ "user_id": retirement_status.user.pk,
+ "original_username": retirement_status.original_username,
AccountRetirementPartnerReportView.ORIGINAL_EMAIL_KEY: retirement_status.original_email,
AccountRetirementPartnerReportView.ORIGINAL_NAME_KEY: retirement_status.original_name,
- 'orgs': self._get_orgs_for_user(retirement_status.user),
- 'created': retirement_status.created,
+ "orgs": self._get_orgs_for_user(retirement_status.user),
+ "created": retirement_status.created,
}
return retirement
@@ -761,7 +749,7 @@ def retirement_partner_status_create(self, request):
Creates a UserRetirementPartnerReportingStatus object for the given user
as part of the retirement pipeline.
"""
- username = request.data['username']
+ username = request.data["username"]
try:
retirement = UserRetirementStatus.get_retirement_for_retirement_action(username)
@@ -771,10 +759,10 @@ def retirement_partner_status_create(self, request):
UserRetirementPartnerReportingStatus.objects.get_or_create(
user=retirement.user,
defaults={
- 'original_username': retirement.original_username,
- 'original_email': retirement.original_email,
- 'original_name': retirement.original_name
- }
+ "original_username": retirement.original_username,
+ "original_email": retirement.original_email,
+ "original_name": retirement.original_name,
+ },
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -790,14 +778,13 @@ def retirement_partner_cleanup(self, request):
Deletes UserRetirementPartnerReportingStatus objects for a list of users
that have been reported on.
"""
- usernames = [u['original_username'] for u in request.data]
+ usernames = [u["original_username"] for u in request.data]
if not usernames:
- return Response('No original_usernames given.', status=status.HTTP_400_BAD_REQUEST)
+ return Response("No original_usernames given.", status=status.HTTP_400_BAD_REQUEST)
retirement_statuses = UserRetirementPartnerReportingStatus.objects.filter(
- is_being_processed=True,
- original_username__in=usernames
+ is_being_processed=True, original_username__in=usernames
)
# Need to de-dupe usernames that differ only by case to find the exact right match
@@ -809,15 +796,15 @@ def retirement_partner_cleanup(self, request):
# to disambiguate them in Python, which will respect case in the comparison.
if len(usernames) != len(retirement_statuses_clean):
return Response(
- '{} original_usernames given, {} found!\n'
- 'Given usernames:\n{}\n'
- 'Found UserRetirementReportingStatuses:\n{}'.format(
+ "{} original_usernames given, {} found!\n"
+ "Given usernames:\n{}\n"
+ "Found UserRetirementReportingStatuses:\n{}".format(
len(usernames),
len(retirement_statuses_clean),
usernames,
- ', '.join([rs.original_username for rs in retirement_statuses_clean])
+ ", ".join([rs.original_username for rs in retirement_statuses_clean]),
),
- status=status.HTTP_400_BAD_REQUEST
+ status=status.HTTP_400_BAD_REQUEST,
)
retirement_statuses.delete()
@@ -829,7 +816,11 @@ class CancelAccountRetirementStatusView(ViewSet):
"""
Provides API endpoints for canceling retirement process for a user's account.
"""
- permission_classes = (permissions.IsAuthenticated, CanCancelUserRetirement,)
+
+ permission_classes = (
+ permissions.IsAuthenticated,
+ CanCancelUserRetirement,
+ )
def cancel_retirement(self, request):
"""
@@ -839,26 +830,23 @@ def cancel_retirement(self, request):
This also handles the top level error handling, and permissions.
"""
try:
- retirement_id = request.data['retirement_id']
+ retirement_id = request.data["retirement_id"]
except KeyError:
- return Response(
- status=status.HTTP_400_BAD_REQUEST,
- data={'message': 'retirement_id must be specified.'}
- )
+ return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": "retirement_id must be specified."})
try:
retirement = UserRetirementStatus.objects.get(id=retirement_id)
except UserRetirementStatus.DoesNotExist:
- return Response(data={"message": 'Retirement does not exist!'}, status=status.HTTP_400_BAD_REQUEST)
+ return Response(data={"message": "Retirement does not exist!"}, status=status.HTTP_400_BAD_REQUEST)
- if retirement.current_state.state_name != 'PENDING':
+ if retirement.current_state.state_name != "PENDING":
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": f"Retirement requests can only be cancelled for users in the PENDING state. Current "
- f"request state for '{retirement.original_username}': "
- f"{retirement.current_state.state_name}"
- }
+ f"request state for '{retirement.original_username}': "
+ f"{retirement.current_state.state_name}"
+ },
)
handle_retirement_cancellation(retirement)
@@ -870,7 +858,11 @@ class AccountRetirementStatusView(ViewSet):
"""
Provides API endpoints for managing the user retirement process.
"""
- permission_classes = (permissions.IsAuthenticated, CanRetireUser,)
+
+ permission_classes = (
+ permissions.IsAuthenticated,
+ CanRetireUser,
+ )
parser_classes = (JSONParser,)
serializer_class = UserRetirementStatusSerializer
@@ -883,37 +875,35 @@ def retirement_queue(self, request):
created in the retirement queue at least `cool_off_days` ago.
"""
try:
- cool_off_days = int(request.GET['cool_off_days'])
+ cool_off_days = int(request.GET["cool_off_days"])
if cool_off_days < 0:
- raise RetirementStateError('Invalid argument for cool_off_days, must be greater than 0.')
+ raise RetirementStateError("Invalid argument for cool_off_days, must be greater than 0.")
- states = request.GET.getlist('states')
+ states = request.GET.getlist("states")
if not states:
raise RetirementStateError('Param "states" required with at least one state.')
state_objs = RetirementState.objects.filter(state_name__in=states)
if state_objs.count() != len(states):
found = [s.state_name for s in state_objs]
- raise RetirementStateError(f'Unknown state. Requested: {states} Found: {found}')
+ raise RetirementStateError(f"Unknown state. Requested: {states} Found: {found}")
- limit = request.GET.get('limit')
+ limit = request.GET.get("limit")
if limit:
try:
limit_count = int(limit)
except ValueError:
return Response(
- f'Limit could not be parsed: {limit}, please ensure this is an integer',
- status=status.HTTP_400_BAD_REQUEST
+ f"Limit could not be parsed: {limit}, please ensure this is an integer",
+ status=status.HTTP_400_BAD_REQUEST,
)
earliest_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=cool_off_days)
- retirements = UserRetirementStatus.objects.select_related(
- 'user', 'current_state', 'last_state'
- ).filter(
- current_state__in=state_objs, created__lt=earliest_datetime
- ).order_by(
- 'id'
+ retirements = (
+ UserRetirementStatus.objects.select_related("user", "current_state", "last_state")
+ .filter(current_state__in=state_objs, created__lt=earliest_datetime)
+ .order_by("id")
)
if limit:
retirements = retirements[:limit_count]
@@ -921,10 +911,9 @@ def retirement_queue(self, request):
return Response(serializer.data)
# This should only occur on the int() conversion of cool_off_days at this point
except ValueError:
- return Response('Invalid cool_off_days, should be integer.', status=status.HTTP_400_BAD_REQUEST)
+ return Response("Invalid cool_off_days, should be integer.", status=status.HTTP_400_BAD_REQUEST)
except KeyError as exc:
- return Response(f'Missing required parameter: {str(exc)}',
- status=status.HTTP_400_BAD_REQUEST)
+ return Response(f"Missing required parameter: {str(exc)}", status=status.HTTP_400_BAD_REQUEST)
except RetirementStateError as exc:
return Response(str(exc), status=status.HTTP_400_BAD_REQUEST)
@@ -939,36 +928,33 @@ def retirements_by_status_and_date(self, request):
so to get one day you would set both dates to that day.
"""
try:
- start_date = datetime.datetime.strptime(request.GET['start_date'], '%Y-%m-%d').replace(tzinfo=pytz.UTC)
- end_date = datetime.datetime.strptime(request.GET['end_date'], '%Y-%m-%d').replace(tzinfo=pytz.UTC)
+ start_date = datetime.datetime.strptime(request.GET["start_date"], "%Y-%m-%d").replace(tzinfo=pytz.UTC)
+ end_date = datetime.datetime.strptime(request.GET["end_date"], "%Y-%m-%d").replace(tzinfo=pytz.UTC)
now = datetime.datetime.now(pytz.UTC)
if start_date > now or end_date > now or start_date > end_date:
- raise RetirementStateError('Dates must be today or earlier, and start must be earlier than end.')
+ raise RetirementStateError("Dates must be today or earlier, and start must be earlier than end.")
# Add a day to make sure we get all the way to 23:59:59.999, this is compared "lt" in the query
# not "lte".
end_date += datetime.timedelta(days=1)
- state = request.GET['state']
+ state = request.GET["state"]
state_obj = RetirementState.objects.get(state_name=state)
- retirements = UserRetirementStatus.objects.select_related(
- 'user', 'current_state', 'last_state', 'user__profile'
- ).filter(
- current_state=state_obj, created__lt=end_date, created__gte=start_date
- ).order_by(
- 'id'
+ retirements = (
+ UserRetirementStatus.objects.select_related("user", "current_state", "last_state", "user__profile")
+ .filter(current_state=state_obj, created__lt=end_date, created__gte=start_date)
+ .order_by("id")
)
serializer = UserRetirementStatusSerializer(retirements, many=True)
return Response(serializer.data)
# This should only occur on the datetime conversion of the start / end dates.
except ValueError as exc:
- return Response(f'Invalid start or end date: {str(exc)}', status=status.HTTP_400_BAD_REQUEST)
+ return Response(f"Invalid start or end date: {str(exc)}", status=status.HTTP_400_BAD_REQUEST)
except KeyError as exc:
- return Response(f'Missing required parameter: {str(exc)}',
- status=status.HTTP_400_BAD_REQUEST)
+ return Response(f"Missing required parameter: {str(exc)}", status=status.HTTP_400_BAD_REQUEST)
except RetirementState.DoesNotExist:
- return Response('Unknown retirement state.', status=status.HTTP_400_BAD_REQUEST)
+ return Response("Unknown retirement state.", status=status.HTTP_400_BAD_REQUEST)
except RetirementStateError as exc:
return Response(str(exc), status=status.HTTP_400_BAD_REQUEST)
@@ -980,9 +966,9 @@ def retrieve(self, request, username): # pylint: disable=unused-argument
"""
try:
user = get_potentially_retired_user_by_username(username)
- retirement = UserRetirementStatus.objects.select_related(
- 'user', 'current_state', 'last_state'
- ).get(user=user)
+ retirement = UserRetirementStatus.objects.select_related("user", "current_state", "last_state").get(
+ user=user
+ )
serializer = UserRetirementStatusSerializer(instance=retirement)
return Response(serializer.data)
except (UserRetirementStatus.DoesNotExist, User.DoesNotExist):
@@ -1008,7 +994,7 @@ def partial_update(self, request):
The content type for this request is 'application/json'.
"""
try:
- username = request.data['username']
+ username = request.data["username"]
retirements = UserRetirementStatus.objects.filter(original_username=username)
# During a narrow window learners were able to re-use a username that had been retired if
@@ -1049,20 +1035,19 @@ def cleanup(self, request):
Deletes a batch of retirement requests by username.
"""
try:
- usernames = request.data['usernames']
+ usernames = request.data["usernames"]
if not isinstance(usernames, list):
- raise TypeError('Usernames should be an array.')
+ raise TypeError("Usernames should be an array.")
- complete_state = RetirementState.objects.get(state_name='COMPLETE')
+ complete_state = RetirementState.objects.get(state_name="COMPLETE")
retirements = UserRetirementStatus.objects.filter(
- original_username__in=usernames,
- current_state=complete_state
+ original_username__in=usernames, current_state=complete_state
)
# Sanity check that they're all valid usernames in the right state
if len(usernames) != len(retirements):
- raise UserRetirementStatus.DoesNotExist('Not all usernames exist in the COMPLETE state.')
+ raise UserRetirementStatus.DoesNotExist("Not all usernames exist in the COMPLETE state.")
retirements.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1076,7 +1061,11 @@ class LMSAccountRetirementView(ViewSet):
"""
Provides an API endpoint for retiring a user in the LMS.
"""
- permission_classes = (permissions.IsAuthenticated, CanRetireUser,)
+
+ permission_classes = (
+ permissions.IsAuthenticated,
+ CanRetireUser,
+ )
parser_classes = (JSONParser,)
@request_requires_username
@@ -1093,13 +1082,13 @@ def post(self, request):
Retires the user with the given username in the LMS.
"""
- username = request.data['username']
+ username = request.data["username"]
try:
retirement = UserRetirementStatus.get_retirement_for_retirement_action(username)
RevisionPluginRevision.retire_user(retirement.user)
ArticleRevision.retire_user(retirement.user)
- PendingNameChange.delete_by_user_value(retirement.user, field='user')
+ PendingNameChange.delete_by_user_value(retirement.user, field="user")
ManualEnrollmentAudit.retire_manual_enrollments(retirement.user, retirement.retired_email)
CreditRequest.retire_user(retirement)
@@ -1115,7 +1104,7 @@ def post(self, request):
sender=self.__class__,
email=retirement.original_email,
new_email=retirement.retired_email,
- user=retirement.user
+ user=retirement.user,
)
except UserRetirementStatus.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
@@ -1131,7 +1120,11 @@ class AccountRetirementView(ViewSet):
"""
Provides API endpoint for retiring a user.
"""
- permission_classes = (permissions.IsAuthenticated, CanRetireUser,)
+
+ permission_classes = (
+ permissions.IsAuthenticated,
+ CanRetireUser,
+ )
parser_classes = (JSONParser,)
@request_requires_username
@@ -1148,7 +1141,7 @@ def post(self, request):
Retires the user with the given username. This includes retiring this username, the associated email address,
and any other PII associated with this user.
"""
- username = request.data['username']
+ username = request.data["username"]
try:
retirement_status = UserRetirementStatus.get_retirement_for_retirement_action(username)
@@ -1173,18 +1166,18 @@ def post(self, request):
self.retire_entitlement_support_detail(user)
# Retire misc. models that may contain PII of this user
- PendingEmailChange.delete_by_user_value(user, field='user')
- UserOrgTag.delete_by_user_value(user, field='user')
+ PendingEmailChange.delete_by_user_value(user, field="user")
+ UserOrgTag.delete_by_user_value(user, field="user")
# Retire any objects linked to the user via their original email
- CourseEnrollmentAllowed.delete_by_user_value(original_email, field='email')
- UnregisteredLearnerCohortAssignments.delete_by_user_value(original_email, field='email')
+ CourseEnrollmentAllowed.delete_by_user_value(original_email, field="email")
+ UnregisteredLearnerCohortAssignments.delete_by_user_value(original_email, field="email")
# This signal allows code in higher points of LMS to retire the user as necessary
USER_RETIRE_LMS_CRITICAL.send(sender=self.__class__, user=user)
- user.first_name = ''
- user.last_name = ''
+ user.first_name = ""
+ user.last_name = ""
user.is_active = False
user.username = retired_username
user.save()
@@ -1227,24 +1220,20 @@ def retire_users_data_sharing_consent(username, retired_username):
@staticmethod
def retire_sapsf_data_transmission(user): # lint-amnesty, pylint: disable=missing-function-docstring
for ent_user in EnterpriseCustomerUser.objects.filter(user_id=user.id):
- for enrollment in EnterpriseCourseEnrollment.objects.filter(
- enterprise_customer_user=ent_user
- ):
+ for enrollment in EnterpriseCourseEnrollment.objects.filter(enterprise_customer_user=ent_user):
audits = SapSuccessFactorsLearnerDataTransmissionAudit.objects.filter(
enterprise_course_enrollment_id=enrollment.id
)
- audits.update(sapsf_user_id='')
+ audits.update(sapsf_user_id="")
@staticmethod
def retire_degreed_data_transmission(user): # lint-amnesty, pylint: disable=missing-function-docstring
for ent_user in EnterpriseCustomerUser.objects.filter(user_id=user.id):
- for enrollment in EnterpriseCourseEnrollment.objects.filter(
- enterprise_customer_user=ent_user
- ):
+ for enrollment in EnterpriseCourseEnrollment.objects.filter(enterprise_customer_user=ent_user):
audits = DegreedLearnerDataTransmissionAudit.objects.filter(
enterprise_course_enrollment_id=enrollment.id
)
- audits.update(degreed_user_email='')
+ audits.update(degreed_user_email="")
@staticmethod
def retire_user_from_pending_enterprise_customer_user(user, retired_email):
@@ -1256,7 +1245,7 @@ def retire_entitlement_support_detail(user):
Updates all CourseEntitleSupportDetail records for the given user to have an empty ``comments`` field.
"""
for entitlement in CourseEntitlement.objects.filter(user_id=user.id):
- entitlement.courseentitlementsupportdetail_set.all().update(comments='')
+ entitlement.courseentitlementsupportdetail_set.all().update(comments="")
@staticmethod
def clear_pii_from_certificate_records(user):
@@ -1279,6 +1268,7 @@ class UsernameReplacementView(APIView):
This API will be called first, before calling the APIs in other services as this
one handles the checks on the usernames provided.
"""
+
permission_classes = (permissions.IsAuthenticated, CanReplaceUsername)
def post(self, request):
@@ -1320,16 +1310,16 @@ def post(self, request):
# (model_name, column_name)
MODELS_WITH_USERNAME = (
- ('auth.user', 'username'),
- ('consent.DataSharingConsent', 'username'),
- ('consent.HistoricalDataSharingConsent', 'username'),
- ('credit.CreditEligibility', 'username'),
- ('credit.CreditRequest', 'username'),
- ('credit.CreditRequirementStatus', 'username'),
- ('user_api.UserRetirementPartnerReportingStatus', 'original_username'),
- ('user_api.UserRetirementStatus', 'original_username')
+ ("auth.user", "username"),
+ ("consent.DataSharingConsent", "username"),
+ ("consent.HistoricalDataSharingConsent", "username"),
+ ("credit.CreditEligibility", "username"),
+ ("credit.CreditRequest", "username"),
+ ("credit.CreditRequirementStatus", "username"),
+ ("user_api.UserRetirementPartnerReportingStatus", "original_username"),
+ ("user_api.UserRetirementStatus", "original_username"),
)
- UNIQUE_SUFFIX_LENGTH = getattr(settings, 'SOCIAL_AUTH_UUID_LENGTH', 4)
+ UNIQUE_SUFFIX_LENGTH = getattr(settings, "SOCIAL_AUTH_UUID_LENGTH", 4)
username_mappings = request.data.get("username_mappings")
replacement_locations = self._load_models(MODELS_WITH_USERNAME)
@@ -1344,9 +1334,7 @@ def post(self, request):
desired_username = list(username_pair.values())[0]
new_username = self._generate_unique_username(desired_username, suffix_length=UNIQUE_SUFFIX_LENGTH)
successfully_replaced = self._replace_username_for_all_models(
- current_username,
- new_username,
- replacement_locations
+ current_username, new_username, replacement_locations
)
if successfully_replaced:
successful_replacements.append({current_username: new_username})
@@ -1354,14 +1342,11 @@ def post(self, request):
failed_replacements.append({current_username: new_username})
return Response(
status=status.HTTP_200_OK,
- data={
- "successful_replacements": successful_replacements,
- "failed_replacements": failed_replacements
- }
+ data={"successful_replacements": successful_replacements, "failed_replacements": failed_replacements},
)
def _load_models(self, models_with_fields):
- """ Takes tuples that contain a model path and returns the list with a loaded version of the model """
+ """Takes tuples that contain a model path and returns the list with a loaded version of the model"""
try:
replacement_locations = [(apps.get_model(model), column) for (model, column) in models_with_fields]
except LookupError:
@@ -1370,7 +1355,7 @@ def _load_models(self, models_with_fields):
return replacement_locations
def _has_valid_schema(self, post_data):
- """ Verifies the data is a list of objects with a single key:value pair """
+ """Verifies the data is a list of objects with a single key:value pair"""
if not isinstance(post_data, list):
return False
for obj in post_data:
@@ -1389,7 +1374,7 @@ def _generate_unique_username(self, desired_username, suffix_length=4):
while True:
if User.objects.filter(username=new_username).exists():
# adding a dash between user-supplied and system-generated values to avoid weird combinations
- new_username = desired_username + '-' + username_suffix_generator(suffix_length)
+ new_username = desired_username + "-" + username_suffix_generator(suffix_length)
else:
break
return new_username
@@ -1404,10 +1389,8 @@ def _replace_username_for_all_models(self, current_username, new_username, repla
try:
with transaction.atomic():
num_rows_changed = 0
- for (model, column) in replacement_locations:
- num_rows_changed += model.objects.filter(
- **{column: current_username}
- ).update(
+ for model, column in replacement_locations:
+ num_rows_changed += model.objects.filter(**{column: current_username}).update(
**{column: new_username}
)
except Exception as exc: # pylint: disable=broad-except
@@ -1416,7 +1399,7 @@ def _replace_username_for_all_models(self, current_username, new_username, repla
current_username,
new_username,
model.__class__.__name__, # Retrieves the model name that it failed on
- exc
+ exc,
)
return False
if num_rows_changed == 0:
diff --git a/openedx/core/djangoapps/user_api/views.py b/openedx/core/djangoapps/user_api/views.py
index b4fcc68db649..d52493556a19 100644
--- a/openedx/core/djangoapps/user_api/views.py
+++ b/openedx/core/djangoapps/user_api/views.py
@@ -1,6 +1,5 @@
"""HTTP end-points for the User API. """
-
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.http import HttpResponse
from django.utils.decorators import method_decorator
@@ -16,21 +15,22 @@
from rest_framework.views import APIView
from openedx.core.djangoapps.django_comment_common.models import Role
-from openedx.core.lib.api.view_utils import require_post_params
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangoapps.user_api.preferences.api import get_country_time_zones, update_email_opt_in
from openedx.core.djangoapps.user_api.serializers import (
CountryTimeZoneSerializer,
UserPreferenceSerializer,
- UserSerializer
+ UserSerializer,
)
from openedx.core.lib.api.permissions import ApiKeyHeaderPermission
+from openedx.core.lib.api.view_utils import require_post_params
class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
DRF class for interacting with the User ORM object
"""
+
permission_classes = (ApiKeyHeaderPermission,)
queryset = User.objects.all().prefetch_related("preferences").select_related("profile")
serializer_class = UserSerializer
@@ -42,6 +42,7 @@ class ForumRoleUsersListView(generics.ListAPIView):
"""
Forum roles are represented by a list of user dicts
"""
+
permission_classes = (ApiKeyHeaderPermission,)
serializer_class = UserSerializer
paginate_by = 10
@@ -51,10 +52,10 @@ def get_queryset(self):
"""
Return a list of users with the specified role/course pair
"""
- name = self.kwargs['name']
- course_id_string = self.request.query_params.get('course_id')
+ name = self.kwargs["name"]
+ course_id_string = self.request.query_params.get("course_id")
if not course_id_string:
- raise ParseError('course_id must be specified')
+ raise ParseError("course_id must be specified")
course_id = CourseKey.from_string(course_id_string)
role = Role.objects.get_or_create(course_id=course_id, name=name)[0]
users = role.users.prefetch_related("preferences").select_related("profile").all()
@@ -65,6 +66,7 @@ class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet):
"""
DRF class for interacting with the UserPreference ORM
"""
+
permission_classes = (ApiKeyHeaderPermission,)
queryset = UserPreference.objects.all()
filter_backends = (DjangoFilterBackend,)
@@ -78,26 +80,30 @@ class PreferenceUsersListView(generics.ListAPIView):
"""
DRF class for listing a user's preferences
"""
+
permission_classes = (ApiKeyHeaderPermission,)
serializer_class = UserSerializer
paginate_by = 10
paginate_by_param = "page_size"
def get_queryset(self):
- return User.objects.filter(
- preferences__key=self.kwargs["pref_key"]
- ).prefetch_related("preferences").select_related("profile")
+ return (
+ User.objects.filter(preferences__key=self.kwargs["pref_key"])
+ .prefetch_related("preferences")
+ .select_related("profile")
+ )
class UpdateEmailOptInPreference(APIView):
- """View for updating the email opt in preference. """
+ """View for updating the email opt in preference."""
+
authentication_classes = (SessionAuthenticationAllowInactiveUser,)
permission_classes = (IsAuthenticated,)
@method_decorator(require_post_params(["course_id", "email_opt_in"]))
@method_decorator(ensure_csrf_cookie)
def post(self, request):
- """ Post function for updating the email opt in preference.
+ """Post function for updating the email opt in preference.
Allows the modification or creation of the email opt in preference at an
organizational level.
@@ -111,17 +117,13 @@ def post(self, request):
assume False.
"""
- course_id = request.data['course_id']
+ course_id = request.data["course_id"]
try:
org = locator.CourseLocator.from_string(course_id).org
except InvalidKeyError:
- return HttpResponse(
- status=400,
- content=f"No course '{course_id}' found",
- content_type="text/plain"
- )
+ return HttpResponse(status=400, content=f"No course '{course_id}' found", content_type="text/plain")
# Only check for true. All other values are False.
- email_opt_in = request.data['email_opt_in'].lower() == 'true'
+ email_opt_in = request.data["email_opt_in"].lower() == "true"
update_email_opt_in(request.user, org, email_opt_in)
return HttpResponse(status=status.HTTP_200_OK)
@@ -152,9 +154,10 @@ class CountryTimeZoneListView(generics.ListAPIView):
* time_zone: The name of the time zone.
* description: The display version of the time zone
"""
+
serializer_class = CountryTimeZoneSerializer
paginator = None
def get_queryset(self):
- country_code = self.request.GET.get('country_code', None)
+ country_code = self.request.GET.get("country_code", None)
return get_country_time_zones(country_code)
diff --git a/openedx/core/djangoapps/user_authn/views/register.py b/openedx/core/djangoapps/user_authn/views/register.py
index 17ca1d4f2a91..ab57687d2cd4 100644
--- a/openedx/core/djangoapps/user_authn/views/register.py
+++ b/openedx/core/djangoapps/user_authn/views/register.py
@@ -390,7 +390,9 @@ def _track_user_registration(user, profile, params, third_party_provider, regist
'is_year_of_birth_selected': bool(profile.year_of_birth),
'is_education_selected': bool(profile.level_of_education_display),
'is_goal_set': bool(profile.goals),
- 'total_registration_time': round(float(params.get('totalRegistrationTime', '0'))),
+ 'total_registration_time': round(
+ float(params.get('total_registration_time') or params.get('totalRegistrationTime') or 0)
+ ),
'activation_key': registration.activation_key if registration else None,
'host': params.get('host', ''),
'app_name': params.get('app_name', ''),
diff --git a/openedx/core/djangoapps/xblock/rest_api/views.py b/openedx/core/djangoapps/xblock/rest_api/views.py
index 8c2d16839a67..9972e7463b23 100644
--- a/openedx/core/djangoapps/xblock/rest_api/views.py
+++ b/openedx/core/djangoapps/xblock/rest_api/views.py
@@ -226,7 +226,9 @@ def post(self, request, usage_key_str):
old_metadata = block.get_explicitly_set_fields_by_scope(Scope.settings)
old_content = block.get_explicitly_set_fields_by_scope(Scope.content)
- block.data = data
+ # only update data if it was passed
+ if data is not None:
+ block.data = data
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'.
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 0a95e5c8a4cb..8c7e19a18a86 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -12,6 +12,12 @@
# This file contains all common constraints for edx-repos
-c common_constraints.txt
+# Date: 2024-08-21
+# Description: This is the major upgrade of algoliasearch python client and it will
+# break one of the edX' platform plugin, so we need to make that compatible first.
+# Ticket: https://github.com/openedx/edx-platform/issues/35334
+algoliasearch<4.0.0
+
# As it is not clarified what exact breaking changes will be introduced as per
# the next major release, ensure the installed version is within boundaries.
celery>=5.2.2,<6.0.0
@@ -20,7 +26,7 @@ celery>=5.2.2,<6.0.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.23.8
+edx-enterprise==4.23.13
# Stay on LTS version, remove once this is added to common constraint
Django<5.0
@@ -87,7 +93,7 @@ libsass==0.10.0
click==8.1.6
# pinning this version to avoid updates while the library is being developed
-openedx-learning==0.10.1
+openedx-learning==0.11.2
# Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise.
openai<=0.28.1
@@ -136,7 +142,3 @@ django-storages<1.14.4
# We are pinning this until after all the smaller migrations get handled and then we can migrate this all at once.
# Ticket to unpin: https://github.com/edx/edx-arch-experiments/issues/760
social-auth-app-django<=5.4.1
-
-# Xblock==5.0.0 changed how entrypoints were loaded, breaking a workaround for overriding blocks.
-# See ticket: https://github.com/openedx/XBlock/issues/777
-xblock[django]==4.0.1
diff --git a/requirements/edx-sandbox/base.txt b/requirements/edx-sandbox/base.txt
index 8942bc7dd9cb..4a7b0c0a7d35 100644
--- a/requirements/edx-sandbox/base.txt
+++ b/requirements/edx-sandbox/base.txt
@@ -35,13 +35,13 @@ markupsafe==2.1.5
# via
# chem
# openedx-calc
-matplotlib==3.9.0
+matplotlib==3.9.2
# via -r requirements/edx-sandbox/base.in
mpmath==1.3.0
# via sympy
networkx==3.3
# via -r requirements/edx-sandbox/base.in
-nltk==3.8.1
+nltk==3.9.1
# via
# -r requirements/edx-sandbox/base.in
# chem
@@ -82,7 +82,7 @@ six==1.16.0
# via
# codejail-includes
# python-dateutil
-sympy==1.13.1
+sympy==1.13.2
# via
# -r requirements/edx-sandbox/base.in
# openedx-calc
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 6a15216943f6..640ccbe982e7 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -8,16 +8,18 @@
# via -r requirements/edx/github.in
acid-xblock==0.3.1
# via -r requirements/edx/kernel.in
-aiohappyeyeballs==2.3.4
+aiohappyeyeballs==2.4.0
# via aiohttp
-aiohttp==3.10.1
+aiohttp==3.10.5
# via
# geoip2
# openai
aiosignal==1.3.1
# via aiohttp
algoliasearch==3.0.0
- # via -r requirements/edx/bundled.in
+ # via
+ # -c requirements/edx/../constraints.txt
+ # -r requirements/edx/bundled.in
amqp==5.2.0
# via kombu
analytics-python==1.4.post1
@@ -45,7 +47,7 @@ attrs==24.2.0
# openedx-events
# openedx-learning
# referencing
-babel==2.15.0
+babel==2.16.0
# via
# -r requirements/edx/kernel.in
# enmerkar
@@ -68,13 +70,13 @@ bleach[css]==6.1.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/kernel.in
-boto3==1.34.154
+boto3==1.35.1
# via
# -r requirements/edx/kernel.in
# django-ses
# fs-s3fs
# ora2
-botocore==1.34.154
+botocore==1.35.1
# via
# -r requirements/edx/kernel.in
# boto3
@@ -83,7 +85,7 @@ bridgekeeper==0.9
# via -r requirements/edx/kernel.in
cachecontrol==0.14.0
# via firebase-admin
-cachetools==5.4.0
+cachetools==5.5.0
# via google-auth
camel-converter[pydantic]==3.1.2
# via meilisearch
@@ -465,7 +467,7 @@ edx-drf-extensions==10.3.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.23.8
+edx-enterprise==4.23.13
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
@@ -515,7 +517,7 @@ edx-search==4.0.0
# via -r requirements/edx/kernel.in
edx-sga==0.25.0
# via -r requirements/edx/bundled.in
-edx-submissions==3.7.6
+edx-submissions==3.7.7
# via
# -r requirements/edx/kernel.in
# ora2
@@ -589,9 +591,9 @@ google-api-core[grpc]==2.19.1
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-api-python-client==2.139.0
+google-api-python-client==2.141.0
# via firebase-admin
-google-auth==2.32.0
+google-auth==2.34.0
# via
# google-api-core
# google-api-python-client
@@ -605,27 +607,27 @@ google-cloud-core==2.4.1
# via
# google-cloud-firestore
# google-cloud-storage
-google-cloud-firestore==2.17.0
+google-cloud-firestore==2.17.2
# via firebase-admin
-google-cloud-storage==2.18.0
+google-cloud-storage==2.18.2
# via firebase-admin
google-crc32c==1.5.0
# via
# google-cloud-storage
# google-resumable-media
-google-resumable-media==2.7.1
+google-resumable-media==2.7.2
# via google-cloud-storage
googleapis-common-protos==1.63.2
# via
# google-api-core
# grpcio-status
-grpcio==1.65.4
+grpcio==1.65.5
# via
# google-api-core
# grpcio-status
-grpcio-status==1.62.3
+grpcio-status==1.65.5
# via google-api-core
-gunicorn==22.0.0
+gunicorn==23.0.0
# via -r requirements/edx/kernel.in
help-tokens==2.4.0
# via -r requirements/edx/kernel.in
@@ -646,7 +648,7 @@ idna==3.7
# requests
# snowflake-connector-python
# yarl
-importlib-metadata==6.11.0
+importlib-metadata==8.3.0
# via -r requirements/edx/kernel.in
inflection==0.5.1
# via
@@ -755,7 +757,7 @@ monotonic==1.6
# via
# analytics-python
# py2neo
-more-itertools==10.3.0
+more-itertools==10.4.0
# via cssutils
mpmath==1.3.0
# via sympy
@@ -767,13 +769,13 @@ multidict==6.0.5
# yarl
mysqlclient==2.2.4
# via -r requirements/edx/kernel.in
-newrelic==9.12.0
+newrelic==9.13.0
# via
# -r requirements/edx/bundled.in
# edx-django-utils
nh3==0.2.18
# via -r requirements/edx/kernel.in
-nltk==3.8.1
+nltk==3.9.1
# via chem
nodeenv==1.9.1
# via -r requirements/edx/kernel.in
@@ -821,7 +823,7 @@ openedx-filters==1.9.0
# -r requirements/edx/kernel.in
# lti-consumer-xblock
# ora2
-openedx-learning==0.10.1
+openedx-learning==0.11.2
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
@@ -841,7 +843,7 @@ packaging==24.1
# snowflake-connector-python
pansi==2020.7.3
# via py2neo
-paramiko==3.4.0
+paramiko==3.4.1
# via edx-enterprise
path==16.11.0
# via
@@ -881,7 +883,7 @@ proto-plus==1.24.0
# via
# google-api-core
# google-cloud-firestore
-protobuf==4.25.4
+protobuf==5.27.3
# via
# google-api-core
# google-cloud-firestore
@@ -1022,7 +1024,7 @@ pytz==2024.1
# xblock
pyuca==1.2
# via -r requirements/edx/kernel.in
-pyyaml==6.0.1
+pyyaml==6.0.2
# via
# -r requirements/edx/kernel.in
# code-annotations
@@ -1099,9 +1101,9 @@ scipy==1.14.0
# openedx-calc
semantic-version==2.10.0
# via edx-drf-extensions
-shapely==2.0.5
+shapely==2.0.6
# via -r requirements/edx/kernel.in
-simplejson==3.19.2
+simplejson==3.19.3
# via
# -r requirements/edx/kernel.in
# sailthru-client
@@ -1162,7 +1164,7 @@ sortedcontainers==2.4.0
# via
# -r requirements/edx/kernel.in
# snowflake-connector-python
-soupsieve==2.5
+soupsieve==2.6
# via beautifulsoup4
sqlparse==0.5.1
# via django
@@ -1179,7 +1181,7 @@ stevedore==5.2.0
# edx-opaque-keys
super-csv==3.2.0
# via edx-bulk-grades
-sympy==1.13.1
+sympy==1.13.2
# via openedx-calc
testfixtures==8.3.0
# via edx-enterprise
@@ -1187,7 +1189,7 @@ text-unidecode==1.3
# via python-slugify
tinycss2==1.2.1
# via bleach
-tomlkit==0.13.0
+tomlkit==0.13.2
# via snowflake-connector-python
tqdm==4.66.5
# via
@@ -1233,7 +1235,7 @@ voluptuous==0.15.2
# via ora2
walrus==0.9.4
# via edx-event-bus-redis
-watchdog==4.0.1
+watchdog==4.0.2
# via -r requirements/edx/paver.txt
wcwidth==0.2.13
# via prompt-toolkit
@@ -1250,15 +1252,14 @@ webencodings==0.5.1
# bleach
# html5lib
# tinycss2
-webob==1.8.7
+webob==1.8.8
# via
# -r requirements/edx/kernel.in
# xblock
wrapt==1.16.0
# via -r requirements/edx/paver.txt
-xblock[django]==4.0.1
+xblock[django]==5.1.0
# via
- # -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
# acid-xblock
# crowdsourcehinter-xblock
@@ -1290,7 +1291,7 @@ xss-utils==0.6.0
# via -r requirements/edx/kernel.in
yarl==1.9.4
# via aiohttp
-zipp==3.19.2
+zipp==3.20.0
# via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt
index 8155169e33d3..9ae45b7ff72c 100644
--- a/requirements/edx/development.txt
+++ b/requirements/edx/development.txt
@@ -16,12 +16,12 @@ acid-xblock==0.3.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-aiohappyeyeballs==2.3.4
+aiohappyeyeballs==2.4.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# aiohttp
-aiohttp==3.10.1
+aiohttp==3.10.5
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -38,6 +38,7 @@ alabaster==1.0.0
# sphinx
algoliasearch==3.0.0
# via
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
amqp==5.2.0
@@ -96,7 +97,7 @@ attrs==24.2.0
# openedx-events
# openedx-learning
# referencing
-babel==2.15.0
+babel==2.16.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -139,14 +140,14 @@ boto==2.49.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-boto3==1.34.154
+boto3==1.35.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.34.154
+botocore==1.35.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -165,7 +166,7 @@ cachecontrol==0.14.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# firebase-admin
-cachetools==5.4.0
+cachetools==5.5.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -740,7 +741,7 @@ edx-drf-extensions==10.3.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.23.8
+edx-enterprise==4.23.13
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
@@ -813,7 +814,7 @@ edx-sga==0.25.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-edx-submissions==3.7.6
+edx-submissions==3.7.7
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -876,13 +877,13 @@ execnet==2.1.1
# via
# -r requirements/edx/testing.txt
# pytest-xdist
-factory-boy==3.3.0
+factory-boy==3.3.1
# via -r requirements/edx/testing.txt
-faker==26.2.0
+faker==27.0.0
# via
# -r requirements/edx/testing.txt
# factory-boy
-fastapi==0.112.0
+fastapi==0.112.1
# via
# -r requirements/edx/testing.txt
# pact-python
@@ -951,12 +952,12 @@ google-api-core[grpc]==2.19.1
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-api-python-client==2.139.0
+google-api-python-client==2.141.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# firebase-admin
-google-auth==2.32.0
+google-auth==2.34.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -977,12 +978,12 @@ google-cloud-core==2.4.1
# -r requirements/edx/testing.txt
# google-cloud-firestore
# google-cloud-storage
-google-cloud-firestore==2.17.0
+google-cloud-firestore==2.17.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# firebase-admin
-google-cloud-storage==2.18.0
+google-cloud-storage==2.18.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -993,7 +994,7 @@ google-crc32c==1.5.0
# -r requirements/edx/testing.txt
# google-cloud-storage
# google-resumable-media
-google-resumable-media==2.7.1
+google-resumable-media==2.7.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1008,18 +1009,18 @@ grimp==3.4.1
# via
# -r requirements/edx/testing.txt
# import-linter
-grpcio==1.65.4
+grpcio==1.65.5
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# google-api-core
# grpcio-status
-grpcio-status==1.62.3
+grpcio-status==1.65.5
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# google-api-core
-gunicorn==22.0.0
+gunicorn==23.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1063,7 +1064,7 @@ imagesize==1.4.1
# sphinx
import-linter==2.0
# via -r requirements/edx/testing.txt
-importlib-metadata==6.11.0
+importlib-metadata==8.3.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1257,7 +1258,7 @@ monotonic==1.6
# -r requirements/edx/testing.txt
# analytics-python
# py2neo
-more-itertools==10.3.0
+more-itertools==10.4.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1289,7 +1290,7 @@ mysqlclient==2.2.4
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-newrelic==9.12.0
+newrelic==9.13.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1298,7 +1299,7 @@ nh3==0.2.18
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-nltk==3.8.1
+nltk==3.9.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1371,7 +1372,7 @@ openedx-filters==1.9.0
# -r requirements/edx/testing.txt
# lti-consumer-xblock
# ora2
-openedx-learning==0.10.1
+openedx-learning==0.11.2
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
@@ -1411,7 +1412,7 @@ pansi==2020.7.3
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# py2neo
-paramiko==3.4.0
+paramiko==3.4.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1491,7 +1492,7 @@ proto-plus==1.24.0
# -r requirements/edx/testing.txt
# google-api-core
# google-cloud-firestore
-protobuf==4.25.4
+protobuf==5.27.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1777,7 +1778,7 @@ pyuca==1.2
# -r requirements/edx/testing.txt
pywatchman==2.0.0
# via -r requirements/edx/development.in
-pyyaml==6.0.1
+pyyaml==6.0.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1888,11 +1889,11 @@ semantic-version==2.10.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-drf-extensions
-shapely==2.0.5
+shapely==2.0.6
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-simplejson==3.19.2
+simplejson==3.19.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1980,7 +1981,7 @@ sortedcontainers==2.4.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# snowflake-connector-python
-soupsieve==2.5
+soupsieve==2.6
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2048,7 +2049,7 @@ staff-graded-xblock==2.3.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-starlette==0.37.2
+starlette==0.38.2
# via
# -r requirements/edx/testing.txt
# fastapi
@@ -2066,7 +2067,7 @@ super-csv==3.2.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-bulk-grades
-sympy==1.13.1
+sympy==1.13.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2088,13 +2089,13 @@ tinycss2==1.2.1
# bleach
tomli==2.0.1
# via django-stubs
-tomlkit==0.13.0
+tomlkit==0.13.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# pylint
# snowflake-connector-python
-tox==4.17.0
+tox==4.18.0
# via -r requirements/edx/testing.txt
tqdm==4.66.5
# via
@@ -2104,7 +2105,7 @@ tqdm==4.66.5
# openai
types-pytz==2024.1.0.20240417
# via django-stubs
-types-pyyaml==6.0.12.20240724
+types-pyyaml==6.0.12.20240808
# via
# django-stubs
# djangorestframework-stubs
@@ -2163,7 +2164,7 @@ user-util==1.1.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-uvicorn==0.30.5
+uvicorn==0.30.6
# via
# -r requirements/edx/testing.txt
# pact-python
@@ -2190,7 +2191,7 @@ walrus==0.9.4
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-event-bus-redis
-watchdog==4.0.1
+watchdog==4.0.2
# via
# -r requirements/edx/development.in
# -r requirements/edx/doc.txt
@@ -2216,7 +2217,7 @@ webencodings==0.5.1
# bleach
# html5lib
# tinycss2
-webob==1.8.7
+webob==1.8.8
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2230,9 +2231,8 @@ wrapt==1.16.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# astroid
-xblock[django]==4.0.1
+xblock[django]==5.1.0
# via
- # -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# acid-xblock
@@ -2281,7 +2281,7 @@ yarl==1.9.4
# -r requirements/edx/testing.txt
# aiohttp
# pact-python
-zipp==3.19.2
+zipp==3.20.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt
index bb81b7b71ac3..d315955694e7 100644
--- a/requirements/edx/doc.txt
+++ b/requirements/edx/doc.txt
@@ -10,11 +10,11 @@ accessible-pygments==0.0.5
# via pydata-sphinx-theme
acid-xblock==0.3.1
# via -r requirements/edx/base.txt
-aiohappyeyeballs==2.3.4
+aiohappyeyeballs==2.4.0
# via
# -r requirements/edx/base.txt
# aiohttp
-aiohttp==3.10.1
+aiohttp==3.10.5
# via
# -r requirements/edx/base.txt
# geoip2
@@ -26,7 +26,9 @@ aiosignal==1.3.1
alabaster==1.0.0
# via sphinx
algoliasearch==3.0.0
- # via -r requirements/edx/base.txt
+ # via
+ # -c requirements/edx/../constraints.txt
+ # -r requirements/edx/base.txt
amqp==5.2.0
# via
# -r requirements/edx/base.txt
@@ -65,7 +67,7 @@ attrs==24.2.0
# openedx-events
# openedx-learning
# referencing
-babel==2.15.0
+babel==2.16.0
# via
# -r requirements/edx/base.txt
# enmerkar
@@ -100,13 +102,13 @@ bleach[css]==6.1.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/base.txt
-boto3==1.34.154
+boto3==1.35.1
# via
# -r requirements/edx/base.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.34.154
+botocore==1.35.1
# via
# -r requirements/edx/base.txt
# boto3
@@ -117,7 +119,7 @@ cachecontrol==0.14.0
# via
# -r requirements/edx/base.txt
# firebase-admin
-cachetools==5.4.0
+cachetools==5.5.0
# via
# -r requirements/edx/base.txt
# google-auth
@@ -545,7 +547,7 @@ edx-drf-extensions==10.3.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.23.8
+edx-enterprise==4.23.13
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -596,7 +598,7 @@ edx-search==4.0.0
# via -r requirements/edx/base.txt
edx-sga==0.25.0
# via -r requirements/edx/base.txt
-edx-submissions==3.7.6
+edx-submissions==3.7.7
# via
# -r requirements/edx/base.txt
# ora2
@@ -689,11 +691,11 @@ google-api-core[grpc]==2.19.1
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-api-python-client==2.139.0
+google-api-python-client==2.141.0
# via
# -r requirements/edx/base.txt
# firebase-admin
-google-auth==2.32.0
+google-auth==2.34.0
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -711,11 +713,11 @@ google-cloud-core==2.4.1
# -r requirements/edx/base.txt
# google-cloud-firestore
# google-cloud-storage
-google-cloud-firestore==2.17.0
+google-cloud-firestore==2.17.2
# via
# -r requirements/edx/base.txt
# firebase-admin
-google-cloud-storage==2.18.0
+google-cloud-storage==2.18.2
# via
# -r requirements/edx/base.txt
# firebase-admin
@@ -724,7 +726,7 @@ google-crc32c==1.5.0
# -r requirements/edx/base.txt
# google-cloud-storage
# google-resumable-media
-google-resumable-media==2.7.1
+google-resumable-media==2.7.2
# via
# -r requirements/edx/base.txt
# google-cloud-storage
@@ -733,16 +735,16 @@ googleapis-common-protos==1.63.2
# -r requirements/edx/base.txt
# google-api-core
# grpcio-status
-grpcio==1.65.4
+grpcio==1.65.5
# via
# -r requirements/edx/base.txt
# google-api-core
# grpcio-status
-grpcio-status==1.62.3
+grpcio-status==1.65.5
# via
# -r requirements/edx/base.txt
# google-api-core
-gunicorn==22.0.0
+gunicorn==23.0.0
# via -r requirements/edx/base.txt
help-tokens==2.4.0
# via -r requirements/edx/base.txt
@@ -766,7 +768,7 @@ idna==3.7
# yarl
imagesize==1.4.1
# via sphinx
-importlib-metadata==6.11.0
+importlib-metadata==8.3.0
# via -r requirements/edx/base.txt
inflection==0.5.1
# via
@@ -902,7 +904,7 @@ monotonic==1.6
# -r requirements/edx/base.txt
# analytics-python
# py2neo
-more-itertools==10.3.0
+more-itertools==10.4.0
# via
# -r requirements/edx/base.txt
# cssutils
@@ -921,13 +923,13 @@ multidict==6.0.5
# yarl
mysqlclient==2.2.4
# via -r requirements/edx/base.txt
-newrelic==9.12.0
+newrelic==9.13.0
# via
# -r requirements/edx/base.txt
# edx-django-utils
nh3==0.2.18
# via -r requirements/edx/base.txt
-nltk==3.8.1
+nltk==3.9.1
# via
# -r requirements/edx/base.txt
# chem
@@ -980,7 +982,7 @@ openedx-filters==1.9.0
# -r requirements/edx/base.txt
# lti-consumer-xblock
# ora2
-openedx-learning==0.10.1
+openedx-learning==0.11.2
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -1005,7 +1007,7 @@ pansi==2020.7.3
# via
# -r requirements/edx/base.txt
# py2neo
-paramiko==3.4.0
+paramiko==3.4.1
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -1058,7 +1060,7 @@ proto-plus==1.24.0
# -r requirements/edx/base.txt
# google-api-core
# google-cloud-firestore
-protobuf==4.25.4
+protobuf==5.27.3
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -1227,7 +1229,7 @@ pytz==2024.1
# xblock
pyuca==1.2
# via -r requirements/edx/base.txt
-pyyaml==6.0.1
+pyyaml==6.0.2
# via
# -r requirements/edx/base.txt
# code-annotations
@@ -1319,9 +1321,9 @@ semantic-version==2.10.0
# via
# -r requirements/edx/base.txt
# edx-drf-extensions
-shapely==2.0.5
+shapely==2.0.6
# via -r requirements/edx/base.txt
-simplejson==3.19.2
+simplejson==3.19.3
# via
# -r requirements/edx/base.txt
# sailthru-client
@@ -1388,7 +1390,7 @@ sortedcontainers==2.4.0
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
-soupsieve==2.5
+soupsieve==2.6
# via
# -r requirements/edx/base.txt
# beautifulsoup4
@@ -1447,7 +1449,7 @@ super-csv==3.2.0
# via
# -r requirements/edx/base.txt
# edx-bulk-grades
-sympy==1.13.1
+sympy==1.13.2
# via
# -r requirements/edx/base.txt
# openedx-calc
@@ -1463,7 +1465,7 @@ tinycss2==1.2.1
# via
# -r requirements/edx/base.txt
# bleach
-tomlkit==0.13.0
+tomlkit==0.13.2
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
@@ -1521,7 +1523,7 @@ walrus==0.9.4
# via
# -r requirements/edx/base.txt
# edx-event-bus-redis
-watchdog==4.0.1
+watchdog==4.0.2
# via -r requirements/edx/base.txt
wcwidth==0.2.13
# via
@@ -1541,15 +1543,14 @@ webencodings==0.5.1
# bleach
# html5lib
# tinycss2
-webob==1.8.7
+webob==1.8.8
# via
# -r requirements/edx/base.txt
# xblock
wrapt==1.16.0
# via -r requirements/edx/base.txt
-xblock[django]==4.0.1
+xblock[django]==5.1.0
# via
- # -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# acid-xblock
# crowdsourcehinter-xblock
@@ -1585,7 +1586,7 @@ yarl==1.9.4
# via
# -r requirements/edx/base.txt
# aiohttp
-zipp==3.19.2
+zipp==3.20.0
# via
# -r requirements/edx/base.txt
# importlib-metadata
diff --git a/requirements/edx/paver.txt b/requirements/edx/paver.txt
index 0b82d71c91e6..faa0085f1631 100644
--- a/requirements/edx/paver.txt
+++ b/requirements/edx/paver.txt
@@ -61,7 +61,7 @@ urllib3==1.26.19
# via
# -c requirements/edx/../constraints.txt
# requests
-watchdog==4.0.1
+watchdog==4.0.2
# via -r requirements/edx/paver.in
wrapt==1.16.0
# via -r requirements/edx/paver.in
diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt
index 18c988dfb622..fcb7db05a5ef 100644
--- a/requirements/edx/testing.txt
+++ b/requirements/edx/testing.txt
@@ -8,11 +8,11 @@
# via -r requirements/edx/base.txt
acid-xblock==0.3.1
# via -r requirements/edx/base.txt
-aiohappyeyeballs==2.3.4
+aiohappyeyeballs==2.4.0
# via
# -r requirements/edx/base.txt
# aiohttp
-aiohttp==3.10.1
+aiohttp==3.10.5
# via
# -r requirements/edx/base.txt
# geoip2
@@ -22,7 +22,9 @@ aiosignal==1.3.1
# -r requirements/edx/base.txt
# aiohttp
algoliasearch==3.0.0
- # via -r requirements/edx/base.txt
+ # via
+ # -c requirements/edx/../constraints.txt
+ # -r requirements/edx/base.txt
amqp==5.2.0
# via
# -r requirements/edx/base.txt
@@ -67,7 +69,7 @@ attrs==24.2.0
# openedx-events
# openedx-learning
# referencing
-babel==2.15.0
+babel==2.16.0
# via
# -r requirements/edx/base.txt
# enmerkar
@@ -100,13 +102,13 @@ bleach[css]==6.1.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/base.txt
-boto3==1.34.154
+boto3==1.35.1
# via
# -r requirements/edx/base.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.34.154
+botocore==1.35.1
# via
# -r requirements/edx/base.txt
# boto3
@@ -117,7 +119,7 @@ cachecontrol==0.14.0
# via
# -r requirements/edx/base.txt
# firebase-admin
-cachetools==5.4.0
+cachetools==5.5.0
# via
# -r requirements/edx/base.txt
# google-auth
@@ -569,7 +571,7 @@ edx-drf-extensions==10.3.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.23.8
+edx-enterprise==4.23.13
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -622,7 +624,7 @@ edx-search==4.0.0
# via -r requirements/edx/base.txt
edx-sga==0.25.0
# via -r requirements/edx/base.txt
-edx-submissions==3.7.6
+edx-submissions==3.7.7
# via
# -r requirements/edx/base.txt
# ora2
@@ -670,11 +672,11 @@ event-tracking==3.0.0
# edx-search
execnet==2.1.1
# via pytest-xdist
-factory-boy==3.3.0
+factory-boy==3.3.1
# via -r requirements/edx/testing.in
-faker==26.2.0
+faker==27.0.0
# via factory-boy
-fastapi==0.112.0
+fastapi==0.112.1
# via pact-python
fastavro==1.9.5
# via
@@ -723,11 +725,11 @@ google-api-core[grpc]==2.19.1
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-api-python-client==2.139.0
+google-api-python-client==2.141.0
# via
# -r requirements/edx/base.txt
# firebase-admin
-google-auth==2.32.0
+google-auth==2.34.0
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -745,11 +747,11 @@ google-cloud-core==2.4.1
# -r requirements/edx/base.txt
# google-cloud-firestore
# google-cloud-storage
-google-cloud-firestore==2.17.0
+google-cloud-firestore==2.17.2
# via
# -r requirements/edx/base.txt
# firebase-admin
-google-cloud-storage==2.18.0
+google-cloud-storage==2.18.2
# via
# -r requirements/edx/base.txt
# firebase-admin
@@ -758,7 +760,7 @@ google-crc32c==1.5.0
# -r requirements/edx/base.txt
# google-cloud-storage
# google-resumable-media
-google-resumable-media==2.7.1
+google-resumable-media==2.7.2
# via
# -r requirements/edx/base.txt
# google-cloud-storage
@@ -769,16 +771,16 @@ googleapis-common-protos==1.63.2
# grpcio-status
grimp==3.4.1
# via import-linter
-grpcio==1.65.4
+grpcio==1.65.5
# via
# -r requirements/edx/base.txt
# google-api-core
# grpcio-status
-grpcio-status==1.62.3
+grpcio-status==1.65.5
# via
# -r requirements/edx/base.txt
# google-api-core
-gunicorn==22.0.0
+gunicorn==23.0.0
# via -r requirements/edx/base.txt
h11==0.14.0
# via uvicorn
@@ -807,7 +809,7 @@ idna==3.7
# yarl
import-linter==2.0
# via -r requirements/edx/testing.in
-importlib-metadata==6.11.0
+importlib-metadata==8.3.0
# via -r requirements/edx/base.txt
inflection==0.5.1
# via
@@ -953,7 +955,7 @@ monotonic==1.6
# -r requirements/edx/base.txt
# analytics-python
# py2neo
-more-itertools==10.3.0
+more-itertools==10.4.0
# via
# -r requirements/edx/base.txt
# cssutils
@@ -972,13 +974,13 @@ multidict==6.0.5
# yarl
mysqlclient==2.2.4
# via -r requirements/edx/base.txt
-newrelic==9.12.0
+newrelic==9.13.0
# via
# -r requirements/edx/base.txt
# edx-django-utils
nh3==0.2.18
# via -r requirements/edx/base.txt
-nltk==3.8.1
+nltk==3.9.1
# via
# -r requirements/edx/base.txt
# chem
@@ -1031,7 +1033,7 @@ openedx-filters==1.9.0
# -r requirements/edx/base.txt
# lti-consumer-xblock
# ora2
-openedx-learning==0.10.1
+openedx-learning==0.11.2
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -1059,7 +1061,7 @@ pansi==2020.7.3
# via
# -r requirements/edx/base.txt
# py2neo
-paramiko==3.4.0
+paramiko==3.4.1
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -1120,7 +1122,7 @@ proto-plus==1.24.0
# -r requirements/edx/base.txt
# google-api-core
# google-cloud-firestore
-protobuf==4.25.4
+protobuf==5.27.3
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -1345,7 +1347,7 @@ pytz==2024.1
# xblock
pyuca==1.2
# via -r requirements/edx/base.txt
-pyyaml==6.0.1
+pyyaml==6.0.2
# via
# -r requirements/edx/base.txt
# code-annotations
@@ -1436,9 +1438,9 @@ semantic-version==2.10.0
# via
# -r requirements/edx/base.txt
# edx-drf-extensions
-shapely==2.0.5
+shapely==2.0.6
# via -r requirements/edx/base.txt
-simplejson==3.19.2
+simplejson==3.19.3
# via
# -r requirements/edx/base.txt
# sailthru-client
@@ -1506,7 +1508,7 @@ sortedcontainers==2.4.0
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
-soupsieve==2.5
+soupsieve==2.6
# via
# -r requirements/edx/base.txt
# beautifulsoup4
@@ -1516,7 +1518,7 @@ sqlparse==0.5.1
# django
staff-graded-xblock==2.3.0
# via -r requirements/edx/base.txt
-starlette==0.37.2
+starlette==0.38.2
# via fastapi
stevedore==5.2.0
# via
@@ -1530,7 +1532,7 @@ super-csv==3.2.0
# via
# -r requirements/edx/base.txt
# edx-bulk-grades
-sympy==1.13.1
+sympy==1.13.2
# via
# -r requirements/edx/base.txt
# openedx-calc
@@ -1547,12 +1549,12 @@ tinycss2==1.2.1
# via
# -r requirements/edx/base.txt
# bleach
-tomlkit==0.13.0
+tomlkit==0.13.2
# via
# -r requirements/edx/base.txt
# pylint
# snowflake-connector-python
-tox==4.17.0
+tox==4.18.0
# via -r requirements/edx/testing.in
tqdm==4.66.5
# via
@@ -1598,7 +1600,7 @@ urllib3==1.26.19
# requests
user-util==1.1.0
# via -r requirements/edx/base.txt
-uvicorn==0.30.5
+uvicorn==0.30.6
# via pact-python
vine==5.1.0
# via
@@ -1616,7 +1618,7 @@ walrus==0.9.4
# via
# -r requirements/edx/base.txt
# edx-event-bus-redis
-watchdog==4.0.1
+watchdog==4.0.2
# via -r requirements/edx/base.txt
wcwidth==0.2.13
# via
@@ -1636,7 +1638,7 @@ webencodings==0.5.1
# bleach
# html5lib
# tinycss2
-webob==1.8.7
+webob==1.8.8
# via
# -r requirements/edx/base.txt
# xblock
@@ -1644,9 +1646,8 @@ wrapt==1.16.0
# via
# -r requirements/edx/base.txt
# astroid
-xblock[django]==4.0.1
+xblock[django]==5.1.0
# via
- # -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# acid-xblock
# crowdsourcehinter-xblock
@@ -1683,7 +1684,7 @@ yarl==1.9.4
# -r requirements/edx/base.txt
# aiohttp
# pact-python
-zipp==3.19.2
+zipp==3.20.0
# via
# -r requirements/edx/base.txt
# importlib-metadata
diff --git a/requirements/pip.txt b/requirements/pip.txt
index 7a6ada8e0a92..f0cf3d109992 100644
--- a/requirements/pip.txt
+++ b/requirements/pip.txt
@@ -10,5 +10,5 @@ wheel==0.44.0
# The following packages are considered to be unsafe in a requirements file:
pip==24.2
# via -r requirements/pip.in
-setuptools==72.1.0
+setuptools==73.0.0
# via -r requirements/pip.in
diff --git a/scripts/user_retirement/requirements/base.txt b/scripts/user_retirement/requirements/base.txt
index 47e6e79c2240..3e5ed4738070 100644
--- a/scripts/user_retirement/requirements/base.txt
+++ b/scripts/user_retirement/requirements/base.txt
@@ -10,13 +10,13 @@ attrs==24.2.0
# via zeep
backoff==2.2.1
# via -r scripts/user_retirement/requirements/base.in
-boto3==1.34.154
+boto3==1.35.1
# via -r scripts/user_retirement/requirements/base.in
-botocore==1.34.154
+botocore==1.35.1
# via
# boto3
# s3transfer
-cachetools==5.4.0
+cachetools==5.5.0
# via google-auth
certifi==2024.7.4
# via requests
@@ -52,9 +52,9 @@ edx-rest-api-client==5.7.1
# via -r scripts/user_retirement/requirements/base.in
google-api-core==2.19.1
# via google-api-python-client
-google-api-python-client==2.139.0
+google-api-python-client==2.141.0
# via -r scripts/user_retirement/requirements/base.in
-google-auth==2.32.0
+google-auth==2.34.0
# via
# google-api-core
# google-api-python-client
@@ -81,9 +81,9 @@ lxml==4.9.4
# via
# -c scripts/user_retirement/requirements/../../../requirements/constraints.txt
# zeep
-more-itertools==10.3.0
+more-itertools==10.4.0
# via simple-salesforce
-newrelic==9.12.0
+newrelic==9.13.0
# via edx-django-utils
pbr==6.0.0
# via stevedore
@@ -120,7 +120,7 @@ pytz==2024.1
# via
# jenkinsapi
# zeep
-pyyaml==6.0.1
+pyyaml==6.0.2
# via -r scripts/user_retirement/requirements/base.in
requests==2.32.3
# via
@@ -143,7 +143,7 @@ s3transfer==0.10.2
# via boto3
simple-salesforce==1.12.6
# via -r scripts/user_retirement/requirements/base.in
-simplejson==3.19.2
+simplejson==3.19.3
# via -r scripts/user_retirement/requirements/base.in
six==1.16.0
# via
diff --git a/scripts/user_retirement/requirements/testing.txt b/scripts/user_retirement/requirements/testing.txt
index 006eabeef436..f7464dfa0602 100644
--- a/scripts/user_retirement/requirements/testing.txt
+++ b/scripts/user_retirement/requirements/testing.txt
@@ -14,17 +14,17 @@ attrs==24.2.0
# zeep
backoff==2.2.1
# via -r scripts/user_retirement/requirements/base.txt
-boto3==1.34.154
+boto3==1.35.1
# via
# -r scripts/user_retirement/requirements/base.txt
# moto
-botocore==1.34.154
+botocore==1.35.1
# via
# -r scripts/user_retirement/requirements/base.txt
# boto3
# moto
# s3transfer
-cachetools==5.4.0
+cachetools==5.5.0
# via
# -r scripts/user_retirement/requirements/base.txt
# google-auth
@@ -76,9 +76,9 @@ google-api-core==2.19.1
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-python-client
-google-api-python-client==2.139.0
+google-api-python-client==2.141.0
# via -r scripts/user_retirement/requirements/base.txt
-google-auth==2.32.0
+google-auth==2.34.0
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-core
@@ -126,13 +126,13 @@ markupsafe==2.1.5
# werkzeug
mock==5.1.0
# via -r scripts/user_retirement/requirements/testing.in
-more-itertools==10.3.0
+more-itertools==10.4.0
# via
# -r scripts/user_retirement/requirements/base.txt
# simple-salesforce
moto==4.2.14
# via -r scripts/user_retirement/requirements/testing.in
-newrelic==9.12.0
+newrelic==9.13.0
# via
# -r scripts/user_retirement/requirements/base.txt
# edx-django-utils
@@ -200,7 +200,7 @@ pytz==2024.1
# -r scripts/user_retirement/requirements/base.txt
# jenkinsapi
# zeep
-pyyaml==6.0.1
+pyyaml==6.0.2
# via
# -r scripts/user_retirement/requirements/base.txt
# responses
@@ -242,7 +242,7 @@ s3transfer==0.10.2
# boto3
simple-salesforce==1.12.6
# via -r scripts/user_retirement/requirements/base.txt
-simplejson==3.19.2
+simplejson==3.19.3
# via -r scripts/user_retirement/requirements/base.txt
six==1.16.0
# via
diff --git a/themes/red-theme/lms/templates/ace_common/edx_ace/common/base_body.html b/themes/red-theme/lms/templates/ace_common/edx_ace/common/base_body.html
index 8d51b16498d7..9319217aa4cf 100644
--- a/themes/red-theme/lms/templates/ace_common/edx_ace/common/base_body.html
+++ b/themes/red-theme/lms/templates/ace_common/edx_ace/common/base_body.html
@@ -63,7 +63,7 @@
|