Best practices for passing model names as strings - python

I have two different models that I would like to filter similarly by a common field name at different times, so i've written a single context function that handles both models by taking a string as an argument to use as the model name. Right now I'm using eval(), but something in my gut tells me that's a grave error. Is there a more pythonic way to do what I'm describing?
Here's a shortened version of what my code looks like at the moment:
def reference_context(model, value):
menu = main_menu()
info = company_info()
pages = get_list_or_404(eval(model), category = value)
Secondly, is there a way to pass a keyword in a similar fashion, so I could have something along the lines of:
def reference_context(model, category, value):
menu = main_menu()
info = company_info()
pages = get_list_or_404(eval(model), eval(category) = value)
And commentary on any other issue is welcome and greatly encouraged.

If they are come from the same module (models.py), you can use getattr to retrieve the model class, and kwargs (a dict with double asterisk) this way:
from myapp import models
def reference_context(model, value):
menu = main_menu()
info = company_info()
pages = get_list_or_404(getattr(models, model), **{category: value})

You can use the get_model utility function, which takes the app name and model name.
from django.db.models import get_model
User = get_model("auth", "User") # returns django.contrib.auth.models.User

I don't really see why you need to pass the model as a string - just pass the model reference. E.g.
class ModelA(models.Model):
...
class ModelB(models.Model):
...
def reference_context(model, **kw):
menu = main_menu()
info = company_info()
pages = get_list_or_404(model, **kw)
# ...
In this setup you can pass any model and any query you want, e.g.
reference_context(ModelA, category="Hello")
or
reference_context(ModelB, item__ordered__lte=now)
As explained in my comment, if you really need to map strings to models, use an explicit registry/mapping. This prevents people from manipulating form data which might allow them to create a User in stead of, for example, a "Book":
model_map = dict(book=ModelA, magazine=ModelB)
reference_context(model_map[model_as_string], ...)

Related

Django Rest Framework filter class method with two parameters

I have a model which relates to many models, like this:
class Father:
son = # Foreign key to Son model
class Son:
#property
def son_daughters:
if ... :
obj = TypeA.objects.get(...)
elif ... :
obj = TypeB.objects.get(...)
else:
obj = TypeC.objects.get(...)
return obj
I would like to get Father data from daughter name or type. I have this filter class where I need to send two query set parameters related to daughter in order to get daughter ids and apply it as a filter to Father. This is my filter class:
class FatherFilter(django_filters.rest_framework.FilterSet):
def daughter(self, method_name, args, **kwargs):
print(method_name, args, kwargs)
...
daughter_id = django_filters.NumberFilter(method=daughter)
But when I call this endpoint I just get one query parameter and not all.
Is there a way to get the query parameters inside this method instead of just one?
Thanks in advance.
In order to achieve this, I found that Django Rest Framework has a class that extends from django_filters. This class is called BaseFilterBackend and can be used to extend the default backend for filtering any request. So what I did was adding a class extended from BaseFilterBackend like this:
from rest_framework import filters
class FatherFilterBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
daughter_id = request.query_params.get("daughter_id", None)
daughter_name = request.query_params.get("daughter_name", None)
if daughter_id and daughter_name:
kwargs = {
daughter_name: daughter_id
}
queryset = queryset.filter(**kwargs)
return queryset
This filter will apply before others, so even if you are using a FilterSet class, you will not lose the filters from your BaseFilterBackend. The problem with this solution is that relies in rest_framework.filters package, a filter that its not related to django_filters.
This might not be the best way to achieve this, so if you have better ideas please add them to help others with a similar problem.

Add a dynamically generated Model to a models.py in an Django project

I am generating a Django model based on an abstract model class AbstractAttr and a normal model (let's say Foo).
I want my foo/models.py to look like this:
from bar.models import Attrs
# ...
class Foo(models.Model):
....
attrs = Attrs()
In the Attrs class which mimics a field I have a contribute_to_class that generates the required model using type(). The generated model c is called FooAttr.
Everything works. If I migrate, I see FooAttr appear in the proper table.
EXCEPT FOR ONE THING.
I want to be able to from foo.models import FooAttr. Somehow my generated FooAttr class is not bound to the models.py file in which it is generated.
If I change my models.py to this:
class Foo(models.Model):
# ...
FooAttr = generate_foo_attr_class(...)
it works, but this is not what I want (for example, this forces the dev to guess the generate class name).
Is what I want possible, define the class somewhat like in the first example AND bind it to the specific models.py module?
The project (pre-Alpha) is here (in develop branch):
https://github.com/zostera/django-mav
Some relevant code:
def create_model_attribute_class(model_class, class_name=None, related_name=None, meta=None):
"""
Generate a value class (derived from AbstractModelAttribute) for a given model class
:param model_class: The model to create a AbstractModelAttribute class for
:param class_name: The name of the AbstractModelAttribute class to generate
:param related_name: The related name
:return: A model derives from AbstractModelAttribute with an object pointing to model_class
"""
if model_class._meta.abstract:
# This can't be done, because `object = ForeignKey(model_class)` would fail.
raise TypeError("Can't create attrs for abstract class {0}".format(model_class.__name__))
# Define inner Meta class
if not meta:
meta = {}
meta['app_label'] = model_class._meta.app_label
meta['db_tablespace'] = model_class._meta.db_tablespace
meta['managed'] = model_class._meta.managed
meta['unique_together'] = list(meta.get('unique_together', [])) + [('attribute', 'object')]
meta.setdefault('db_table', '{0}_attr'.format(model_class._meta.db_table))
# The name of the class to generate
if class_name is None:
value_class_name = '{name}Attr'.format(name=model_class.__name__)
else:
value_class_name = class_name
# The related name to set
if related_name is None:
model_class_related_name = 'attrs'
else:
model_class_related_name = related_name
# Make a type for our class
value_class = type(
str(value_class_name),
(AbstractModelAttribute,),
dict(
# Set to same module as model_class
__module__=model_class.__module__,
# Add a foreign key to model_class
object=models.ForeignKey(
model_class,
related_name=model_class_related_name
),
# Add Meta class
Meta=type(
str('Meta'),
(object,),
meta
),
))
return value_class
class Attrs(object):
def contribute_to_class(self, cls, name):
# Called from django.db.models.base.ModelBase.__new__
mav_class = create_model_attribute_class(model_class=cls, related_name=name)
cls.ModelAttributeClass = mav_class
I see you create the model from within models.py, so I think you should be able to add it to the module's globals. How about this:
new_class = create_model_attribute_class(**kwargs)
globals()[new_class.__name__] = new_class
del new_class # no need to keep original around
Thanks all for thinking about this. I have updated the source code of the project at GitHub and added more tests. See https://github.com/zostera/django-mav
Since the actual generation of the models is done outside of foo/models.py (it takes place in mav/models.py, it seems Pythonically impossible to link the model to foo/models.py. Also, after rethinking this, it seems to automagically for Python (explicit is better, no magic).
So my new strategy is to use simple functions, a decorator to make it easy to add mav, and link the generated models to mac/attrs.py, so I can universally from mav.attrs import FooAttr. I also link the generated class to the Foo model as Foo._mav_class.
(In this comment, Foo is of course used as an example model that we want to add model-attribute-value to).

Import issues with custom ModelField that refers to a Model in the same app

I've created a series of custom ModelFields that are simply restricted ForeignKeys. Below you'll find CompanyField. When instantiated, you may provide a type (e.g., Client, Vendor). With a type provided, the field ensures that only values that have the appropriate type are allowed.
The crm app, the one that defines the custom fields, compiles and runs just fine. Eventually I added references to the fields to a different app (incidents) using "from crm import fields". Now I'm seeing a whole bunch of errors like this:
incidents.incident: 'group' has a relation with model Company, which has either not been installed or is abstract.
Here are all the gory details. Please let me know if there's any more information I could provide which may be helpful.
## crm/fields.py
import models as crmmods
class CompanyField(models.ForeignKey):
def __init__(self, *args, **kwargs):
# This is a hack to get South working. In either case, we just need to
# make sure the FK refers to Company.
try:
# kwargs['to'] == crmmods.company doesn't work for some reason I
# still haven't figured out
if str(kwargs['to']) != str(crmmods.Company):
raise AttributeError("Only crm.models.Company is accepted " + \
"for keyword argument 'to'")
except:
kwargs['to'] = 'Company'
# See if a CompanyType was provided and, if so, store it as self.type
if len(args) > 0:
company_type = args[0]
# Type is expected to be a string or CompanyType
if isinstance(company_type, str):
company_type = company_type.upper()
if hasattr(crmmods.CompanyType, company_type):
company_type = getattr(crmmods.CompanyType, company_type)
else:
raise AttributeError(
"%s is not a valid CompanyType." % company_type)
elif not isinstance(company_type, crmmods.CompanyType):
raise AttributeError(
"Expected str or CompanyType for first argument.")
self.type = company_type
else:
self.type = None
super(CompanyField, self).__init__(**kwargs)
def formfield(self, **kwargs):
# Restrict the formfield so it only displays Companies with the correct
# type.
if self.type:
kwargs['queryset'] = \
crmmods.Company.objects.filter(companytype__role=self.type)
return super(CompanyField, self).formfield(**kwargs)
def validate(self, value, model_instance):
super(CompanyField, self).validate(value, model_instance)
# No type set, nothing to check.
if not value or not self.type:
return
# Ensure that value is correct type.
if not \
value.companytype_set.filter(role=self.type).exists():
raise ValidationError("Company does not have the " + \
"required roles.")
## crm/models.py
import fields
class CompanyType(models.Model):
name = models.CharField(max_length=25)
class Company(models.Model):
type = models.ForeignKey(CompanyType)
class Person(models.Model):
name = models.CharField(max_length=50)
company = fields.CompanyField("Client")
## incidents/models.py
from crm import fields as crmfields
class Incident(models.Model):
company = crmfields.CompanyField("Client")
You have a circular package dependency. fields imports models which imports fields which imports models which imports fields . . .
Circular package dependecies are A BAD IDEA(tm). Although it may work in some cases, it doesn't work in your case, for a complicated reason involving metaclasses, which I will spare myself.
EDIT
The reason is that the Django ORM module uses metaclasses to turn its class variables (the fields of the Model) into property descriptors on the object. This is done at class definition time by the metaclass. A class is defined when the module is loaded. For this reason its attributes must also be defined at class loading. This is unlike the code of a method, where references to names are resolved the moment a class is executed.
Now, since you refer to a field object in your class definition from models and back again, this will not work.
If you place all three in the same package, your problem will be solved.
The was fixed, after much toil, simply by changing
kwargs['to'] = 'Company'
to
kwargs['to'] = 'crm.Company'
It seems that when the 'to' argument was evaluated outside of the crm app, it was evaluating it in the context of the incidents app. That is, it was looking for 'incidents.Company' which, as the error message suggested, didn't exist.

WTForms: I can't seem to dynamically give a QuerySelectField a default value

I have a form which looks sort of like this:
class AddProductForm(Form):
title = TextField('Title')
type = QuerySelectField('Type',
query_factory=lambda: ProductType.query.order_by(ProductType.sequence).all())
def __init__(self, formdata=None, obj=None, prefix='', **kwargs):
try:
product_type_id = ProductType.query.filter_by(name=obj['product_type']).one().product_type_id
kwargs.setdefault('type', product_type_id)
except NoResultFound:
pass
Form.__init__(self, formdata, obj, prefix, **kwargs)
As you can see I am trying to set this up to give a sensible default for the product_type when the form is loaded. However, while this kind of code worked for setting a title as an example, it isn't working for the QuerySelectField 'type'. Does anybody have any ideas how I could fix this?
Supposing that this isn't possible, does anybody know how I could dynamically add form elements to a form?
To define a default value in a QuerySelectField of WTForms, you can do it directly in the field definition:
my_field = QuerySelectField(
label=u"My Label",
query_factory=lambda: Model.query(...),
get_pk=lambda item: item.id,
get_label=lambda item: item.name,
allow_blank=True,
default=lambda: Model.query.filter(...).one())
However, it is a little less simple than what it looks. Take this into account:
The default value must be a callable
This callable must return an object instance of your model (not its pk, but the whole object)
It will only be used if no obj or formdata is used to initialize the form instance (in both cases, the default value is ignored)
In your case, since you are initializing the default value using an attribute of your obj that you don't have at hand at initialization time, you may use a factory instead:
def add_product_form_factory(default_type_name):
class AddProductForm(Form):
title = TextField('Title')
type = QuerySelectField('Type',
query_factory=lambda: ProductType.query.order_by(ProductType.sequence).all(),
default=ProductType.query.filter_by(name=default_type_name).one()
)
return AddProductForm
Here you have a function that composes the form according to the parameters (in this case you don't need the default being a callable):
AddProductForm = add_product_form_factory(default_type_name='foo')
form = AddProductForm()
ps. the benefits of a form factory is that you can dynamically work with forms, creating fields or setting choices at run time.
use parameters like:
QuerySelectField(allow_blank=True, blank_text=u'-- please choose --', ...)
can add default select option

Django Passing Custom Form Parameters to Formset

This was fixed in Django 1.9 with form_kwargs.
I have a Django Form that looks like this:
class ServiceForm(forms.Form):
option = forms.ModelChoiceField(queryset=ServiceOption.objects.none())
rate = forms.DecimalField(widget=custom_widgets.SmallField())
units = forms.IntegerField(min_value=1, widget=custom_widgets.SmallField())
def __init__(self, *args, **kwargs):
affiliate = kwargs.pop('affiliate')
super(ServiceForm, self).__init__(*args, **kwargs)
self.fields["option"].queryset = ServiceOption.objects.filter(affiliate=affiliate)
I call this form with something like this:
form = ServiceForm(affiliate=request.affiliate)
Where request.affiliate is the logged in user. This works as intended.
My problem is that I now want to turn this single form into a formset. What I can't figure out is how I can pass the affiliate information to the individual forms when creating the formset. According to the docs to make a formset out of this I need to do something like this:
ServiceFormSet = forms.formsets.formset_factory(ServiceForm, extra=3)
And then I need to create it like this:
formset = ServiceFormSet()
Now how can I pass affiliate=request.affiliate to the individual forms this way?
Official Document Way
Django 2.0:
ArticleFormSet = formset_factory(MyArticleForm)
formset = ArticleFormSet(form_kwargs={'user': request.user})
https://docs.djangoproject.com/en/2.0/topics/forms/formsets/#passing-custom-parameters-to-formset-forms
I would use functools.partial and functools.wraps:
from functools import partial, wraps
from django.forms.formsets import formset_factory
ServiceFormSet = formset_factory(wraps(ServiceForm)(partial(ServiceForm, affiliate=request.affiliate)), extra=3)
I think this is the cleanest approach, and doesn't affect ServiceForm in any way (i.e. by making it difficult to subclass).
I would build the form class dynamically in a function, so that it has access to the affiliate via closure:
def make_service_form(affiliate):
class ServiceForm(forms.Form):
option = forms.ModelChoiceField(
queryset=ServiceOption.objects.filter(affiliate=affiliate))
rate = forms.DecimalField(widget=custom_widgets.SmallField())
units = forms.IntegerField(min_value=1,
widget=custom_widgets.SmallField())
return ServiceForm
As a bonus, you don't have to rewrite the queryset in the option field. The downside is that subclassing is a little funky. (Any subclass has to be made in a similar way.)
edit:
In response to a comment, you can call this function about any place you would use the class name:
def view(request):
affiliate = get_object_or_404(id=request.GET.get('id'))
formset_cls = formset_factory(make_service_form(affiliate))
formset = formset_cls(request.POST)
...
This is what worked for me, Django 1.7:
from django.utils.functional import curry
lols = {'lols':'lols'}
formset = modelformset_factory(MyModel, form=myForm, extra=0)
formset.form = staticmethod(curry(MyForm, lols=lols))
return formset
#form.py
class MyForm(forms.ModelForm):
def __init__(self, lols, *args, **kwargs):
Hope it helps someone, took me long enough to figure it out ;)
I like the closure solution for being "cleaner" and more Pythonic (so +1 to mmarshall answer) but Django forms also have a callback mechanism you can use for filtering querysets in formsets.
It's also not documented, which I think is an indicator the Django devs might not like it as much.
So you basically create your formset the same but add the callback:
ServiceFormSet = forms.formsets.formset_factory(
ServiceForm, extra=3, formfield_callback=Callback('option', affiliate).cb)
This is creating an instance of a class that looks like this:
class Callback(object):
def __init__(self, field_name, aff):
self._field_name = field_name
self._aff = aff
def cb(self, field, **kwargs):
nf = field.formfield(**kwargs)
if field.name == self._field_name: # this is 'options' field
nf.queryset = ServiceOption.objects.filter(affiliate=self._aff)
return nf
This should give you the general idea. It's a little more complex making the callback an object method like this, but gives you a little more flexibility as opposed to doing a simple function callback.
I wanted to place this as a comment to Carl Meyers answer, but since that requires points I just placed it here. This took me 2 hours to figure out so I hope it will help someone.
A note about using the inlineformset_factory.
I used that solution my self and it worked perfect, until I tried it with the inlineformset_factory. I was running Django 1.0.2 and got some strange KeyError exception. I upgraded to latest trunk and it worked direct.
I can now use it similar to this:
BookFormSet = inlineformset_factory(Author, Book, form=BookForm)
BookFormSet.form = staticmethod(curry(BookForm, user=request.user))
As of commit e091c18f50266097f648efc7cac2503968e9d217 on Tue Aug 14 23:44:46 2012 +0200 the accepted solution can't work anymore.
The current version of django.forms.models.modelform_factory() function uses a "type construction technique", calling the type() function on the passed form to get the metaclass type, then using the result to construct a class-object of its type on the fly::
# Instatiate type(form) in order to use the same metaclass as form.
return type(form)(class_name, (form,), form_class_attrs)
This means even a curryed or partial object passed instead of a form "causes the duck to bite you" so to speak: it'll call a function with the construction parameters of a ModelFormClass object, returning the error message::
function() argument 1 must be code, not str
To work around this I wrote a generator function that uses a closure to return a subclass of any class specified as first parameter, that then calls super.__init__ after updateing the kwargs with the ones supplied on the generator function's call::
def class_gen_with_kwarg(cls, **additionalkwargs):
"""class generator for subclasses with additional 'stored' parameters (in a closure)
This is required to use a formset_factory with a form that need additional
initialization parameters (see http://stackoverflow.com/questions/622982/django-passing-custom-form-parameters-to-formset)
"""
class ClassWithKwargs(cls):
def __init__(self, *args, **kwargs):
kwargs.update(additionalkwargs)
super(ClassWithKwargs, self).__init__(*args, **kwargs)
return ClassWithKwargs
Then in your code you'll call the form factory as::
MyFormSet = inlineformset_factory(ParentModel, Model,form = class_gen_with_kwarg(MyForm, user=self.request.user))
caveats:
this received very little testing, at least for now
supplied parameters could clash and overwrite those used by whatever code will use the object returned by the constructor
Carl Meyer's solution looks very elegant. I tried implementing it for modelformsets. I was under the impression that I could not call staticmethods within a class, but the following inexplicably works:
class MyModel(models.Model):
myField = models.CharField(max_length=10)
class MyForm(ModelForm):
_request = None
class Meta:
model = MyModel
def __init__(self,*args,**kwargs):
self._request = kwargs.pop('request', None)
super(MyForm,self).__init__(*args,**kwargs)
class MyFormsetBase(BaseModelFormSet):
_request = None
def __init__(self,*args,**kwargs):
self._request = kwargs.pop('request', None)
subFormClass = self.form
self.form = curry(subFormClass,request=self._request)
super(MyFormsetBase,self).__init__(*args,**kwargs)
MyFormset = modelformset_factory(MyModel,formset=MyFormsetBase,extra=1,max_num=10,can_delete=True)
MyFormset.form = staticmethod(curry(MyForm,request=MyFormsetBase._request))
In my view, if I do something like this:
formset = MyFormset(request.POST,queryset=MyModel.objects.all(),request=request)
Then the "request" keyword gets propagated to all of the member forms of my formset. I'm pleased, but I have no idea why this is working - it seems wrong. Any suggestions?
I spent some time trying to figure out this problem before I saw this posting.
The solution I came up with was the closure solution (and it is a solution I've used before with Django model forms).
I tried the curry() method as described above, but I just couldn't get it to work with Django 1.0 so in the end I reverted to the closure method.
The closure method is very neat and the only slight oddness is that the class definition is nested inside the view or another function. I think the fact that this looks odd to me is a hangup from my previous programming experience and I think someone with a background in more dynamic languages wouldn't bat an eyelid!
I had to do a similar thing. This is similar to the curry solution:
def form_with_my_variable(myvar):
class MyForm(ServiceForm):
def __init__(self, myvar=myvar, *args, **kwargs):
super(SeriveForm, self).__init__(myvar=myvar, *args, **kwargs)
return MyForm
factory = inlineformset_factory(..., form=form_with_my_variable(myvar), ... )
I'm a newbie here so I can't add comment. I hope this code will work too:
ServiceFormSet = formset_factory(ServiceForm, extra=3)
ServiceFormSet.formset = staticmethod(curry(ServiceForm, affiliate=request.affiliate))
as for adding additional parameters to the formset's BaseFormSet instead of form.
based on this answer I found more clear solution:
class ServiceForm(forms.Form):
option = forms.ModelChoiceField(
queryset=ServiceOption.objects.filter(affiliate=self.affiliate))
rate = forms.DecimalField(widget=custom_widgets.SmallField())
units = forms.IntegerField(min_value=1,
widget=custom_widgets.SmallField())
#staticmethod
def make_service_form(affiliate):
self.affiliate = affiliate
return ServiceForm
And run it in view like
formset_factory(form=ServiceForm.make_service_form(affiliate))

Categories

Resources