Skip to content

Commit c76e9fb

Browse files
committed
refactor(socialaccount): Uniform SocialAccount.to_str()
Squashed commit of the following: commit b23a8a7 Author: David D Lowe <[email protected]> Date: Fri Jul 19 14:24:06 2024 +0100 fix(socialaccount): Make base to_str method look at more keys commit 90c8b48 Author: David D Lowe <[email protected]> Date: Fri Jul 19 14:11:56 2024 +0100 fix(socialaccount): make QuickBooks' to_str ignore emailVerified commit 7651401 Author: David D Lowe <[email protected]> Date: Fri Jul 19 14:02:58 2024 +0100 fix(socialaccount): Make base to_str more resilient It can now handle extra_data values that are not dictionaries without throwing an exception, and it only returns an `email`, `username` and `name` value if it is a string. commit 34576ba Author: David D Lowe <[email protected]> Date: Fri Jul 19 13:56:23 2024 +0100 fix(socialaccount): Fully store extra_data of Edmodo account commit c71090d Author: David D Lowe <[email protected]> Date: Fri Jul 19 13:55:12 2024 +0100 fix(socialaccount): Fully store extra_data of Amazon Cognito account commit 3fdca2e Author: David D Lowe <[email protected]> Date: Tue Jul 16 14:25:16 2024 +0100 fix(socialaccount): Ensure AppleAccount's to_str handles private email commit 0ea8b75 Author: David D Lowe <[email protected]> Date: Tue Jul 16 14:15:43 2024 +0100 fix: Use f-string commit 6e449db Author: David D Lowe <[email protected]> Date: Tue Jul 16 14:04:02 2024 +0100 fix(socialaccount): Delete superflous to_str method implementations These classes are all subclasses of ProviderAccount, which provides a default to_str implementation. If the subclass's to_str implementation does not differ enough from the to_str method of its parent class, then delete it. commit 5b58d17 Author: David D Lowe <[email protected]> Date: Wed Jul 3 16:04:50 2024 +0100 fix(socialaccount): make to_str return unique ID The view allauth.socialaccount.views.connections displays a list of social accounts to the user. Instead of using the display name of a social account, which may not be unique, use the unique username or email address instead. That way, accounts with identical display names can be distinguished from each other. Fixes #3906 commit bc78638 Author: David D Lowe <[email protected]> Date: Tue Jul 2 06:27:36 2024 +0100 fix(socialaccount): store email and name in extra_data All providers should store email, first name and last name in the SocialAccount's extra_data field, if possible. It is not enough to store this information in the User instance, as there may be multiple social accounts associated with that user, with differing email addresses, first names and last names. Most providers already implemented this functionality. This commit fixes AmazonCognito, Edmodo and MediaWiki.
1 parent b13cef1 commit c76e9fb

File tree

213 files changed

+619
-419
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

213 files changed

+619
-419
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Daniel Eriksson
4848
Daniel Widerin
4949
David Ascher
5050
David Cain
51+
David D. Lowe
5152
David Evans
5253
David Friedman
5354
David Hummel

ChangeLog.rst

+10
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ Note worthy changes
2323
- Headless: You can now alter the user data payload by overriding the newly
2424
introduced ``serialize_user()`` adapter method.
2525

26+
- Ensured that email address, given name and family name fields are stored in
27+
the SocialAccount instance. This information was not previously saved in
28+
Amazon Cognito, Edmodo, and MediaWiki SocialAccount instances.
29+
30+
- When multiple third-party accounts of the same provider were connected, the
31+
third-party account connections overview did not always provide a clear
32+
recognizable distinction between those accounts. Now, the
33+
``SocialAccount.__str__()`` has been altered to return the unique username or
34+
email address, rather than a non-unique display name.
35+
2636

2737
Backwards incompatible changes
2838
------------------------------

allauth/socialaccount/providers/agave/provider.py

-4
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@ def get_profile_url(self):
1010
def get_avatar_url(self):
1111
return self.account.extra_data.get("avatar_url", "dflt")
1212

13-
def to_str(self):
14-
dflt = super(AgaveAccount, self).to_str()
15-
return self.account.extra_data.get("name", dflt)
16-
1713

1814
class AgaveProvider(OAuth2Provider):
1915
id = "agave"

allauth/socialaccount/providers/agave/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,6 @@ def get_mocked_response(self):
2929
}
3030
""",
3131
)
32+
33+
def get_expected_to_str(self):
34+
return "jdoe"

allauth/socialaccount/providers/amazon/provider.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55

66
class AmazonAccount(ProviderAccount):
7-
def to_str(self):
8-
return self.account.extra_data.get("name", super(AmazonAccount, self).to_str())
7+
pass
98

109

1110
class AmazonProvider(OAuth2Provider):

allauth/socialaccount/providers/amazon/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ def get_mocked_response(self):
1919
}
2020
}""",
2121
)
22+
23+
def get_expected_to_str(self):
24+

allauth/socialaccount/providers/amazon_cognito/provider.py

+9-22
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@
1010

1111

1212
class AmazonCognitoAccount(ProviderAccount):
13-
def to_str(self):
14-
dflt = super(AmazonCognitoAccount, self).to_str()
15-
16-
return self.account.extra_data.get("username", dflt)
17-
1813
def get_avatar_url(self):
1914
return self.account.extra_data.get("picture")
2015

@@ -54,23 +49,15 @@ def extract_email_addresses(self, data):
5449
)
5550

5651
def extract_extra_data(self, data):
57-
return {
58-
"address": data.get("address"),
59-
"birthdate": data.get("birthdate"),
60-
"gender": data.get("gender"),
61-
"locale": data.get("locale"),
62-
"middlename": data.get("middlename"),
63-
"nickname": data.get("nickname"),
64-
"phone_number": data.get("phone_number"),
65-
"phone_number_verified": convert_to_python_bool_if_value_is_json_string_bool(
66-
data.get("phone_number_verified")
67-
),
68-
"picture": data.get("picture"),
69-
"preferred_username": data.get("preferred_username"),
70-
"profile": data.get("profile"),
71-
"website": data.get("website"),
72-
"zoneinfo": data.get("zoneinfo"),
73-
}
52+
ret = dict(data)
53+
phone_number_verified = data.get("phone_number_verified")
54+
if phone_number_verified is not None:
55+
ret["phone_number_verified"] = (
56+
convert_to_python_bool_if_value_is_json_string_bool(
57+
"phone_number_verified"
58+
)
59+
)
60+
return ret
7461

7562
@classmethod
7663
def get_slug(cls):

allauth/socialaccount/providers/amazon_cognito/tests.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ class AmazonCognitoTestCase(OAuth2TestsMixin, TestCase):
3939

4040
def get_mocked_response(self):
4141
mocked_payload = json.dumps(_get_mocked_claims())
42-
4342
return MockedResponse(status_code=200, content=mocked_payload)
4443

44+
def get_expected_to_str(self):
45+
return "johndoe"
46+
4547
@override_settings(SOCIALACCOUNT_PROVIDERS={"amazon_cognito": {}})
4648
def test_oauth2_adapter_raises_if_domain_settings_is_missing(
4749
self,

allauth/socialaccount/providers/angellist/provider.py

-4
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ def get_profile_url(self):
1212
def get_avatar_url(self):
1313
return self.account.extra_data.get("image")
1414

15-
def to_str(self):
16-
dflt = super(AngelListAccount, self).to_str()
17-
return self.account.extra_data.get("name", dflt)
18-
1915

2016
class AngelListProvider(OAuth2Provider):
2117
id = "angellist"

allauth/socialaccount/providers/angellist/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ def get_mocked_response(self):
2323
"email"]}
2424
""",
2525
)
26+
27+
def get_expected_to_str(self):
28+

allauth/socialaccount/providers/apple/provider.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,26 @@
99
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
1010

1111

12+
class AppleAccount(ProviderAccount):
13+
def to_str(self):
14+
email = self.account.extra_data.get("email")
15+
if email and not email.lower().endswith("@privaterelay.appleid.com"):
16+
return email
17+
18+
name = self.account.extra_data.get("name") or {}
19+
if name.get("firstName") or name.get("lastName"):
20+
full_name = f"{name['firstName'] or ''} {name['lastName'] or ''}"
21+
full_name = full_name.strip()
22+
if full_name:
23+
return full_name
24+
25+
return super().to_str()
26+
27+
1228
class AppleProvider(OAuth2Provider):
1329
id = "apple"
1430
name = "Apple"
15-
account_class = ProviderAccount
31+
account_class = AppleAccount
1632
oauth2_adapter_class = AppleOAuth2Adapter
1733
supports_token_authentication = True
1834

allauth/socialaccount/providers/apple/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ def get_mocked_response(self):
170170
200, KEY_SERVER_RESP_JSON, {"content-type": "application/json"}
171171
)
172172

173+
def get_expected_to_str(self):
174+
return "A B"
175+
173176
def get_complete_parameters(self, auth_request_params):
174177
"""
175178
Add apple specific response parameters which they include in the

allauth/socialaccount/providers/asana/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ def get_mocked_response(self):
1515
{"id": 3133777, "name": "Personal Projects"}], "email": "[email protected]",
1616
"name": "Test Name", "id": 43748387}}""",
1717
)
18+
19+
def get_expected_to_str(self):
20+

allauth/socialaccount/providers/atlassian/provider.py

-4
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@ class AtlassianAccount(ProviderAccount):
88
def get_profile_url(self):
99
return self.account.extra_data.get("picture")
1010

11-
def to_str(self):
12-
dflt = super().to_str()
13-
return self.account.extra_data.get("name", dflt)
14-
1511

1612
class AtlassianProvider(OAuth2Provider):
1713
id = "atlassian"

allauth/socialaccount/providers/atlassian/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ def get_mocked_response(self):
2828
}
2929
}"""
3030
return MockedResponse(200, response_data)
31+
32+
def get_expected_to_str(self):
33+

allauth/socialaccount/providers/auth0/provider.py

-4
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ class Auth0Account(ProviderAccount):
77
def get_avatar_url(self):
88
return self.account.extra_data.get("picture")
99

10-
def to_str(self):
11-
dflt = super(Auth0Account, self).to_str()
12-
return self.account.extra_data.get("name", dflt)
13-
1410

1511
class Auth0Provider(OAuth2Provider):
1612
id = "auth0"

allauth/socialaccount/providers/auth0/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ def get_mocked_response(self):
2020
}
2121
""",
2222
)
23+
24+
def get_expected_to_str(self):
25+

allauth/socialaccount/providers/authentiq/provider.py

-4
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,6 @@ def get_profile_url(self):
5050
def get_avatar_url(self):
5151
return self.account.extra_data.get("picture")
5252

53-
def to_str(self):
54-
dflt = super(AuthentiqAccount, self).to_str()
55-
return self.account.extra_data.get("name", dflt)
56-
5753

5854
class AuthentiqProvider(OAuth2Provider):
5955
id = "authentiq"

allauth/socialaccount/providers/authentiq/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ def get_mocked_response(self):
2727
),
2828
)
2929

30+
def get_expected_to_str(self):
31+
32+
3033
@override_settings(
3134
SOCIALACCOUNT_QUERY_EMAIL=False,
3235
)

allauth/socialaccount/providers/baidu/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ def get_mocked_response(self):
1414
{"portrait": "78c0e9839de59bbde7859ccf43",
1515
"uname": "\u90dd\u56fd\u715c", "uid": "3225892368"}""",
1616
)
17+
18+
def get_expected_to_str(self):
19+
return "\u90dd\u56fd\u715c"

allauth/socialaccount/providers/base/provider.py

+92-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import Dict, Optional
22

33
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
44

@@ -144,7 +144,8 @@ def extract_uid(self, data):
144144
def extract_extra_data(self, data):
145145
"""
146146
Extracts fields from `data` that will be stored in
147-
`SocialAccount`'s `extra_data` JSONField.
147+
`SocialAccount`'s `extra_data` JSONField, such as email address, first
148+
name, last name, and phone number.
148149
149150
:return: any JSON-serializable Python structure.
150151
"""
@@ -249,9 +250,31 @@ def get_brand(self):
249250
def __str__(self):
250251
return self.to_str()
251252

253+
def get_user_data(self) -> Optional[Dict]:
254+
"""Typically, the ``extra_data`` directly contains user related keys.
255+
For some providers, however, they are nested below a different key. In
256+
that case, you can override this method so that the base ``__str__()``
257+
will still be able to find the data.
258+
"""
259+
ret = self.account.extra_data
260+
if not isinstance(ret, dict):
261+
ret = None
262+
return ret
263+
252264
def to_str(self):
253265
"""
254-
This did not use to work in the past due to py2 compatibility:
266+
Returns string representation of this social account. This is the
267+
unique identifier of the account, such as its username or its email
268+
address. It should be meaningful to human beings, which means a numeric
269+
ID number is rarely the appropriate representation here.
270+
271+
Subclasses are meant to override this method.
272+
273+
Users will see the string representation of their social accounts in
274+
the page rendered by the allauth.socialaccount.views.connections view.
275+
276+
The following code did not use to work in the past due to py2
277+
compatibility:
255278
256279
class GoogleAccount(ProviderAccount):
257280
def __str__(self):
@@ -261,4 +284,70 @@ def __str__(self):
261284
So we have this method `to_str` that can be overridden in a conventional
262285
fashion, without having to worry about it.
263286
"""
287+
user_data = self.get_user_data()
288+
if user_data:
289+
combi_values = {}
290+
tbl = [
291+
# Prefer username -- it's the most human recognizable & unique.
292+
(
293+
None,
294+
[
295+
"username",
296+
"userName",
297+
"user_name",
298+
"login",
299+
"handle",
300+
],
301+
),
302+
# Second best is email
303+
(None, ["email", "Email", "mail", "email_address"]),
304+
(
305+
None,
306+
[
307+
"name",
308+
"display_name",
309+
"displayName",
310+
"Display_Name",
311+
"nickname",
312+
],
313+
),
314+
# Use the full name
315+
(None, ["full_name", "fullName"]),
316+
# Alternatively, try to assemble a full name ourselves.
317+
(
318+
"first_name",
319+
[
320+
"first_name",
321+
"firstname",
322+
"firstName",
323+
"First_Name",
324+
"given_name",
325+
"givenName",
326+
],
327+
),
328+
(
329+
"last_name",
330+
[
331+
"last_name",
332+
"lastname",
333+
"lastName",
334+
"Last_Name",
335+
"family_name",
336+
"familyName",
337+
"surname",
338+
],
339+
),
340+
]
341+
for store_as, variants in tbl:
342+
for key in variants:
343+
value = user_data.get(key)
344+
if isinstance(value, str):
345+
value = value.strip()
346+
if value and not store_as:
347+
return value
348+
combi_values[store_as] = value
349+
first_name = combi_values.get("first_name") or ""
350+
last_name = combi_values.get("last_name") or ""
351+
if first_name or last_name:
352+
return f"{first_name} {last_name}".strip()
264353
return self.get_brand()["name"]

allauth/socialaccount/providers/basecamp/provider.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ class BasecampAccount(ProviderAccount):
99
def get_avatar_url(self):
1010
return None
1111

12-
def to_str(self):
13-
dflt = super(BasecampAccount, self).to_str()
14-
return self.account.extra_data.get("name", dflt)
12+
def get_user_data(self):
13+
return self.account.extra_data.get("identity", {})
1514

1615

1716
class BasecampProvider(OAuth2Provider):

allauth/socialaccount/providers/basecamp/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ def get_mocked_response(self):
4141
]
4242
}""",
4343
)
44+
45+
def get_expected_to_str(self):
46+

allauth/socialaccount/providers/battlenet/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ def get_mocked_response(self):
1818
data = {"battletag": self._battletag, "id": self._uid}
1919
return MockedResponse(200, json.dumps(data))
2020

21+
def get_expected_to_str(self):
22+
return self._battletag
23+
2124
def test_valid_response_no_battletag(self):
2225
data = {"id": 12345}
2326
response = MockedResponse(200, json.dumps(data))

0 commit comments

Comments
 (0)