Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prefer social account data over direct user data #1140

Merged
merged 5 commits into from
Jul 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions dandiapi/api/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class UserMetadataFactory(factory.django.DjangoModelFactory):
class Meta:
model = UserMetadata

status = UserMetadata.Status.APPROVED
status = UserMetadata.Status.APPROVED.value


class UserFactory(factory.django.DjangoModelFactory):
Expand All @@ -54,10 +54,20 @@ def extra_data(self):
first_name = self.user.first_name
last_name = self.user.last_name
name = f'{first_name} {last_name}'

# Supply a fake created date at least 1 year before now
created = (
faker.Faker()
.date_time_between(end_date=datetime.datetime.now() - datetime.timedelta(days=365))
.isoformat()
)

# Supply different values from User object, since social account values maybe be different
return {
'login': self.user.username,
'login': faker.Faker().user_name(),
'name': name,
'email': self.user.username,
'email': faker.Faker().ascii_email(),
'created_at': created,
}


Expand Down
26 changes: 25 additions & 1 deletion dandiapi/api/tests/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from dandiapi.api.mail import ADMIN_EMAIL
from dandiapi.api.models import UserMetadata
from dandiapi.api.views.auth import COLLECT_USER_NAME_QUESTIONS, NEW_USER_QUESTIONS, QUESTIONS
from dandiapi.api.views.users import user_to_dict


def serialize_social_account(social_account):
Expand Down Expand Up @@ -125,6 +126,27 @@ def test_user_search(api_client, social_account, social_account_factory):
).data == [serialize_social_account(social_account)]


@pytest.mark.django_db
def test_user_search_prefer_social(api_client, user_factory, social_account):
api_client.force_authenticate(user=social_account.user)

# Check that when social account is present, it is used
assert api_client.get(
'/api/users/search/?',
{'username': social_account.user.username},
format='json',
).data == [serialize_social_account(social_account)]

# Create user without a social account
user = user_factory()
api_client.force_authenticate(user=user)
assert api_client.get(
'/api/users/search/?',
{'username': user.username},
format='json',
).data == [user_to_dict(user)]


@pytest.mark.django_db
def test_user_search_blank_username(api_client, user):
api_client.force_authenticate(user=user)
Expand Down Expand Up @@ -188,7 +210,9 @@ def test_user_search_limit_enforced(api_client, user, user_factory, social_accou
'/api/users/search/?',
{'username': 'odysseus'},
format='json',
).data == [serialize_social_account(social_account) for social_account in social_accounts[:10]]
).json() == [
serialize_social_account(social_account) for social_account in social_accounts[:10]
]


@pytest.mark.django_db
Expand Down
45 changes: 31 additions & 14 deletions dandiapi/api/views/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from allauth.socialaccount.models import SocialAccount
from django.contrib.auth.models import User
from django.db.models import Q
from django.db.models import OuterRef, Q, Subquery
from django.http.response import HttpResponseBase
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import api_view, parser_classes, permission_classes
Expand All @@ -22,14 +22,10 @@


def _get_user_status(user: User):
# Workaround for https://github.com/dandi/dandi-archive/issues/1085
# TODO: remove/robustify some other way whenever underlying reason is
# identified
metadata = getattr(user, 'metadata', None)
if metadata:
return metadata.status
logger.error("User %s lacks .metadata. Returning user's status as INCOMPLETE", user)
return UserMetadata.Status.INCOMPLETE
try:
return user.metadata.status
except UserMetadata.DoesNotExist:
return UserMetadata.Status.INCOMPLETE.value


def user_to_dict(user: User):
Expand All @@ -43,7 +39,6 @@ def user_to_dict(user: User):
'username': user.username,
'name': f'{user.first_name} {user.last_name}'.strip(),
'status': _get_user_status(user),
'created': user.date_joined,
}


Expand All @@ -56,14 +51,30 @@ def social_account_to_dict(social_account: SocialAccount):
# We are assuming that login is a required field for GitHub users
username = social_account.extra_data['login']
name = social_account.extra_data.get('name') or name
created = social_account.extra_data.get('created_at')

return {
'admin': user.is_superuser,
'username': username,
'name': name,
'status': _get_user_status(user),
'created': created,
}


def serialize_user(user: User):
"""Serialize a user that's been annotated with a `social_account_data` field."""
username = user.username
name = f'{user.first_name} {user.last_name}'.strip()

# Prefer social account info if present
if user.social_account_data is not None:
username = user.social_account_data.get('login', username)
name = user.social_account_data.get('name', name)

return {
'admin': user.is_superuser,
'username': username,
'name': name,
'status': _get_user_status(user),
}


Expand Down Expand Up @@ -106,7 +117,13 @@ def users_search_view(request: Request) -> HttpResponseBase:

# Perform a search, excluding any inactive users and the 'AnonymousUser' account
qs = (
User.objects.exclude(username='AnonymousUser')
User.objects.select_related('metadata')
.annotate(
social_account_data=Subquery(
SocialAccount.objects.filter(user=OuterRef('pk')).values('extra_data')
)
)
.exclude(username='AnonymousUser')
.filter(
is_active=True,
metadata__status=UserMetadata.Status.APPROVED,
Expand All @@ -121,6 +138,6 @@ def users_search_view(request: Request) -> HttpResponseBase:
.order_by('date_joined')
)[:10]

users = [user_to_dict(user) for user in qs]
users = [serialize_user(user) for user in qs]
response_serializer = UserDetailSerializer(users, many=True)
return Response(response_serializer.data)