feat: linting only (#35370)
Editing this file is going to cause a lot of automatic linting, so lint-only commit for nicer code review.
This commit is contained in:
@@ -21,7 +21,6 @@ from django.utils.translation import gettext as _
|
||||
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 @@ from common.djangoapps.student.models import ( # lint-amnesty, pylint: disable=
|
||||
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 import accounts
|
||||
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 @@ from ..models import (
|
||||
RetirementStateError,
|
||||
UserOrgTag,
|
||||
UserRetirementPartnerReportingStatus,
|
||||
UserRetirementStatus
|
||||
UserRetirementStatus,
|
||||
)
|
||||
from .api import get_account_settings, update_account_settings
|
||||
from .permissions import (
|
||||
@@ -83,13 +82,13 @@ from .permissions import (
|
||||
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 @@ from .utils import create_retirement_request_and_deactivate_account, username_su
|
||||
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 request_requires_username(function):
|
||||
|
||||
class AccountViewSet(ViewSet):
|
||||
"""
|
||||
**Use Cases**
|
||||
**Use Cases**
|
||||
|
||||
Get or update a user's account information. Updates are supported
|
||||
only through merge patch.
|
||||
Get or update a user's account information. Updates are supported
|
||||
only through merge patch.
|
||||
|
||||
**Example Requests**
|
||||
**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]
|
||||
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"
|
||||
PATCH /api/user/v1/accounts/{username}/{"key":"value"} "application/merge-patch+json"
|
||||
|
||||
POST /api/user/v1/accounts/search_emails "application/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 ("").
|
||||
**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.
|
||||
**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:
|
||||
Otherwise, an HTTP 200 "OK" response is returned. The response
|
||||
contains the following value:
|
||||
|
||||
* username: The username associated with the account.
|
||||
* username: The username associated with the account.
|
||||
|
||||
**Response Values for GET requests to /accounts endpoints**
|
||||
**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 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.
|
||||
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:
|
||||
* 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"
|
||||
* 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:
|
||||
* 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".
|
||||
* "code": string ISO 639-1 language code e.g. "en".
|
||||
|
||||
* level_of_education: One of the following values:
|
||||
* 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
|
||||
* "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.
|
||||
* 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.
|
||||
* "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:
|
||||
* 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
|
||||
* "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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
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.
|
||||
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.
|
||||
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**
|
||||
**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.
|
||||
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 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 "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 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 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.
|
||||
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 @@ class AccountViewSet(ViewSet):
|
||||
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 @@ class AccountViewSet(ViewSet):
|
||||
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 @@ class AccountViewSet(ViewSet):
|
||||
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 @@ class AccountViewSet(ViewSet):
|
||||
"""
|
||||
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 @@ class AccountViewSet(ViewSet):
|
||||
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 @@ class AccountViewSet(ViewSet):
|
||||
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 @@ class NameChangeView(ViewSet):
|
||||
}
|
||||
"""
|
||||
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 @@ class NameChangeView(ViewSet):
|
||||
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 @@ class DeactivateLogoutView(APIView):
|
||||
|
||||
# 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 @@ class DeactivateLogoutView(APIView):
|
||||
# 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 @@ class DeactivateLogoutView(APIView):
|
||||
)
|
||||
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 @@ class DeactivateLogoutView(APIView):
|
||||
"""
|
||||
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 @@ class DeactivateLogoutView(APIView):
|
||||
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 @@ class DeactivateLogoutView(APIView):
|
||||
"""
|
||||
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 @@ class DeactivateLogoutView(APIView):
|
||||
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 @@ class AccountRetirementPartnerReportView(ViewSet):
|
||||
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 @@ class AccountRetirementPartnerReportView(ViewSet):
|
||||
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 @@ class AccountRetirementPartnerReportView(ViewSet):
|
||||
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 @@ class AccountRetirementPartnerReportView(ViewSet):
|
||||
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 @@ class AccountRetirementPartnerReportView(ViewSet):
|
||||
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 @@ class AccountRetirementPartnerReportView(ViewSet):
|
||||
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 @@ class AccountRetirementPartnerReportView(ViewSet):
|
||||
# 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 @@ class CancelAccountRetirementStatusView(ViewSet):
|
||||
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 @@ class AccountRetirementStatusView(ViewSet):
|
||||
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 @@ class AccountRetirementStatusView(ViewSet):
|
||||
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 @@ class AccountRetirementStatusView(ViewSet):
|
||||
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 @@ class AccountRetirementStatusView(ViewSet):
|
||||
"""
|
||||
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 @@ class AccountRetirementStatusView(ViewSet):
|
||||
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 @@ class AccountRetirementStatusView(ViewSet):
|
||||
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 @@ class LMSAccountRetirementView(ViewSet):
|
||||
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 @@ class LMSAccountRetirementView(ViewSet):
|
||||
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 @@ class AccountRetirementView(ViewSet):
|
||||
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 @@ class AccountRetirementView(ViewSet):
|
||||
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 @@ class AccountRetirementView(ViewSet):
|
||||
@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 @@ class AccountRetirementView(ViewSet):
|
||||
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 @@ class UsernameReplacementView(APIView):
|
||||
|
||||
# (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 @@ class UsernameReplacementView(APIView):
|
||||
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 @@ class UsernameReplacementView(APIView):
|
||||
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 @@ class UsernameReplacementView(APIView):
|
||||
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 @@ class UsernameReplacementView(APIView):
|
||||
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 @@ class UsernameReplacementView(APIView):
|
||||
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 @@ class UsernameReplacementView(APIView):
|
||||
current_username,
|
||||
new_username,
|
||||
model.__class__.__name__, # Retrieves the model name that it failed on
|
||||
exc
|
||||
exc,
|
||||
)
|
||||
return False
|
||||
if num_rows_changed == 0:
|
||||
|
||||
Reference in New Issue
Block a user