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

Override fields not init #7

Closed
wants to merge 10 commits into from
59 changes: 36 additions & 23 deletions drf_dynamic_fields/__init__.py
@@ -1,7 +1,6 @@
"""
Mixin to dynamically select only a subset of fields per DRF resource.
"""

import warnings


Expand All @@ -10,32 +9,46 @@ class DynamicFieldsMixin(object):
A serializer mixin that takes an additional `fields` argument that controls
which fields should be displayed.
"""
def __init__(self, *args, **kwargs):
super(DynamicFieldsMixin, self).__init__(*args, **kwargs)

# If the context is not set, return
if not self.context:
return
@property
def fields(self):
"""
Filters the fields according to the `fields` query parameter.

a blank `fields` parameter (?fields) will remove all fields.
not passing `fields` will pass all fields
individual fields are comma separated (?fields=id,name,url,email)
"""
fields = super(DynamicFieldsMixin, self).fields

# If the request is not passed in, warn and return
if 'request' not in self.context:
if not hasattr(self, '_context'):
# we are being called before a request cycle.
return fields

try:
request = self.context['request']
except KeyError:
warnings.warn('Context does not have access to request')
return
return fields

# NOTE: drf test framework builds a request object where the query
# parameters are found under the GET attribute.
if hasattr(self.context['request'], 'query_params'):
fields = self.context['request'].query_params.get('fields', None)
elif hasattr(self.context['request'], 'GET'):
fields = self.context['request'].GET.get('fields', None)
else:
params = getattr(
request, 'query_params', getattr(request, 'GET', None)
)
if params is None:
warnings.warn('Request object does not contain query paramters')
return

if fields:
fields = fields.split(',')
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields.keys())
for field_name in existing - allowed:
self.fields.pop(field_name)

try:
filter_fields = params.get('fields', None).split(',')
except AttributeError:
return fields

# Drop any fields that are not specified in the `fields` argument.
allowed = set(filter(None, filter_fields))
existing = set(fields.keys())

for field in existing - allowed:
fields.pop(field)

return fields
15 changes: 15 additions & 0 deletions manage.py
@@ -0,0 +1,15 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
For running tests.
"""
from __future__ import unicode_literals, absolute_import

import os
import sys

if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
from django.core.management import execute_from_command_line

execute_from_command_line(sys.argv)
34 changes: 34 additions & 0 deletions runtests.py
@@ -0,0 +1,34 @@
#!/usr/bin/env python
# -*- coding: utf-8
"""
Run tests with python runtests.py

Taken from the django cookiecutter project.
"""
from __future__ import unicode_literals, absolute_import

import os
import sys

import django
from django.conf import settings
from django.test.utils import get_runner


def run_tests(*test_args):
"""
I am here to satisfy the code quality checker.
"""
if not test_args:
test_args = ['tests']

os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings'
django.setup()
TestRunner = get_runner(settings) # noqa
test_runner = TestRunner()
failures = test_runner.run_tests(test_args)
sys.exit(bool(failures))


if __name__ == '__main__':
run_tests(*sys.argv[1:])
Empty file added tests/__init__.py
Empty file.
13 changes: 13 additions & 0 deletions tests/models.py
@@ -0,0 +1,13 @@
"""
Some models for the tests. We are modelling a school.
"""
from django.db import models


class Teacher(models.Model):
"""No fields, no fun."""


class School(models.Model):
"""Schools just have teachers, no students."""
teachers = models.ManyToManyField(Teacher)
44 changes: 44 additions & 0 deletions tests/serializers.py
@@ -0,0 +1,44 @@
"""
For the tests.
"""
from rest_framework import serializers

from drf_dynamic_fields import DynamicFieldsMixin

from .models import Teacher, School


class TeacherSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
"""
The request_info field is to highlight the issue accessing request during
a nested serializer.
"""

request_info = serializers.SerializerMethodField()

class Meta:
model = Teacher
fields = ('id', 'request_info')

def get_request_info(self, teacher):
"""
a meaningless method that attempts
to access the request object.
"""
request = self.context['request']
return request.build_absolute_uri(
'/api/v1/teacher/{}'.format(teacher.pk)
)


class SchoolSerializer(serializers.ModelSerializer):
"""
Interesting enough serializer because the TeacherSerializer
will use ListSerializer due to the `many=True`
"""

teachers = TeacherSerializer(many=True, read_only=True)

class Meta:
model = School
fields = ('id', 'teachers')
37 changes: 37 additions & 0 deletions tests/settings.py
@@ -0,0 +1,37 @@
# -*- coding: utf-8
"""
Settings for test.
"""
from __future__ import unicode_literals, absolute_import

import django

DEBUG = True
USE_TZ = True

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "**************************************************"

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
}
}

ROOT_URLCONF = "tests.urls"

INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sites",
"drf_dynamic_fields",
"tests"
]

SITE_ID = 1

if django.VERSION >= (1, 10):
MIDDLEWARE = ()
else:
MIDDLEWARE_CLASSES = ()
111 changes: 111 additions & 0 deletions tests/test_mixins.py
@@ -0,0 +1,111 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
test_drf-dynamic-fields
------------

Tests for `drf-dynamic-fields` mixins
"""
from collections import OrderedDict

from django.test import TestCase, RequestFactory

from .serializers import SchoolSerializer, TeacherSerializer
from .models import Teacher, School


class TestDynamicFieldsMixin(TestCase):
"""
Test case for the DynamicFieldsMixin
"""

def test_removes_fields(self):
"""
Does it actually remove fields?
"""
rf = RequestFactory()
request = rf.get('/api/v1/schools/1/?fields=id')
serializer = TeacherSerializer(context={'request': request})

self.assertEqual(
set(serializer.fields.keys()),
set(('id',))
)

def test_fields_left_alone(self):
"""
What if no fields param is passed? It should not touch the fields.
"""
rf = RequestFactory()
request = rf.get('/api/v1/schools/1/')
serializer = TeacherSerializer(context={'request': request})

self.assertEqual(
set(serializer.fields.keys()),
set(('id', 'request_info'))
)

def test_fields_all_gone(self):
"""
If we pass a blank fields list, then no fields should return.
"""
rf = RequestFactory()
request = rf.get('/api/v1/schools/1/?fields')
serializer = TeacherSerializer(context={'request': request})

self.assertEqual(
set(serializer.fields.keys()),
set()
)

def test_ordinary_serializer(self):
"""
Check the full JSON output of the serializer.
"""
rf = RequestFactory()
request = rf.get('/api/v1/schools/1/?fields=id')
teacher = Teacher.objects.create()

serializer = TeacherSerializer(teacher, context={'request': request})

self.assertEqual(
serializer.data, {
'id': teacher.id
}
)

def test_as_nested_serializer(self):
"""
Nested serializers are not filtered.
"""

rf = RequestFactory()
request = rf.get('/api/v1/schools/1/')

school = School.objects.create()
teachers = [
Teacher.objects.create(),
Teacher.objects.create()
]
school.teachers.add(*teachers)

serializer = SchoolSerializer(school, context={'request': request})

request_info = 'http://testserver/api/v1/teacher/{}'

self.assertEqual(
serializer.data, {
'teachers': [
OrderedDict([
('id', teachers[0].id),
('request_info', request_info.format(teachers[0].id))
]),
OrderedDict([
('id', teachers[1].id),
('request_info', request_info.format(teachers[1].id))
])
],
'id': school.id
}
)
5 changes: 5 additions & 0 deletions tests/urls.py
@@ -0,0 +1,5 @@
# -*- coding: utf-8
"""
Empty urls for test.
"""
urlpatterns = [] # noqa.