Add ModelField that accepts model instances instead of PKs
Closed this issue · 2 comments
Currently, if you wanted to accept model instances as inputs to your services, you'd have to use ModelChoiceField
. It works well enough, but it has its quirks. It would be a good idea to a have a custom field that can accept model instances directly. This would fix a few issues I've found when using ModelChoiceField
.
Prevent unintended model mixups
Because ModelChoiceField
just validates using the primary key, it's really easy to mix up models.
person = Person.objects.get(pk=1)
tenant = Tenant.objects.get(pk=1)
SendInvoice.execute({'person': tenant.pk})
# meant to pass in person.pk, but because it's just an integer, it still validates
If we pass in model instances, we can check for its type.
person = Person.objects.get(pk=1)
tenant = Tenant.objects.get(pk=1)
SendInvoice.execute({'person': tenant})
# raises InvalidInputsError, person is not an instance of Person
Allow unsaved model instances as inputs
Because ModelChoiceField
fetches your model via its PK, it inherently requires that your model already be saved before being passed in.
With a custom field, you could accept unsaved model instances as inputs, as long as they're valid.
person = Person(
name='Mitch',
age=24,
)
SendInvoice.execute({'person': person})
# works
person = Person(
name=1234,
)
SendInvoice.execute({'person': person})
# raises InvalidInputsError (name should be string, age is required)
Prevent multiple calls to the database
Often I find that I first need to fetch a model instance before I can pass them as inputs. This means that two database queries are made to fetch the same data.
person = Person.objects.get(name='Mitch') # fetch object from the database
SendInvoice.execute({'person': person.pk}) # the form would have to fetch the data from the database again
Solution: ModelField
I feel like it would be relatively straightforward to implement a custom field that accepts a model instance as the parameter.
How I imagine the API to be:
class SendInvoice(Service):
person = ModelField('people.Person', allow_unsaved=True)
# to use
person = Person.objects.get(name='Mitch')
SendInvoice.execute({'person': person})
If someone could help with this, that'd be great.
So basically a field that does type checking, the allow_unsaved
verification, and then proxies everything else to the underlying object?
Nevermind on the proxying part, thinking of a different project. I took a stab at this, about to submit the PR