diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..0d59f902 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM ubuntu:xenial +ENV DEBIAN_FRONTEND noninteractive +ENV TERM=xterm + + +RUN apt-get -y --force-yes update +RUN apt-get -y --force-yes install locales + + +# Set the locale +RUN locale-gen en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 + +# Upgrade packages +RUN apt-get -y --force-yes upgrade +RUN apt-get -y --force-yes install software-properties-common curl git wget unzip nano build-essential autoconf libxml2-dev libssl-dev libbz2-dev libcurl3-dev libjpeg-dev libpng-dev libfreetype6-dev libgmp3-dev libc-client-dev libldap2-dev libmcrypt-dev libmhash-dev freetds-dev libz-dev ncurses-dev libpcre3-dev libsqlite-dev libaspell-dev libreadline6-dev librecode-dev libsnmp-dev libtidy-dev libxslt-dev +RUN apt-get -y --force-yes install ruby-dev debhelper python3-dev devscripts libxml2-dev + +RUN apt-get -y --force-yes install python3-pip python3-setuptools libpython3-dev +RUN apt-get -y --force-yes install python-pip python-setuptools libpython-dev +RUN apt-get install locales +RUN add-apt-repository "deb http://repo.aptly.info/ squeeze main" -y +RUN apt-key adv --keyserver keys.gnupg.net --recv-keys E083A3782A194991 +RUN apt-get update +RUN apt-get -yq --force-yes install dh-virtualenv goaccess aptly + +RUN apt-get install postgresql libpq-dev postgresql-client postgresql-client-common -y +RUN apt-get autoclean + +ADD . /home/ + +WORKDIR /home/ diff --git a/dynamic_rest/processors.py b/dynamic_rest/processors.py index 5acf6262..4fec4c98 100644 --- a/dynamic_rest/processors.py +++ b/dynamic_rest/processors.py @@ -118,6 +118,12 @@ def process(self, obj, parent=None, parent_key=None, depth=0): # move the object into a new top-level bucket # and mark it as seen self.data[name].append(obj) + else: + # obj sideloaded, but maybe with other fields + for o in self.data[name]: + if o.instance.pk == pk: + o.update(obj) + break # replace the object with a reference if parent is not None and parent_key is not None: diff --git a/tests/migrations/0005_auto_20170712_0759.py b/tests/migrations/0005_auto_20170712_0759.py new file mode 100644 index 00000000..b7ea2f02 --- /dev/null +++ b/tests/migrations/0005_auto_20170712_0759.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2017-07-12 07:59 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tests', '0004_user_is_dead'), + ] + + operations = [ + migrations.CreateModel( + name='Car', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # noqa + ('name', models.CharField(max_length=60)), + ], + ), + migrations.CreateModel( + name='Country', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # noqa + ('name', models.CharField(max_length=60)), + ('short_name', models.CharField(max_length=30)), + ], + ), + migrations.CreateModel( + name='Part', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # noqa + ('name', models.CharField(max_length=60)), + ('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.Car')), # noqa + ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.Country')), # noqa + ], + ), + migrations.AddField( + model_name='car', + name='country', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.Country'), # noqa + ), + ] diff --git a/tests/models.py b/tests/models.py index 864525b6..c06b630b 100644 --- a/tests/models.py +++ b/tests/models.py @@ -99,3 +99,19 @@ class C(models.Model): class D(models.Model): name = models.TextField(blank=True) + + +class Country(models.Model): + name = models.CharField(max_length=60) + short_name = models.CharField(max_length=30) + + +class Car(models.Model): + name = models.CharField(max_length=60) + country = models.ForeignKey(Country) + + +class Part(models.Model): + car = models.ForeignKey(Car) + name = models.CharField(max_length=60) + country = models.ForeignKey(Country) diff --git a/tests/serializers.py b/tests/serializers.py index da47dbed..48ebe0e0 100644 --- a/tests/serializers.py +++ b/tests/serializers.py @@ -12,15 +12,18 @@ DynamicModelSerializer ) from tests.models import ( + Car, Cat, + Country, Dog, Group, Horse, Location, + Part, Permission, Profile, User, - Zebra + Zebra, ) @@ -280,3 +283,30 @@ class Meta: 'name', 'origin', ) + + +class CountrySerializer(DynamicModelSerializer): + + class Meta: + model = Country + fields = ('id', 'name', 'short_name') + deferred_fields = ('name', 'short_name') + + +class PartSerializer(DynamicModelSerializer): + country = DynamicRelationField('CountrySerializer') + + class Meta: + model = Part + fields = ('id', 'name', 'country') + deferred_fields = ('name', 'country') + + +class CarSerializer(DynamicModelSerializer): + country = DynamicRelationField('CountrySerializer') + parts = DynamicRelationField('PartSerializer', many=True, source='part_set') # noqa + + class Meta: + model = Car + fields = ('id', 'name', 'country', 'parts') + deferred_fields = ('name', 'country', 'parts') diff --git a/tests/setup.py b/tests/setup.py index 0ada98b9..c7e69abb 100644 --- a/tests/setup.py +++ b/tests/setup.py @@ -1,16 +1,19 @@ from collections import namedtuple from tests.models import ( + Car, Cat, + Country, Dog, Event, Group, Horse, Location, + Part, Permission, User, Zebra -) + ) def create_fixture(): @@ -20,16 +23,20 @@ def create_fixture(): # 2 of the users share the same location # 2 of the users have their own locations # Create 4 dogs. + # Create 2 Country + # Create 1 Car has 2 Parts each from different Country types = [ 'users', 'groups', 'locations', 'permissions', - 'events', 'cats', 'dogs', 'horses', 'zebras' + 'events', 'cats', 'dogs', 'horses', 'zebras', + 'cars', 'countries', 'parts', ] Fixture = namedtuple('Fixture', types) fixture = Fixture( users=[], groups=[], locations=[], permissions=[], - events=[], cats=[], dogs=[], horses=[], zebras=[] + events=[], cats=[], dogs=[], horses=[], zebras=[], + cars=[], countries=[], parts=[] ) for i in range(0, 4): @@ -185,4 +192,47 @@ def create_fixture(): fixture.groups[0].permissions.add(fixture.permissions[0]) fixture.groups[1].permissions.add(fixture.permissions[1]) + countries = [{ + 'id': 1, + 'name': 'United States', + 'short_name': 'US', + }, { + 'id': 2, + 'name': 'China', + 'short_name': 'CN', + }] + + cars = [{ + 'id': 1, + 'name': 'Porshe', + 'country': 1 + }] + + parts = [{ + 'car': 1, + 'name': 'wheel', + 'country': 1 + }, { + 'car': 1, + 'name': 'tire', + 'country': 2 + }] + + for country in countries: + fixture.countries.append(Country.objects.create(**country)) + + for car in cars: + fixture.cars.append(Car.objects.create( + id=car.get('id'), + name=car.get('name'), + country_id=car.get('country') + )) + + for part in parts: + fixture.parts.append(Part.objects.create( + car_id=part.get('car'), + name=part.get('name'), + country_id=part.get('country') + )) + return fixture diff --git a/tests/test_generic.py b/tests/test_generic.py index b77d2d7d..05e74043 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -71,6 +71,25 @@ def test_sideload(self): self.assertTrue('type' in content['users'][0]['favorite_pet']) self.assertTrue('id' in content['users'][0]['favorite_pet']) + def test_multi_sideload_include(self): + url = ( + '/cars/1/?include[]=name&include[]=country.short_name' + '&include[]=parts.name&include[]=parts.country.name' + ) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + content = json.loads(response.content.decode('utf-8')) + self.assertTrue('countries' in content) + + country = None + for _ in content['countries']: + if _['id'] == 1: + country = _ + + self.assertTrue(country) + self.assertTrue('short_name' in country) + self.assertTrue('name' in country) + def test_query_counts(self): # NOTE: Django doesn't seem to prefetch ContentType objects # themselves, and rather caches internally. That means diff --git a/tests/urls.py b/tests/urls.py index 2d4206be..be6ea6b9 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -9,6 +9,7 @@ router.register_resource(viewsets.ProfileViewSet) router.register_resource(viewsets.LocationViewSet) +router.register(r'cars', viewsets.CarViewSet) router.register(r'cats', viewsets.CatViewSet) router.register_resource(viewsets.DogViewSet) router.register_resource(viewsets.HorseViewSet) diff --git a/tests/viewsets.py b/tests/viewsets.py index 03f2cc1a..3c1300aa 100644 --- a/tests/viewsets.py +++ b/tests/viewsets.py @@ -2,6 +2,7 @@ from dynamic_rest.viewsets import DynamicModelViewSet from tests.models import ( + Car, Cat, Dog, Group, @@ -13,6 +14,7 @@ Zebra ) from tests.serializers import ( + CarSerializer, CatSerializer, DogSerializer, GroupSerializer, @@ -23,7 +25,7 @@ UserLocationSerializer, UserSerializer, ZebraSerializer -) + ) class UserViewSet(DynamicModelViewSet): @@ -150,3 +152,8 @@ class ZebraViewSet(DynamicModelViewSet): class PermissionViewSet(DynamicModelViewSet): serializer_class = PermissionSerializer queryset = Permission.objects.all() + + +class CarViewSet(DynamicModelViewSet): + serializer_class = CarSerializer + queryset = Car.objects.all()