Using Factory Boy with GeoDjango PointFields - python

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?

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

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.

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')

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)]}
))

Related

Django Python With Gspread: 'choices' must be an iterable containing (actual value, human readable name) tuples

I am trying to do something that I have never seen done before with django, I am trying to make a model field(path_choices) that shows all of the unique path_names from my google sheet in a choice box so that the user can select one of them. However when I tried to make my choices CharField I am getting the error:
ERRORS:
dashboard.Robot.path_choices: (fields.E005) 'choices' must be an iterable containing (actual value, human readable name) tuples.
Right now the google sheet that I am trying to pull from with gspread only has two path-names, so if anybody has any idea on what is causing this problem or what I can do better with this, the help would be appreciated!
My Code (UPDATED CODE):
from django.db import models
class Robot(models.Model):
name = models.CharField(max_length=100)
status_choices = [('driving', 'driving'), ('waiting', 'waiting'), ('stuck', 'stuck')]
status = models.CharField(choices=status_choices, max_length=7, default='waiting')
path_choices = models.CharField(max_length=255)
My Form:
from django import forms
from django import forms
from .models import Robot
import gspread
from oauth2client.service_account import ServiceAccountCredentials
class RobotForm(forms.ModelForm):
def _generate_choices():
scope = ["REDACTED",'REDACTED',"REDACTED","REDACTED"]
creds = ServiceAccountCredentials.from_json_keyfile_name("dashboard/Files/creds.json", scope)
client = gspread.authorize(creds)
sheet = client.open("tutorial").sheet1
path_name_fetch = sheet.col_values(1)
path_names = []
temp_list = []
path_options = []
for i in path_name_fetch:
if i not in path_names:
path_names.append(i)
for path_name_options in path_names:
temp_list.append(f'{path_name_options}')
temp_list.append(f'{path_name_options}')
path_options.append(tuple(temp_list))
path_choices = forms.ChoiceField(choices=_generate_choices())
class Meta:
model = Robot
fields = {'path_choices'}
What you're trying to do may not be in line with the intended use of Choices. Whenever the possibilities of a Choice change at the Model level, new migrations must be made.
Your implementation of Choice for Robot.status is static and in line with the example in the Django documentation.
If instead you wanted to use a dynamic Choice for your path_choices that is retrieved from Google Sheets, I would recommend doing this in the ModelForm using a ChoiceField.
According to the documentation, the available choices can come from a callable, which would be your path_options wrapped in a function. path_options should then become a CharField without choices, since you manage those in the submission, rather than at the model level.
Models.py
class Robot(models.Model):
...
path_choices = models.CharField(max_length=255)
ModelForms.py
class RobotForm(ModelForm):
def _generate_choices():
# Query GSpread
choices = [('choice','choice'), ...]
return choices
path_choices = forms.ChoiceField(choices=_generate_choices())
class Meta:
model = Robot
fields = ['path_choices', ...]

How to create instance with new name every time when factory called

I have a factoryboy factory for sqlalchemy model which uses faker
from faker import Faker
from db_init import session
fake = Faker()
class ClientAppFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = ClientApp
sqlalchemy_session = session
sqlalchemy_session_persistence = 'commit'
name = fake.word()
display_name = fake.word()
This factory creates rows in database. Field name of my model must be unique. When I call my factory several times in a row, I get several instances with the same name. How can I get different names in my instances?
import pytest
from pytest_factoryboy import register
register(ClientAppFactory)
#pytest.fixture
def several_client_apps():
ca1 = client_app_factory()
ca2 = client_app_factory()
In this case I get error about name field unique constraint.
I found an answer. Factory has method build which redefine factory attributes every time when you call it.
#pytest.fixture
def several_client_apps():
ca1 = client_app_factory.build()
ca2 = client_app_factory.build()
This is a common mistake with factoryboy and `faker.
Using faker.Faker in a Factory will establish a fixed value on the definition of the class. What you want instead is to use factory.Faker or delay any faker.Faker execution with a lazy function.
An example with both strategies:
from faker import Faker
import factory
fake = Faker()
word_list = ["one", "two", "three"]
class ExampleFactory(factory.DictFactory):
name = factory.Faker("word")
display_name = factory.LazyFunction(fake.word)
another_name = factory.LazyFunction(lambda: fake.word(word_list))
ExampleFactory.bu()
ExampleFactory.create_batch(5)
I used a DictFactory to make it simpler to test, but it should work with model-based factories too.
The another_name field uses a lambda to be able to add parameters to the fake method.

Factory Boy Circular Import not working as desired

I have 2 models, Account and AccountUser. An Account has many AccountUsers. I'm trying to create this using Factory Boy's circular imports but have been unsuccessful so far with various changes. There's not much on SO about factory boy, and not enough on Google to help me. Can anyone help me make this work without errors?
I put a pdb at the bottom of stub_account.py and do acct = AccountFactory() but I get *** NameError: name 'AccountFactory' is not defined. If I do acct = AccountFactory().build() or .create() I get the same error. If I define those variables inside the script instead of while in pdb I get the same error.
project_root/app/tests/scripts/stubs/stub_account.py
from app.models import Account
from faker import Faker
import factory
fake = Faker()
class AccountFactory(factory.Factory):
class Meta:
model = Account
billing_contact = factory.SubFactory(
"app.tests.scripts.stubs.stub_account_user.AccountUserFactory")
project_root/app/tests/scripts/stubs/stub_account_user.py
from app.models import AccountUser
from faker import Faker
import factory
fake = Faker()
class AccountUserFactory(factory.Factory):
class Meta:
model = AccountUser
_parent = factory.SubFactory(AccountFactory)
For reverse foreign key you have to use factory.RelatedFactory

Django model - object attribute update after creation

I am working on some Django project (first time) and after lot of searching i have no clue how to proper update object attribute after creation. I have sucha models.py
from django.db import models
import os
# Create your models here.
class Place(models.Model):
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Add templates folder for dynamic usage, os independent
TEMPLATE_DIR = os.path.join(BASE_DIR, "templates/places")
name = models.CharField(max_length=100, unique=True)
display_name = models.CharField(max_length=100)
floor = models.DecimalField(max_digits=3, decimal_places=0)
template = models.FilePathField(path=TEMPLATE_DIR, match=".*html")
url = models.URLField(unique=True)
# Display name of object in object list view
def __str__(self):
return self.name
Question is how to update url attribute after object creation of url to this particular object, it should be something like (base url + object_id i know object_id is created after object creation) but after searching documentation i dont have ideas how to do this properly.
I tried get_absolute_path but with no success.
Maybe some kind post_save method override ?
One option would be to override the model save method and detect if the model instance is being added or updated and update the url field accordingly:
class Place(models.Model):
# ...
def save(self, *args, **kwargs):
is_adding = self._state.adding
super(Place, self).save(self, *args, **kwargs)
if is_adding:
url = os.path.join(self.TEMPLATE_DIR, str(self.pk))
super(Place, self).save(self, *args, **kwargs)
However, you needn't actually store the url as you can derive the value from other model fields when required. Hence, you can delete the url field and create a url method instead:
class Place(models.Model):
# ...
#property
def url(self):
return os.path.join(self.TEMPLATE_DIR, str(self.pk))
Here, the #property decoration here allows you to access place.url as if it was a model field.
This might be a good time to introduce something like Django celery into you're project and run this task async after creation.
You could have a task in a file like this:
# I'm making assumptions on project architecture..
from app.places import Place
from app.places.utils import PLACE_BASE_URL
#shared_task
def task_update_place_url(place_id):
"""
Simple method to update Place url after creation.
"""
place = Place.objects.get(pk=place_id)
place.url = PLACE_BASE_URL + place_id
place.save()
Then inside of your view when you create your Place you can do this:
import json
from django.http.response import HttpResponse
from app.places.models import Place
from app.places.tasks import task_update_place_url
def create_place(request):
"""
Creates a new Place and update url in task
"""
data = json.loads(request)
place = Place.objects.create(**data)
task_update_place_url.delay(place.id)
return HttpResponse("Finished")

Django: How can I get the foreign key's class from a classmethod in the referred-to class?

I have the following two Django Classes MyClassA and MyClassB in two separate files. MyClassB has a foreign key reference to an instance of MyClassA. MyClassA cannot import the class MyClassB.
my_class_a/models.py:
from django.db import models
class MyClassA(models.Model):
name = models.CharField(max_length=50, null=False)
#classmethod
def my_method_a(cls):
# What do I put here to call MyClassB.my_method_b()??
my_class_b/models.py:
from my_class_a.models import MyClassA
from django.db import models
class MyClassB(models.Model):
name = models.CharField(max_length=50, null=False)
my_class_a = models.ForeignKey(MyClassA, related_name="MyClassB_my_class_a")
#staticmethod
def my_method_b():
return "Hello"
From within MyClassA's class method my_method_a, I would like to call MyClassB's static method my_method_b. How can I do it?
If my_method_a was an instance method, I would simply do self.MyClassB_my_class_a.model.my_method_b(). But since I don't have an instance of MyClassA, I don't know how to do it. I would like to take advantage of the related_name field that allows for reverse lookups of instances.
You can do it like this.
#classmethod
def my_method_a(cls):
from myclass_b.models import MyClassB
# yes, you can have an import here. and it will not
# lead to a cyclic import error
MyClassB.my_method_b()
The import failure happens only if you add the import to the top of the file. That would lead to cyclic imports one module cannot be loaded because it depends on another which depends on the other module. However when the import is inside a method the same problem does not arise.

Categories