Using Factory Boy with GeoDjango PointFields
Asked Answered
S

4

7

I'm working on writing tests for a new GeoDjango project I've started. Normally I used Factory Boy and Faker to create model instances for testing. However it's not clear to me how you can mock GeoDjango PointField fields. When looking at the record in Spacialite it appears as a binary blob.

I'm totally new to GIS stuff, and a little confused as to how I can create factories for PointFields in Django.

# models.py

from django.contrib.gis.db import models

class Place(models.Model):
    name = models.CharField(max_length=255)
    location = models.PointField(blank=True, null=True)

    objects = models.GeoManager()

    def __str__(self):
        return "%s" % self.name
# factories.py

import factory
from faker import Factory as FakerFactory
from . import models

faker = FakerFactory.create()

class PlaceFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = models.Place

    name = factory.LazyAttribute(lambda x: faker.name())
    #location = What do I do?
Sabinesabino answered 28/9, 2015 at 17:47 Comment(1)
If you are not fixed on Factory Boy, checkout this extension for model-mommy (by now my preferred library for creating test records): github.com/sigma-geosistemas/mommy_spatial_generatorsAdolescence
A
11

I believe you need to create a custom fuzzy attribute for point instances. Can you try this? Right now I don't have the setup to run it all through.

import random
from django.contrib.gis.geos import Point
from factory.fuzzy import BaseFuzzyAttribute

class FuzzyPoint(BaseFuzzyAttribute):
    def fuzz(self):
        return Point(random.uniform(-180.0, 180.0),
                     random.uniform(-90.0, 90.0))


class PlaceFactory(FakerFactory):
    name = factory.LazyAttribute(lambda x: faker.name())
    location = FuzzyPoint()
    class Meta:
        model = models.Place
Adolescence answered 28/9, 2015 at 20:19 Comment(0)
S
11

Fussy is about to be deprecated as Factory Boy documentation says.

Now that FactoryBoy includes the factory.Faker class, most of these built-in fuzzers are deprecated in favor of their Faker equivalents.

As @Steven B said, you must create your own provider. I did some changes to his code in order to make the provider as generic as possible.

class DjangoGeoPointProvider(BaseProvider):

    def geo_point(self, **kwargs):
        kwargs['coords_only'] = True
        # # generate() is not working in later Faker versions
        # faker = factory.Faker('local_latlng', **kwargs)
        # coords = faker.generate()  
        faker = factory.faker.faker.Faker()
        coords = faker.local_latlng(**kwargs)
        return Point(x=float(coords[1]), y=float(coords[0]), srid=4326)

Note: coords_only must be always true because we just need lat and long values, without any extra metadata.

Note 2: generate() is deprecated, see related answer.

Finally, it is like using the local_latlng provider or any of the built-in providers. Here is a full example:

class TargetFactory(factory.django.DjangoModelFactory):
    factory.Faker.add_provider(DjangoGeoPointProvider)

    class Meta:
        model = Target

    radius = factory.Faker('random_int', min=4500, max=90000)
    location = factory.Faker('geo_point', country_code='US')

Note: 'US' is the default country code, it could be omitted in this example, but you could use any of the other specified countries code in Faker doc.

Spalla answered 6/4, 2020 at 12:16 Comment(0)
E
2

Faker already has some nice geo features. You can create you own provider to make it work for Django, for example:

import factory
from faker.providers import BaseProvider
from django.contrib.gis.geos import Point

class DjangoGeoLocationProvider(BaseProvider):

    countries = ['NL', 'DE', 'FR', 'BE']

    # uses faker.providers.geo
    def geolocation(self, country=None):
        country_code = country or factory.Faker('random_element', elements=self.countries).generate()
        faker = factory.Faker('local_latlng', country_code=country_code, coords_only=True)
        coords = faker.generate()
        return Point(x=float(coords[1]), y=float(coords[0]), srid=4326)

Which will generate Django compatible Points for the given countries.

After registering it:

factory.Faker.add_provider(DjangoGeoLocationProvider)

You can use it in your DjangoModelFactory like any of the built-in providers:

from factory.django import DjangoModelFactory as Factory

class PlaceFactory(Factory):

    class Meta:
        model = Place

    location = factory.Faker('geolocation')
    # or
    another_location = factory.Faker('geolocation', country='NL')
Everyplace answered 9/11, 2019 at 21:39 Comment(0)
C
0

You can also set the value directly.

import json

from factory import LazyAttribute
from factory.django import DjangoModelFactory


class PlaceFactory(DjangoModelFactory):
    static_location = "{'type': 'Point', 'coordinates': [1,1]}"
    random_location = LazyAttribute(lambda x: json.dumps({
        'type': 'Point',
        'coordinates': [random.uniform(-180.0, 180.0), random.uniform(-180.0, 180.0)]}
    ))
Calvincalvina answered 23/2, 2021 at 10:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.