lint w black. add more logging

This commit is contained in:
lpm0073 2022-10-06 10:13:38 -05:00
parent 7c99eb3b88
commit 50d81932d2
4 changed files with 233 additions and 134 deletions

7
data/wp-oauth-me.json Normal file
View File

@ -0,0 +1,7 @@
{
"access_token": "x39tb1rws4gcpsyvahkofrj7xwnrefxuk3jonrjn",
"expires_in": 3600,
"refresh_token": "bo85pscgut9nqy56snnzgw6ixljzspac3f74eemy",
"scope": "basic profile email",
"token_type": "Bearer"
}

View File

@ -11,10 +11,12 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
HERE = os.path.abspath(os.path.dirname(__file__)) HERE = os.path.abspath(os.path.dirname(__file__))
def load_readme(): def load_readme():
with io.open(os.path.join(HERE, "README.rst"), "rt", encoding="utf8") as f: with io.open(os.path.join(HERE, "README.rst"), "rt", encoding="utf8") as f:
return f.read() return f.read()
def load_about(): def load_about():
about = {} about = {}
with io.open( with io.open(
@ -25,6 +27,7 @@ def load_about():
exec(f.read(), about) # pylint: disable=exec-used exec(f.read(), about) # pylint: disable=exec-used
return about return about
def load_requirements(*requirements_paths): def load_requirements(*requirements_paths):
""" """
Load all requirements from the specified requirements files. Load all requirements from the specified requirements files.
@ -34,7 +37,9 @@ def load_requirements(*requirements_paths):
requirements = set() requirements = set()
for path in requirements_paths: for path in requirements_paths:
requirements.update( requirements.update(
line.split("#")[0].strip() for line in open(path).readlines() if is_requirement(line.strip()) line.split("#")[0].strip()
for line in open(path).readlines()
if is_requirement(line.strip())
) )
return list(requirements) return list(requirements)
@ -54,19 +59,22 @@ def is_requirement(line):
or line.startswith("git+") or line.startswith("git+")
) )
README = load_readme() README = load_readme()
ABOUT = load_about() ABOUT = load_about()
VERSION = ABOUT["__version__"] VERSION = ABOUT["__version__"]
setup( setup(
name='wp-oauth-backend', name="wp-oauth-backend",
version=VERSION, version=VERSION,
description=('An OAuth backend for the WP OAuth Wordpress Plugin, ' description=(
'that is customized for use in Open edX installations.'), "An OAuth backend for the WP OAuth Wordpress Plugin, "
"that is customized for use in Open edX installations."
),
long_description=README, long_description=README,
author='Lawrence McDaniel, lpm0073@gmail.com', author="Lawrence McDaniel, lpm0073@gmail.com",
author_email='lpm0073@gmail.com', author_email="lpm0073@gmail.com",
url='https://github.com/StepwiseMath/wp-oauth-backend', url="https://github.com/StepwiseMath/wp-oauth-backend",
project_urls={ project_urls={
"Code": "https://github.com/StepwiseMath/wp-oauth-backend", "Code": "https://github.com/StepwiseMath/wp-oauth-backend",
"Issue tracker": "https://github.com/StepwiseMath/wp-oauth-backend/issues", "Issue tracker": "https://github.com/StepwiseMath/wp-oauth-backend/issues",
@ -76,11 +84,11 @@ setup(
include_package_data=True, include_package_data=True,
package_data={"": ["*.html"]}, # include any templates found in this repo. package_data={"": ["*.html"]}, # include any templates found in this repo.
zip_safe=False, zip_safe=False,
keywords='WP OAuth', keywords="WP OAuth",
python_requires=">=3.7", python_requires=">=3.7",
install_requires=load_requirements("requirements/stable-psa.txt"), install_requires=load_requirements("requirements/stable-psa.txt"),
classifiers=[ classifiers=[
'Intended Audience :: Developers', "Intended Audience :: Developers",
'License :: OSI Approved :: MIT License', "License :: OSI Approved :: MIT License",
], ],
) )

View File

@ -1 +1 @@
__version__ = '0.1.0' __version__ = "0.1.0"

View File

@ -21,6 +21,8 @@ User = get_user_model()
logger = getLogger(__name__) logger = getLogger(__name__)
VERBOSE_LOGGING = True VERBOSE_LOGGING = True
class StepwiseMathWPOAuth2(BaseOAuth2): class StepwiseMathWPOAuth2(BaseOAuth2):
""" """
WP OAuth authentication backend customized for Open edX. WP OAuth authentication backend customized for Open edX.
@ -29,73 +31,74 @@ class StepwiseMathWPOAuth2(BaseOAuth2):
Notes: Notes:
- Python Social Auth social_core and/or Open edX's third party authentication core - Python Social Auth social_core and/or Open edX's third party authentication core
are finicky about how the "properties" are implemented. Anything that actually are finicky about how the "properties" are implemented. Anything that actually
declared as a Python class variable needs to remain a Python class variable. declared as a Python class variable needs to remain a Python class variable.
DO NOT refactor these into formal Python properties as something upstream will DO NOT refactor these into formal Python properties as something upstream will
break your code. break your code.
- for some reason adding an __init__() def to this class also causes something - for some reason adding an __init__() def to this class also causes something
upstream to break. If you try this then you'll get an error about a missing upstream to break. If you try this then you'll get an error about a missing
positional argument, 'strategy'. positional argument, 'strategy'.
""" """
_user_details = None _user_details = None
# This defines the backend name and identifies it during the auth process. # This defines the backend name and identifies it during the auth process.
# The name is used in the URLs /login/<backend name> and /complete/<backend name>. # The name is used in the URLs /login/<backend name> and /complete/<backend name>.
# #
# This is the string value that will appear in the LMS Django Admin # This is the string value that will appear in the LMS Django Admin
# Third Party Authentication / Provider Configuration (OAuth) # Third Party Authentication / Provider Configuration (OAuth)
# setup page drop-down box titled, "Backend name:", just above # setup page drop-down box titled, "Backend name:", just above
# the "Client ID:" and "Client Secret:" fields. # the "Client ID:" and "Client Secret:" fields.
name = 'stepwisemath-oauth' name = "stepwisemath-oauth"
# note: no slash at the end of the base url. Python Social Auth # note: no slash at the end of the base url. Python Social Auth
# might clean this up for you, but i'm not 100% certain of that. # might clean this up for you, but i'm not 100% certain of that.
BASE_URL = "https://stepwisemath.ai" BASE_URL = "https://stepwisemath.ai"
# The default key name where the user identification field is defined, its # The default key name where the user identification field is defined, its
# used in the auth process when some basic user data is returned. This Id # used in the auth process when some basic user data is returned. This Id
# is stored in the UserSocialAuth.uid field and this, together with the # is stored in the UserSocialAuth.uid field and this, together with the
# UserSocialAuth.provider field, is used to uniquely identify a user association. # UserSocialAuth.provider field, is used to uniquely identify a user association.
ID_KEY = 'id' ID_KEY = "id"
# Flags the backend to enforce email validation during the pipeline # Flags the backend to enforce email validation during the pipeline
# (if the corresponding pipeline social_core.pipeline.mail.mail_validation was enabled). # (if the corresponding pipeline social_core.pipeline.mail.mail_validation was enabled).
REQUIRES_EMAIL_VALIDATION = False REQUIRES_EMAIL_VALIDATION = False
# Some providers give nothing about the user but some basic data like the # Some providers give nothing about the user but some basic data like the
# user Id or an email address. The default scope attribute is used to # user Id or an email address. The default scope attribute is used to
# specify a default value for the scope argument to request those extra bits. # specify a default value for the scope argument to request those extra bits.
# #
# wp-oauth supports 4 scopes: basic, email, profile, openeid. # wp-oauth supports 4 scopes: basic, email, profile, openeid.
# we want the first three of these. # we want the first three of these.
# see https://wp-oauth.com/docs/how-to/adding-supported-scopes/ # see https://wp-oauth.com/docs/how-to/adding-supported-scopes/
DEFAULT_SCOPE = ['basic', 'profile', 'email'] DEFAULT_SCOPE = ["basic", "profile", "email"]
# Specifying the method type required to retrieve your access token if its # Specifying the method type required to retrieve your access token if its
# not the default GET request. # not the default GET request.
ACCESS_TOKEN_METHOD = 'POST' ACCESS_TOKEN_METHOD = "POST"
# require redirect domain to match the original initiating domain. # require redirect domain to match the original initiating domain.
SOCIAL_AUTH_SANITIZE_REDIRECTS = True SOCIAL_AUTH_SANITIZE_REDIRECTS = True
# During the auth process some basic user data is returned by the provider # During the auth process some basic user data is returned by the provider
# or retrieved by the user_data() method which usually is used to call # or retrieved by the user_data() method which usually is used to call
# some API on the provider to retrieve it. This data will be stored in the # some API on the provider to retrieve it. This data will be stored in the
# UserSocialAuth.extra_data attribute, but to make it accessible under some # UserSocialAuth.extra_data attribute, but to make it accessible under some
# common names on different providers, this attribute defines a list of # common names on different providers, this attribute defines a list of
# tuples in the form (name, alias) where name is the key in the user data # tuples in the form (name, alias) where name is the key in the user data
# (which should be a dict instance) and alias is the name to store it on extra_data. # (which should be a dict instance) and alias is the name to store it on extra_data.
EXTRA_DATA = [ EXTRA_DATA = [
('id', 'id'), ("id", "id"),
('is_superuser', 'is_superuser'), ("is_superuser", "is_superuser"),
('is_staff', 'is_staff'), ("is_staff", "is_staff"),
('date_joined', 'date_joined'), ("date_joined", "date_joined"),
] ]
# the value of the scope separator is user-defined. Check the # the value of the scope separator is user-defined. Check the
# scopes field value for your oauth client in your wordpress host. # scopes field value for your oauth client in your wordpress host.
# the wp-oauth default value for scopes is 'basic' but can be # the wp-oauth default value for scopes is 'basic' but can be
# changed to a list. example 'basic, email, profile'. This # changed to a list. example 'basic, email, profile'. This
# list can be delimited with commas, spaces, whatever. # list can be delimited with commas, spaces, whatever.
SCOPE_SEPARATOR = " " SCOPE_SEPARATOR = " "
@ -108,40 +111,70 @@ class StepwiseMathWPOAuth2(BaseOAuth2):
def is_valid_user_details(self, response) -> bool: def is_valid_user_details(self, response) -> bool:
""" """
validate that the object passed is a dict containing at least the keys validate that the object passed is a dict containing at least the keys
in qc_keys. in qc_keys.
""" """
if not type(response) == dict: if not type(response) == dict:
logger.warning('is_valid_user_details() was expecting a dict but received an object of type: {type}'.format( logger.warning(
type=type(response) "is_valid_user_details() was expecting a dict but received an object of type: {type}".format(
)) type=type(response)
)
)
return False return False
qc_keys = ['id', 'date_joined', 'email', 'first_name', 'fullname', 'is_staff', 'is_superuser', 'last_name', 'username'] qc_keys = [
if all(key in response for key in qc_keys): return True "id",
"date_joined",
"email",
"first_name",
"fullname",
"is_staff",
"is_superuser",
"last_name",
"username",
]
if all(key in response for key in qc_keys):
return True
return False return False
def is_wp_oauth_response(self, response) -> bool: def is_wp_oauth_response(self, response) -> bool:
""" """
validate the structure of the response object from wp-oauth. it's validate the structure of the response object from wp-oauth. it's
supposed to be a dict with at least the keys included in qc_keys. supposed to be a dict with at least the keys included in qc_keys.
""" """
if not type(response) == dict: if not type(response) == dict:
logger.warning('is_valid_user_details() was expecting a dict but received an object of type: {type}'.format( logger.warning(
type=type(response) "is_valid_user_details() was expecting a dict but received an object of type: {type}".format(
)) type=type(response)
)
)
return False return False
qc_keys = ['ID' 'display_name', 'user_email', 'user_login', 'user_roles'] qc_keys = ["ID" "display_name", "user_email", "user_login", "user_roles"]
if all(key in response for key in qc_keys): return True if all(key in response for key in qc_keys):
return True
return False return False
def is_wp_oauth_extended_response(self, response) -> bool: def is_wp_oauth_refresh_token_response(self, response) -> bool:
""" """
validate the structure of the extended response object from wp-oauth. it's validate that the structure of the response contains the keys of
supposed to be a dict with at least the keys included in qc_keys. a refresh token dict.
""" """
if not self.is_valid_user_details(response): return False if not self.is_valid_user_details(response):
qc_keys = ['access_token' 'expires_in', 'refresh_token', 'scope', 'token_type'] return False
if all(key in response for key in qc_keys): return True qc_keys = ["access_token" "expires_in", "refresh_token", "scope", "token_type"]
if all(key in response for key in qc_keys):
return True
return False
def is_get_user_details_extended_dict(self, response) -> bool:
"""
validate whether the structure the response is a dict that
contains a.) all keys of a get_user_details() return, plus,
b.) all keys of a wp-oauth refresh token response.
"""
if not self.is_valid_user_details(response):
return False
if self.is_wp_oauth_refresh_token_response(response):
return True
return False return False
# override Python Social Auth default end points. # override Python Social Auth default end points.
@ -153,21 +186,21 @@ class StepwiseMathWPOAuth2(BaseOAuth2):
def AUTHORIZATION_URL(self) -> str: def AUTHORIZATION_URL(self) -> str:
retval = f"{self.BASE_URL}/oauth/authorize" retval = f"{self.BASE_URL}/oauth/authorize"
if VERBOSE_LOGGING: if VERBOSE_LOGGING:
logger.info('AUTHORIZATION_URL: {url}'.format(url=retval)) logger.info("AUTHORIZATION_URL: {url}".format(url=retval))
return retval return retval
@property @property
def ACCESS_TOKEN_URL(self) -> str: def ACCESS_TOKEN_URL(self) -> str:
retval = f"{self.BASE_URL}/oauth/token" retval = f"{self.BASE_URL}/oauth/token"
if VERBOSE_LOGGING: if VERBOSE_LOGGING:
logger.info('ACCESS_TOKEN_URL: {url}'.format(url=retval)) logger.info("ACCESS_TOKEN_URL: {url}".format(url=retval))
return retval return retval
@property @property
def USER_QUERY(self) -> str: def USER_QUERY(self) -> str:
retval = f"{self.BASE_URL}/oauth/me" retval = f"{self.BASE_URL}/oauth/me"
if VERBOSE_LOGGING: if VERBOSE_LOGGING:
logger.info('USER_QUERY: {url}'.format(url=retval)) logger.info("USER_QUERY: {url}".format(url=retval))
return retval return retval
@property @property
@ -178,124 +211,163 @@ class StepwiseMathWPOAuth2(BaseOAuth2):
def user_details(self, value: dict): def user_details(self, value: dict):
if self.is_valid_user_details(value): if self.is_valid_user_details(value):
if VERBOSE_LOGGING: if VERBOSE_LOGGING:
logger.info('user_details.setter: new value set {value}'.format( logger.info(
value=json.dumps(value, sort_keys=True, indent=4) "user_details.setter: new value set {value}".format(
)) value=json.dumps(value, sort_keys=True, indent=4)
)
)
self._user_details = value self._user_details = value
else: else:
logger.error('user_details.setter: tried to pass an invalid object {value}'.format( logger.error(
value=json.dumps(value, sort_keys=True, indent=4) "user_details.setter: tried to pass an invalid object {value}".format(
)) value=json.dumps(value, sort_keys=True, indent=4)
)
)
# see https://python-social-auth.readthedocs.io/en/latest/backends/implementation.html # see https://python-social-auth.readthedocs.io/en/latest/backends/implementation.html
# Return user details from the Wordpress user account # Return user details from the Wordpress user account
def get_user_details(self, response) -> dict: def get_user_details(self, response) -> dict:
if not (self.is_valid_user_details(response) or self.is_wp_oauth_response(response)): if not (
logger.error('get_user_details() - received an unrecognized response object. Cannot continue: {response}'.format( self.is_valid_user_details(response) or self.is_wp_oauth_response(response)
response=json.dumps(response, sort_keys=True, indent=4) ):
)) logger.error(
"get_user_details() - received an unrecognized response object. Cannot continue: {response}".format(
response=json.dumps(response, sort_keys=True, indent=4)
)
)
# if we have cached results then we might be able to recover. # if we have cached results then we might be able to recover.
return self.user_details return self.user_details
if VERBOSE_LOGGING: logger.info('get_user_details() begin with response: {response}'.format( if VERBOSE_LOGGING:
response=json.dumps(response, sort_keys=True, indent=4) logger.info(
)) "get_user_details() begin with response: {response}".format(
response=json.dumps(response, sort_keys=True, indent=4)
)
)
# a def in the third_party_auth pipeline list calls get_user_details() after its already # a def in the third_party_auth pipeline list calls get_user_details() after its already
# been called once. i don't know why. but, it passes the original get_user_details() dict # been called once. i don't know why. but, it passes the original get_user_details() dict
# enhanced with additional token-related keys. if we receive this modified dict then we # enhanced with additional token-related keys. if we receive this modified dict then we
# should pass it along to the next defs in the pipeline. # should pass it along to the next defs in the pipeline.
# #
# If most of the original keys (see dict definition below) exist in the response object # If most of the original keys (see dict definition below) exist in the response object
# then we can assume that this is our case. # then we can assume that this is our case.
if self.is_wp_oauth_extended_response(response): if self.is_get_user_details_extended_dict(response):
# ------------------------------------------------------------- # -------------------------------------------------------------
# expected use case #2: an enhanced derivation of an original # expected use case #2: an enhanced derivation of an original
# user_details dict. This is created when get_user_details() # user_details dict. This is created when get_user_details()
# is called from user_data(). # is called from user_data().
# ------------------------------------------------------------- # -------------------------------------------------------------
if VERBOSE_LOGGING: if VERBOSE_LOGGING:
logger.info('get_user_details() - detected an enhanced get_user_details() dict in the response: {response}'.format( logger.info(
response=json.dumps(response, sort_keys=True, indent=4) "get_user_details() - detected an enhanced get_user_details() dict in the response: {response}".format(
)) response=json.dumps(response, sort_keys=True, indent=4)
)
)
return response return response
# at this point we've ruled out the possibility of the response object # at this point we've ruled out the possibility of the response object
# being a derivation of a user_details dict. So, it should therefore # being a derivation of a user_details dict. So, it should therefore
# conform to the structure of a wp-oauth dict. # conform to the structure of a wp-oauth dict.
if not self.is_wp_oauth_response(response): if not self.is_wp_oauth_response(response):
logger.warning('get_user_details() - response object is not a valid wp-oauth object. Cannot continue. {response}'.format( logger.warning(
response=json.dumps(response, sort_keys=True, indent=4) "get_user_details() - response object is not a valid wp-oauth object. Cannot continue. {response}".format(
)) response=json.dumps(response, sort_keys=True, indent=4)
)
)
return self.user_details return self.user_details
# ------------------------------------------------------------- # -------------------------------------------------------------
# expected use case #1: response object is a dict with all required keys. # expected use case #1: response object is a dict with all required keys.
# ------------------------------------------------------------- # -------------------------------------------------------------
if VERBOSE_LOGGING: if VERBOSE_LOGGING:
logger.info('get_user_details() - start. response: {response}'.format( logger.info(
response=json.dumps(response, sort_keys=True, indent=4) "get_user_details() - start. response: {response}".format(
)) response=json.dumps(response, sort_keys=True, indent=4)
)
)
# try to parse out the first and last names # try to parse out the first and last names
split_name = response.get('display_name', '').split() split_name = response.get("display_name", "").split()
first_name = split_name[0] if len(split_name) > 0 else '' first_name = split_name[0] if len(split_name) > 0 else ""
last_name = split_name[-1] if len(split_name) == 2 else '' last_name = split_name[-1] if len(split_name) == 2 else ""
# check for superuser / staff status # check for superuser / staff status
user_roles = response.get('user_roles', []) user_roles = response.get("user_roles", [])
super_user = 'administrator' in user_roles super_user = "administrator" in user_roles
is_staff = 'administrator' in user_roles is_staff = "administrator" in user_roles
self.user_details = { self.user_details = {
'id': int(response.get('ID'), 0), "id": int(response.get("ID"), 0),
'username': response.get('user_login', ''), "username": response.get("user_login", ""),
'email': response.get('user_email', ''), "email": response.get("user_email", ""),
'first_name': first_name, "first_name": first_name,
'last_name': last_name, "last_name": last_name,
'fullname': response.get('display_name', ''), "fullname": response.get("display_name", ""),
'is_superuser': super_user, "is_superuser": super_user,
'is_staff': is_staff, "is_staff": is_staff,
'refresh_token': response.get('refresh_token', ''), "refresh_token": response.get("refresh_token", ""),
'scope': response.get('scope', ''), "scope": response.get("scope", ""),
'token_type': response.get('token_type', ''), "token_type": response.get("token_type", ""),
'date_joined': response.get('user_registered', ''), "date_joined": response.get("user_registered", ""),
'user_status': response.get('user_status', ''), "user_status": response.get("user_status", ""),
} }
if VERBOSE_LOGGING: if VERBOSE_LOGGING:
logger.info('get_user_details() - finish. user_details: {user_details}'.format( logger.info(
user_details=json.dumps(self.user_details, sort_keys=True, indent=4) "get_user_details() - finish. user_details: {user_details}".format(
)) user_details=json.dumps(self.user_details, sort_keys=True, indent=4)
)
)
return self.user_details return self.user_details
# Load user data from service url end point. Note that in the case of # Load user data from service url end point. Note that in the case of
# wp oauth, the response object returned by self.USER_QUERY # wp oauth, the response object returned by self.USER_QUERY
# is the same as the response object passed to get_user_details(). # is the same as the response object passed to get_user_details().
# #
# see https://python-social-auth.readthedocs.io/en/latest/backends/implementation.html # see https://python-social-auth.readthedocs.io/en/latest/backends/implementation.html
def user_data(self, access_token, *args, **kwargs) -> dict: def user_data(self, access_token, *args, **kwargs) -> dict:
response = None
url = f'{self.USER_QUERY}?' + urlencode({ user_details = None
'access_token': access_token url = f"{self.USER_QUERY}?" + urlencode({"access_token": access_token})
})
if VERBOSE_LOGGING: if VERBOSE_LOGGING:
logger.info("user_data() url: {url}".format(url=url)) logger.info("user_data() url: {url}".format(url=url))
try: try:
response = json.loads(self._urlopen(url)) response = json.loads(self._urlopen(url))
if VERBOSE_LOGGING:
logger.info(
"user_data() response: {response}".format(
response=json.dumps(self.user_details, sort_keys=True, indent=4)
)
)
user_details = self.get_user_details(response)
if VERBOSE_LOGGING:
logger.info(
"user_data() local variable user_details: {user_details}".format(
user_details=json.dumps(user_details, sort_keys=True, indent=4)
)
)
if VERBOSE_LOGGING:
logger.info(
"user_data() class property value of self.user_details: {user_details}".format(
user_details=json.dumps(
self.user_details, sort_keys=True, indent=4
)
)
)
except ValueError as e: except ValueError as e:
logger.error('user_data() {err}'.format(err=e)) logger.error("user_data() {err}".format(err=e))
return None return None
if not self.is_valid_user_details(response): if not self.is_valid_user_details(user_details):
logger.error('user_data() response object is invalid: {response}'.format( logger.error(
response=json.dumps(self.user_details, sort_keys=True, indent=4) "user_data() user_details object is invalid: {user_details}".format(
)) user_details=json.dumps(user_details, sort_keys=True, indent=4)
)
)
return self.user_details return self.user_details
# refresh our internal user_details property after having validated
# response from USER_QUERY
self.get_user_details(response)
# add syncronization of any data fields that get missed by the built-in # add syncronization of any data fields that get missed by the built-in
# open edx third party authentication sync functionality. # open edx third party authentication sync functionality.
@ -303,20 +375,32 @@ class StepwiseMathWPOAuth2(BaseOAuth2):
# this gets called just prior to account creation for # this gets called just prior to account creation for
# new users, hence, we need to catch DoesNotExist # new users, hence, we need to catch DoesNotExist
# exceptions. # exceptions.
user=User.objects.get(username=self.user_details['username']) user = User.objects.get(username=self.user_details["username"])
except User.DoesNotExist: except User.DoesNotExist:
return self.user_details return self.user_details
if (user.is_superuser != self.user_details['is_superuser']) or (user.is_staff != self.user_details['is_staff']): if (user.is_superuser != self.user_details["is_superuser"]) or (
user.is_superuser = self.user_details['is_superuser'] user.is_staff != self.user_details["is_staff"]
user.is_staff = self.user_details['is_staff'] ):
user.is_superuser = self.user_details["is_superuser"]
user.is_staff = self.user_details["is_staff"]
user.save() user.save()
logger.info('Updated the is_superuser/is_staff flags for user {username}'.format(username=user.username)) logger.info(
"Updated the is_superuser/is_staff flags for user {username}".format(
username=user.username
)
)
if (user.first_name != self.user_details['first_name']) or (user.last_name != self.user_details['last_name']): if (user.first_name != self.user_details["first_name"]) or (
user.first_name = self.user_details['first_name'] user.last_name != self.user_details["last_name"]
user.last_name = self.user_details['last_name'] ):
user.first_name = self.user_details["first_name"]
user.last_name = self.user_details["last_name"]
user.save() user.save()
logger.info('Updated first_name/last_name for user {username}'.format(username=user.username)) logger.info(
"Updated first_name/last_name for user {username}".format(
username=user.username
)
)
return self.user_details return self.user_details