From e7f4c611cac8ec81137c3dea78b0b38387be10d0 Mon Sep 17 00:00:00 2001 From: Malcolm Box Date: Wed, 19 Oct 2016 10:57:33 +0100 Subject: [PATCH 1/2] Fix bug #17 - Enable cached queryLists to work --- drf_multiple_model/mixins.py | 12 +++++++++--- drf_multiple_model/tests.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/drf_multiple_model/mixins.py b/drf_multiple_model/mixins.py index a9c52c5..794b842 100644 --- a/drf_multiple_model/mixins.py +++ b/drf_multiple_model/mixins.py @@ -49,8 +49,15 @@ def get_queryList(self): ) queryList = self.queryList + qlist = [] + for query in queryList: + if not isinstance(query, Query): + query = Query.new_from_tuple(query) + qs = query.queryset.all() + query.queryset = qs + qlist.append(query) - return queryList + return qlist def paginate_queryList(self, queryList): """ @@ -70,8 +77,7 @@ def list(self, request, *args, **kwargs): if not isinstance(query, Query): query = Query.new_from_tuple(query) # Run the queryset through Django Rest Framework filters - queryset = query.queryset.all() - queryset = self.filter_queryset(queryset) + queryset = self.filter_queryset(query.queryset) # If there is a user-defined filter, run that too. if query.filter_fn is not None: diff --git a/drf_multiple_model/tests.py b/drf_multiple_model/tests.py index 154614c..7f97553 100644 --- a/drf_multiple_model/tests.py +++ b/drf_multiple_model/tests.py @@ -128,6 +128,17 @@ def get_queryList(self): return queryList +class CachedQueryView(MultipleModelAPIView): + def get_queryList(self): + queryList = cache.get('cachedquerylist') + if not queryList: + title = self.kwargs['play'].replace('-', ' ') + queryList = ((Play.objects.filter(title=title), PlaySerializer), + (Poem.objects.filter(style="Sonnet"), PoemSerializer)) + cache.set('cachedquerylist', queryList) + return queryList + + # Testing PageNumberPagination class BasicPagination(pagination.PageNumberPagination): page_size = 5 @@ -525,6 +536,28 @@ def test_dynamic_queryList(self): ]} ]) + def test_cached_queryList(self): + view = CachedQueryView.as_view() + + request = factory.get('/Julius-Caesar') + with self.assertNumQueries(2): + response = view(request, play="Julius-Caesar") + with self.assertNumQueries(0): + response = view(request, play="Julius-Caesar") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertEqual(len(response.data), 2) + self.assertEqual(response.data, [ + {'play': [ + {'title': 'Julius Caesar', 'genre': 'Tragedy', 'year': 1623}, + ] + }, + {'poem': [ + {'title': "Shall I compare thee to a summer's day?", 'style': 'Sonnet'}, + {'title': "As a decrepit father takes delight", 'style': 'Sonnet'} + ]} + ]) def test_url_endpoint(self): """ From f9483ef2c53b22bc8cc7b11bae141e0ae1b84ab2 Mon Sep 17 00:00:00 2001 From: Malcolm Box Date: Wed, 19 Oct 2016 11:06:46 +0100 Subject: [PATCH 2/2] Fix PEP8 formating --- drf_multiple_model/mixins.py | 6 +- drf_multiple_model/models.py | 1 - drf_multiple_model/tests.py | 133 +++++++++++++++++---------------- drf_multiple_model/views.py | 1 + drf_multiple_model/viewsets.py | 1 + 5 files changed, 75 insertions(+), 67 deletions(-) diff --git a/drf_multiple_model/mixins.py b/drf_multiple_model/mixins.py index 794b842..149da53 100644 --- a/drf_multiple_model/mixins.py +++ b/drf_multiple_model/mixins.py @@ -78,7 +78,7 @@ def list(self, request, *args, **kwargs): query = Query.new_from_tuple(query) # Run the queryset through Django Rest Framework filters queryset = self.filter_queryset(query.queryset) - + # If there is a user-defined filter, run that too. if query.filter_fn is not None: queryset = query.filter_fn(queryset, request, *args, **kwargs) @@ -93,7 +93,7 @@ def list(self, request, *args, **kwargs): # Sort by given attribute, if sorting_attribute is provided if self.sorting_field: results = self.queryList_sort(results) - + # Return paginated results if pagination is enabled page = self.paginate_queryList(results) if page is not None: @@ -137,7 +137,6 @@ def format_data(self, new_data, query, results): return results - # Sort based on the given sorting field property def queryList_sort(self, results): """ @@ -160,6 +159,7 @@ def queryList_sort(self, results): class Query(object): + def __init__(self, queryset, serializer, label=None, filter_fn=None, ): self.queryset = queryset self.serializer = serializer diff --git a/drf_multiple_model/models.py b/drf_multiple_model/models.py index f7323d4..64426ed 100644 --- a/drf_multiple_model/models.py +++ b/drf_multiple_model/models.py @@ -1,2 +1 @@ # Just to make django happy - diff --git a/drf_multiple_model/tests.py b/drf_multiple_model/tests.py index 7f97553..635e19f 100644 --- a/drf_multiple_model/tests.py +++ b/drf_multiple_model/tests.py @@ -1,22 +1,23 @@ -from django.db import models -from django.test import TestCase, override_settings -from django.conf.urls import url +from collections import OrderedDict + import django.template.loader +from django.conf.urls import url +from django.core.cache import cache +from django.db import models from django.template import TemplateDoesNotExist, Template - +from django.test import TestCase, override_settings from rest_framework import serializers, status, renderers, \ - pagination, filters, routers -from rest_framework.test import APIRequestFactory + pagination, filters, routers from rest_framework.test import APIClient +from rest_framework.test import APIRequestFactory -from drf_multiple_model.views import MultipleModelAPIView from drf_multiple_model.mixins import Query +from drf_multiple_model.views import MultipleModelAPIView from drf_multiple_model.viewsets import MultipleModelAPIViewSet -from collections import OrderedDict - factory = APIRequestFactory() + # Models class RestTestModels(models.Model): @@ -29,23 +30,29 @@ class Meta: app_label = "mm_tests" abstract = True + class Play(models.Model): genre = models.CharField(max_length=100) title = models.CharField(max_length=200) year = models.IntegerField(max_length=4) + class Poem(models.Model): title = models.CharField(max_length=200) style = models.CharField(max_length=100) + # Serializers class PlaySerializer(serializers.ModelSerializer): + class Meta: model = Play fields = ('genre', 'title', 'year') + class PoemSerializer(serializers.ModelSerializer): + class Meta: model = Poem fields = ('title', 'style') @@ -58,9 +65,9 @@ class BasicTestView(MultipleModelAPIView): queryList = ((Play.objects.all(), PlaySerializer), (Poem.objects.filter(style="Sonnet"), PoemSerializer)) - + class TestBrowsableAPIView(BasicTestView): - renderer_classes = (renderers.BrowsableAPIRenderer, ) + renderer_classes = (renderers.BrowsableAPIRenderer,) # Testing the objectify property (should return a dict/object instead @@ -119,6 +126,7 @@ class BrokenView(MultipleModelAPIView): # Testing get_queryList function class DynamicQueryView(MultipleModelAPIView): + def get_queryList(self): title = self.kwargs['play'].replace('-', ' ') @@ -129,6 +137,7 @@ def get_queryList(self): class CachedQueryView(MultipleModelAPIView): + def get_queryList(self): queryList = cache.get('cachedquerylist') if not queryList: @@ -143,7 +152,7 @@ def get_queryList(self): class BasicPagination(pagination.PageNumberPagination): page_size = 5 page_size_query_param = 'page_size' - max_page_size = 10 + max_page_size = 10 class PageNumberPaginationView(BasicFlatView): @@ -152,7 +161,7 @@ class PageNumberPaginationView(BasicFlatView): # Testing LinitOffsetPagination class LimitPagination(pagination.LimitOffsetPagination): - default_limit = 5 + default_limit = 5 max_limit = 15 @@ -171,6 +180,7 @@ def title_without_letter(queryset, request, *args, **kwargs): letter_to_exclude = request.query_params['letter'] return queryset.exclude(title__icontains=letter_to_exclude) + class FilterFnView(MultipleModelAPIView): queryList = (Query(Play.objects.all(), PlaySerializer, filter_fn=title_without_letter), (Poem.objects.all(), PoemSerializer)) @@ -178,8 +188,8 @@ class FilterFnView(MultipleModelAPIView): # Testing Built-in DRF Filter class SearchFilterView(BasicTestView): - filter_backends = (filters.SearchFilter, ) - search_fields = ('title', ) + filter_backends = (filters.SearchFilter,) + search_fields = ('title',) # Testing Base Viewset @@ -200,9 +210,11 @@ class BasicTestViewSet(MultipleModelAPIViewSet): url(r"^template$", HTMLRendererView.as_view()), ] -# Tests + +# Tests @override_settings(ROOT_URLCONF=__name__) class TestMMViews(TestCase): + def setUp(self): Play.objects.bulk_create([ Play(title='Romeo And Juliet', @@ -238,7 +250,6 @@ def test_defaults(self): """ view = BasicTestView.as_view() - request = factory.get('/') with self.assertNumQueries(2): @@ -247,23 +258,22 @@ def test_defaults(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 2) self.assertEqual(response.data, [ - { 'play': [ - {'title': 'Romeo And Juliet', 'genre': 'Tragedy', 'year': 1597}, - {'title': "A Midsummer Night's Dream", 'genre': 'Comedy', 'year': 1600}, - {'title': 'Julius Caesar', 'genre': 'Tragedy', 'year': 1623}, - {'title': 'As You Like It', 'genre': 'Comedy', 'year': 1623}, - ] + {'play': [ + {'title': 'Romeo And Juliet', 'genre': 'Tragedy', 'year': 1597}, + {'title': "A Midsummer Night's Dream", 'genre': 'Comedy', 'year': 1600}, + {'title': 'Julius Caesar', 'genre': 'Tragedy', 'year': 1623}, + {'title': 'As You Like It', 'genre': 'Comedy', 'year': 1623}, + ] }, - { 'poem': [ - {'title': "Shall I compare thee to a summer's day?", 'style': 'Sonnet'}, - {'title': "As a decrepit father takes delight", 'style': 'Sonnet'} + {'poem': [ + {'title': "Shall I compare thee to a summer's day?", 'style': 'Sonnet'}, + {'title': "As a decrepit father takes delight", 'style': 'Sonnet'} ]} ]) - def test_post(self): """ - POST requests should throw a 405 Error + POST requests should throw a 405 Error """ view = BasicTestView.as_view() @@ -278,7 +288,7 @@ def test_post(self): def test_put(self): """ - PUT requests should throw a 405 Error + PUT requests should throw a 405 Error """ view = BasicTestView.as_view() @@ -293,7 +303,7 @@ def test_put(self): def test_delete(self): """ - DELETE requests should throw a 405 Error + DELETE requests should throw a 405 Error """ view = BasicTestView.as_view() @@ -312,14 +322,13 @@ def test_objectify(self): """ view = AsObjectView.as_view() - request = factory.get('/') with self.assertNumQueries(2): response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, { + self.assertEqual(response.data, { 'play': [ {'title': 'Romeo And Juliet', 'genre': 'Tragedy', 'year': 1597}, {'title': "A Midsummer Night's Dream", 'genre': 'Comedy', 'year': 1600}, @@ -332,21 +341,19 @@ def test_objectify(self): ] }) - def test_no_label(self): """ Tests that no label (aka add_model_type = False) just gives the data """ view = BasicNoLabelView.as_view() - request = factory.get('/') with self.assertNumQueries(2): response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) - + self.assertEqual(len(response.data), 2) self.assertEqual(response.data, [ [ @@ -360,7 +367,7 @@ def test_no_label(self): {'title': "As a decrepit father takes delight", 'style': 'Sonnet'} ] ]) - + def test_new_labels(self): """ Adding labels as a third element in the queryList elements should use those labels @@ -404,7 +411,7 @@ def test_simple_flat(self): def test_ordered_flat(self): """ - Adding the sorting_field attribute should order the flat items according to whatever field + Adding the sorting_field attribute should order the flat items according to whatever field """ view = OrderedFlatView.as_view() @@ -425,7 +432,7 @@ def test_ordered_flat(self): def test_reversed_ordered(self): """ - Adding the sorting_field attribute should order the flat items according to whatever field + Adding the sorting_field attribute should order the flat items according to whatever field """ view = ReversedFlatView.as_view() @@ -526,13 +533,13 @@ def test_dynamic_queryList(self): self.assertEqual(len(response.data), 2) self.assertEqual(response.data, [ - { 'play': [ - {'title': 'Julius Caesar', 'genre': 'Tragedy', 'year': 1623}, - ] + {'play': [ + {'title': 'Julius Caesar', 'genre': 'Tragedy', 'year': 1623}, + ] }, - { 'poem': [ - {'title': "Shall I compare thee to a summer's day?", 'style': 'Sonnet'}, - {'title': "As a decrepit father takes delight", 'style': 'Sonnet'} + {'poem': [ + {'title': "Shall I compare thee to a summer's day?", 'style': 'Sonnet'}, + {'title': "As a decrepit father takes delight", 'style': 'Sonnet'} ]} ]) @@ -596,7 +603,7 @@ def test_page_number_pagination(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['results']), 1) - # Check change max size + # Check change max size request = factory.get('/', {'page_size': 3}) response = view(request).render() @@ -644,11 +651,11 @@ def test_filter_fn_view(self): """ The filter function is useful if you want to apply filtering to one query but not another (unlike adding view level filtering, which will filter all the - querysets), but that filtering can't be provided at the beginning (for example, it + querysets), but that filtering can't be provided at the beginning (for example, it needs to access a query_param). This is testing the filter_fn. """ - view = FilterFnView.as_view() + view = FilterFnView.as_view() request = factory.get('/', {'letter': 'o'}) @@ -669,7 +676,7 @@ def test_filter_fn_view(self): 'poem': [ {'title': "Shall I compare thee to a summer's day?", 'style': 'Sonnet'}, {'title': "As a decrepit father takes delight", 'style': 'Sonnet'}, - {'title': "A Lover's Complaint", 'style': 'Narrative'} + {'title': "A Lover's Complaint", 'style': 'Narrative'} ] } ]) @@ -678,8 +685,8 @@ def test_search_filter_view(self): """ Tests use of built in DRF filtering with MultipleModelAPIView """ - - view = SearchFilterView.as_view() + + view = SearchFilterView.as_view() request = factory.get('/', {'search': 'as'}) @@ -692,7 +699,7 @@ def test_search_filter_view(self): { 'play': [ {'title': 'As You Like It', 'genre': 'Comedy', 'year': 1623}, - ] + ] }, { 'poem': [ @@ -711,22 +718,23 @@ def test_base_viewset(self): self.assertEqual(len(response.data), 2) self.assertEqual(response.data, [ - { 'play': [ - {'title': 'Romeo And Juliet', 'genre': 'Tragedy', 'year': 1597}, - {'title': "A Midsummer Night's Dream", 'genre': 'Comedy', 'year': 1600}, - {'title': 'Julius Caesar', 'genre': 'Tragedy', 'year': 1623}, - {'title': 'As You Like It', 'genre': 'Comedy', 'year': 1623}, - ] + {'play': [ + {'title': 'Romeo And Juliet', 'genre': 'Tragedy', 'year': 1597}, + {'title': "A Midsummer Night's Dream", 'genre': 'Comedy', 'year': 1600}, + {'title': 'Julius Caesar', 'genre': 'Tragedy', 'year': 1623}, + {'title': 'As You Like It', 'genre': 'Comedy', 'year': 1623}, + ] }, - { 'poem': [ - {'title': "Shall I compare thee to a summer's day?", 'style': 'Sonnet'}, - {'title': "As a decrepit father takes delight", 'style': 'Sonnet'} + {'poem': [ + {'title': "Shall I compare thee to a summer's day?", 'style': 'Sonnet'}, + {'title': "As a decrepit father takes delight", 'style': 'Sonnet'} ]} ]) @override_settings(ROOT_URLCONF=__name__) class TestMMVHTMLRenderer(TestCase): + def setUp(self): Play.objects.bulk_create([ Play(title='Romeo And Juliet', @@ -752,7 +760,6 @@ def setUp(self): style="Narrative") ]) - """ Monkeypatch get_template Taken from DRF Tests @@ -773,13 +780,13 @@ def select_template(template_name_list, dirs=None, using=None): django.template.loader.select_template = select_template def test_html_renderer(self): - """ + """ Testing bug in which results dict failed to be passed into template context """ client = APIClient() response = client.get('/template', {'format': 'html'}) - + # test the data is formatted properly and shows up in the template self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertIn('data', response.data) diff --git a/drf_multiple_model/views.py b/drf_multiple_model/views.py index 3c0100d..aecec05 100644 --- a/drf_multiple_model/views.py +++ b/drf_multiple_model/views.py @@ -4,6 +4,7 @@ class MultipleModelAPIView(MultipleModelMixin, GenericAPIView): + def get_queryset(self): return diff --git a/drf_multiple_model/viewsets.py b/drf_multiple_model/viewsets.py index c515ea2..3c6ad44 100644 --- a/drf_multiple_model/viewsets.py +++ b/drf_multiple_model/viewsets.py @@ -4,5 +4,6 @@ class MultipleModelAPIViewSet(MultipleModelMixin, GenericViewSet): + def get_queryset(self): return