pulseapi/profiles/views/profiles.py (199 lines of code) (raw):
import base64
from itertools import chain
import django_filters
from django.core.files.base import ContentFile
from django.conf import settings
from django.db.models import Q
from django_filters.rest_framework import (
DjangoFilterBackend,
FilterSet,
)
from rest_framework import permissions
from rest_framework.filters import (
SearchFilter,
OrderingFilter
)
from rest_framework.generics import (
RetrieveUpdateAPIView,
RetrieveAPIView,
ListAPIView,
get_object_or_404,
)
from rest_framework.pagination import PageNumberPagination
from pulseapi.profiles.models import UserProfile
from pulseapi.profiles.serializers import (
UserProfileSerializer,
UserProfilePublicWithEntriesSerializer,
UserProfilePublicSerializer,
UserProfileBasicSerializer,
UserProfileListSerializer,
)
class IsProfileOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.user == request.user
class UserProfileAPIView(RetrieveUpdateAPIView):
permission_classes = (
permissions.IsAuthenticated,
IsProfileOwner
)
serializer_class = UserProfileSerializer
def get_object(self):
user = self.request.user
return get_object_or_404(
UserProfile.objects.prefetch_related(
'issues',
'related_user',
'bookmarks_from',
),
related_user=user
)
def get_serializer_context(self):
return {
'user': self.request.user
}
def put(self, request, *args, **kwargs):
"""
If there is a thumbnail, and it was sent as part of an
application/json payload, then we need to unpack a thumbnail
object payload and convert it to a Python ContentFile payload
instead. We use a try/catch because the optional nature means
we need to check using "if hasattr(request.data,'thumbnail'):"
as we as "if request.data['thumbnail']" and these are pretty
much mutually exclusive patterns. A try/pass make far more sense.
"""
payload = request.data
thumbnail = payload.get('thumbnail')
if thumbnail:
name = thumbnail.get('name')
encdata = thumbnail.get('base64')
if name and encdata:
request.data['thumbnail'] = ContentFile(
base64.b64decode(encdata),
name=name,
)
return super(UserProfileAPIView, self).put(request, *args, **kwargs)
class UserProfilePublicAPIView(RetrieveAPIView):
queryset = UserProfile.objects.all()
def get_serializer_class(self):
if self.request.version == settings.API_VERSIONS['version_1']:
return UserProfilePublicWithEntriesSerializer
return UserProfilePublicSerializer
def get_serializer_context(self):
return {
'user': self.request.user
}
class UserProfilePublicSelfAPIView(UserProfilePublicAPIView):
def get_object(self):
user = self.request.user
return get_object_or_404(self.queryset, related_user=user)
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
pass
class ProfileCustomFilter(FilterSet):
"""
We add custom filtering to allow you to filter by:
* Profile ids - pass the `?ids=` query parameter
* Profile type - pass the `?profile_type=` query parameter
* Program type - pass the `?program_type=` query parameter
* Program year - pass the `?program_year=` query parameter
* Limit results - pass the `?limit=` query parameter
"""
ids = NumberInFilter(
field_name='id',
lookup_expr='in'
)
profile_type = django_filters.CharFilter(
field_name='profile_type__value',
lookup_expr='iexact',
)
program_type = django_filters.CharFilter(
field_name='program_type__value',
lookup_expr='iexact',
)
program_year = django_filters.CharFilter(
field_name='program_year__value',
lookup_expr='iexact',
)
name = django_filters.CharFilter(method='filter_name')
def filter_name(self, queryset, name, value):
startswith_lookup = Q(custom_name__istartswith=value) | Q(related_user__name__istartswith=value)
qs_startswith = queryset.filter(startswith_lookup)
qs_contains = queryset.filter(
Q(custom_name__icontains=value) | Q(related_user__name__icontains=value)
).exclude(startswith_lookup)
return list(chain(qs_startswith, qs_contains))
limit = django_filters.NumberFilter(method='filter_limit')
def filter_limit(self, queryset, name, value):
return queryset.limit(value)
@property
def qs(self):
"""
Ensure that if the filter route is called without
a legal filtering argument, we return an empty
queryset, rather than every profile in existence.
"""
request = self.request
if request is None:
return UserProfile.objects.none()
queries = self.request.query_params
if queries is None:
return UserProfile.objects.none()
if 'search' in queries:
qs = super(ProfileCustomFilter, self).qs
else:
qs = UserProfile.objects.none()
fields = ProfileCustomFilter.get_fields()
for key in fields:
if key in queries:
qs = super(ProfileCustomFilter, self).qs
return qs
class Meta:
"""
Required Meta class
"""
model = UserProfile
fields = [
'ids',
'profile_type',
'program_type',
'program_year',
'is_active',
'name',
'limit'
]
class ProfilesPagination(PageNumberPagination):
page_size = 30
page_size_query_param = 'page_size'
max_page_size = 50
class UserProfileListAPIView(ListAPIView):
"""
Query Params:
search=(search in name, affiliation, user_bio, user_bio_long, location)
profile_type=
program_type=
program_year=
is_active=(True or False)
ordering=(id, custom_name, program_year) or negative (e.g. -id) to reverse.
basic=
One of the queries above must be specified to get a result set
You can also control pagination using the following query params:
page_size=(number)
page=(number)
"""
filter_backends = (
OrderingFilter,
DjangoFilterBackend,
SearchFilter,
)
ordering_fields = ('id', 'custom_name', 'program_year',)
ordering = ('-id',)
search_fields = (
'custom_name',
'related_user__name',
'affiliation',
'user_bio',
'user_bio_long',
'location',
)
pagination_class = ProfilesPagination
filter_class = ProfileCustomFilter
def get_queryset(self):
request = self.request
queries = self.request.query_params
# If we are doing a specific search for is_active, return a filtered list for either
# active or inactive profiles. If we are filtering based on anything else, filter out
# inactive profiles by default.
if 'is_active' in queries:
queryset = UserProfile.objects.all().prefetch_related('related_user')
else:
queryset = UserProfile.objects.active().prefetch_related('related_user')
if not request or request.version != settings.API_VERSIONS['version_2']:
# for all requests that aren't v2, we don't need to prefetch
# anything else because no other relationship data is selected
return queryset
return queryset.prefetch_related(
'issues',
'profile_type',
'program_type',
'program_year',
'bookmarks_from',
)
def paginate_queryset(self, queryset):
request = self.request
if not request:
return super().paginate_queryset(queryset)
version = request.version
if version == settings.API_VERSIONS['version_1'] or version == settings.API_VERSIONS['version_2']:
# Don't paginate version 1 and 2 of the API
return None
return super().paginate_queryset(queryset)
def get_serializer_class(self):
request = self.request
if not request:
# mock serializer testing
return UserProfileListSerializer
version = request.version
if version == settings.API_VERSIONS['version_1']:
# v1
return UserProfilePublicWithEntriesSerializer
if 'basic' in request.query_params:
# v2 and above, 'basic' takes precedence over versioned serializers
return UserProfileBasicSerializer
if version == settings.API_VERSIONS['version_2']:
# v2
return UserProfilePublicSerializer
# v3 and above
return UserProfileListSerializer
def get_serializer_context(self):
return {
'user': self.request.user
}