Correct way to define a ModelForm metaclass - python

What I'm trying to do is create a dynamic ModelForm that generates extra fields based on one of its class-attributes to use in a ModelAdmin. Something like:
class MyModelForm(forms.ModelForm):
config_fields = ('book_type', 'is_featured', 'current_price__is_sale')
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm
In this case, MyModelForm would generate fields based on the config_fields attribute by performing some introspection. My approach so far looks something like this (based on this answer https://stackoverflow.com/a/6581949/677985):
class ConfigForm(type):
def __new__(cls, name, bases, attrs):
if 'config_fields' in attrs:
for config_field in attrs['config_fields']:
# ... (removed for clarity)
attrs.update(fields)
return type(name, bases, attrs)
class MyModelForm(forms.ModelForm):
__metaclass__ = ConfigForm
config_fields = ('book_type', 'is_featured', 'current_price__is_sale')
This approach works well enough, but I'm not quite happy with it for several reasons:
The validation doesn't seem to work, but this is a minor concern for now
I'm not quite sure why the "if config_field in attrs:"-condition is needed, but it is
I would prefer for MyModelForm to inherit instead of setting the __metaclass__ attribute, the base-class could then be easily reused and would allow me to easily override the clean- and __init__-methods.
I tried implementing the third item, the result being that the extra-fields did not show up in the admin-form. I'd be grateful if someone could help me figure this out, or at least point me in the right direction.
I am aware that using a metaclass for this probably overkill, and would guess that part of the problem is that ModelForm already has one or two metaclasses in its inheritance-chain. So if anyone has an alternate solution that accomplishes the same, that would make me just as happy.

I believe that the ModelForm already has a metaclass, but you're overwriting it by setting your own. That's why you're not getting validation or any of the other built in goodness of modelforms.
Instead, you should be able to use type directly to create your ModelForm, which will describe the type you want, but still cause the ModelForms metaclass to do its thing.
Example:
config_fields = ('book_type', 'is_featured', 'current_price__is_sale')
# the below is an example, you need more work to construct the proper attrs
attrs = dict((f, forms.SomeField) for f in config_fields)
ConfigModelForm = type('DynamicModelForm', (forms.ModelForm,), attrs)
class MyModelAdmin(admin.ModelAdmin):
form = ConfigModelForm
You can wrap the first part up in a function if need be, and invoke it for your form attribute in your ModelAdmin.
See my answer here for links and discussion on using type.

How about this,
Basically any form that extends your StepForm will also have the metaclass you wanted in the case below it's StepFormMetaclass, please note that if you have the form defined in some form.py file, you will need to import the form in the ___init___.py so that it will execute it during django starting sequence.
from django.forms.forms import DeclarativeFieldsMetaclass
class StepFormMetaclass(DeclarativeFieldsMetaclass):
.......
def __new__(meta_class, name, bases, attributes):
.....
return DeclarativeFieldsMetaclass.__new__(meta_class, name, bases, attributes)
class StepForm(six.with_metaclass(StepFormMetaclass, forms.Form, StepFormMixin)):
def __init__(self, *args, **kwargs):
super(StepForm, self).__init__(*args, **kwargs)
def as_p(self):
return ......

Related

Is there any way to set all methods in a class as being in an atomic transaction in Django?

While using the custom action method of ViewSet in Django, I separated some business logics into another class, of which name is BusinessService.
The BusinessService class can be used from many other methods, and after some analyses, I found all methods (more than 5) in the class should be in an atomic transaction.
So the easiest but repetitive way might be adding #transaction.atomic decorator above the name of the methods, but as a way to follow the DRY principle, I was struggling to remove the redundant repetitions, but couldn't make it in a simple way.
Is there any to make a whole class in an atomic transaction?
Till now, I tried attaching #transaction.atomic above the name of a class, but, of course, had no success, so I analyzed the decorator, and found out the Atomic class which uses __enter__ and __exit__ for transaction management, which requires with or additional something.
You can try using a custom meta class (See What are metaclasses in Python?) which will decorate all methods of the created class. It will be something like below, (Note that there may be a few areas where this meta class can be refined):
from django.db import transaction
class AtomicTransactionMeta(type):
def __new__(cls, name, bases, dct):
for key, value in dct.items():
if not key.startswith('__') and callable(value):
# If a non-dunder method then decorate
dct[key] = transaction.atomic(value)
return super().__new__(cls, name, bases, dct)
class BusinessService(metaclass=AtomicTransactionMeta):
...

Django abstract base model with custom QuerySet class

I am using an approach similar to T. Stone's answer on this question. However, I have added an abstract base class, so my models.py looks like this:
class CustomQuerySetManager(models.Manager):
"""A re-usable Manager to access a custom QuerySet"""
def __getattr__(self, attr, *args):
try:
return getattr(self.__class__, attr, *args)
except AttributeError:
return getattr(self.get_query_set(), attr, *args)
def get_query_set(self):
return self.model.QuerySet(self.model)
class MyModel(models.Model):
class Meta:
abstract = True
class QuerySet(QuerySet):
def user(self, pub, *args, **kwargs):
return self.filter(publisher=pub, *args, **kwargs)
...some more methods here
class Book(MyModel):
title = models.CharField(max_length=100)
authors = models.ManyToManyField(Author, related_name='book_author')
publisher = models.ForeignKey(Publisher)
publication_date = models.DateField()
objects=models.Manager()
obj=CustomQuerySetManager() #for testing purposes only, this will override objects later
This allows me to get all of the books for a given publisher like such:
p = Publisher.object.get(pk=1)
Book.obj.user(p).all()
I would like to extend this so I can define a custom query in the Book model then pass a Q object to the QuerySet class, so the query "publisher=pub" can be different for different models. I still want to be able to call this like Book.obj.user(p).all(). Somewhere in the Book model I need:
pubQ=Q(publisher=pub)
Where can I put this and how do I pass it to QuerySet defined in the Abstract Base Class, while keeping the code as DRY as possible?
That answer is clever, but it breaks the Python principle of "explicit is better than implicit". My first reaction to your code was to tell you that you can't declare a custom queryset inside your model, but I decided to check the mentioned SO answer to see where you got that idea from. Again, it's clever -- not discounting that, but well-written code is self-documenting and should be able to be picked up by any random Django developer and ran with. That's where peer code-reviews come in handy -- had you had one, you'd have instantly got a WTF with that.
The Django core team does it the following way:
class MyQuerySet(models.query.QuerySet):
def some_method(self, an_arg, another_arg, a_kwarg='some_value'):
# do something
return a_queryset
class MyManager(models.Manager):
def get_query_set(self):
return MyQuerySet(self.model)
def some_method(self, *args, **kwargs):
return self.get_query_set().some_method(*args, **kwargs)
It's DRY in the sense that you don't repeat the actual method definition in the manager. But, it's also explicit -- you know exactly what's going on. It's not as DRY as the method you're referencing, but "explicit is better than implicit". Besides if it's done that way in the actual Django codebase, you can be reasonably assured that it's good practice to do so in your own code. And, it has the side-effect of making it much easier to extend and override in subclasses.

How to manipulate form fields in Django dynamically within ModelAdmin?

I have a field (slug) that is "required" in the model, but want to change the field in the ModelAdmin class to be optional. If the user doesn't fill it in, it is automatically filled in by another field (name).
class SomeModel(model.Model):
name = model.CharField(max_length=255)
slug = model.SlugField(unique=True, max_length=255)
I tried to do this various ways, such as overriding get_form() within ModelAdmin or using the ModelForm class and specifying the form specifically.
class SomeModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super(self.__class__, self).get_form(request, obj, **kwargs)
form.slug.required = False
return form
However, neither solution worked for me. Beyond manually creating the form, is there any other quicker solution?
I have a lot of these forms, and doing it by hand might be tedious and hard to maintain.
Found this page through Google when wrestling with the same problem myself. The following will also work in the ModelAdmin:
def get_form(self, *args, **kwargs):
form = super(SomeModelAdmin, self).get_form(*args, **kwargs)
form.base_fields['slug'].required = False
return form
Subsequent forms created from the updated ModelFormMetaclass will have the slug field unrequired.
This works better in my situation, where I have only a single class in which I need to unrequire the field, and don't need to do any data transformation on save. GoogleDroid's solution is better if you have a lot of classes, or where the data transformations are necessary.
In your get_form method, form.fields['slug'].required should work.
But the proper way to do this is to simply provide a custom ModelForm.
class SomeModelForm(forms.ModelForm):
slug = forms.CharField(required=False)
class SomeModelAdmin(admin.ModelAdmin):
form = SomeModelForm
Incidentally, please don't do super(self.__class__, self). You should always explicitly name the current class when using super, otherwise any subclass that inherits from yours and in turn calls super will break.
Edit form.fields, not forms.fields.
By saying self.__class__, you are explicitly stopping Python from working out the inheritance - because it always refers to the concrete class - ie the bottom of the inheritance tree. But if your method is the middle of that tree, then referring to the concrete class in super is wrong - because you want it to call the next level up from where you are, not one up from the bottom. That's why you should always name the class you're in - in this case, super(SomeModelAdmin, self).
I just wanted to report back in case others might find this useful.
I was never able to in get_form method do form.fields['slug'].required and never figured out why. However, I solved my problem by creating a new form inheriting from ModelForm.
I had to override init() to set self.fields['slug'].required = False after calling the parent constructor, then override clean_slug() to modify the slug field content if required by accessing self.data['slug'].
Hope this helps someone

Passing data into django forms

class Test(forms.Form):
def set_choices(self, choices):
self.choices = choices
def get_choices(self):
return self.choices
options = forms.ChoiceField(choices=get_choices())
f = Test()
f.set_choices(...)
Why isn't this possible?
How else can I achieve the goal of passing data into class Test?
Thanks in advance.
This is a basic Python issue. You need to think about the order these commands are executed in, and their scope.
First, you define a form class called Test. That class has three attributes: a set_choices method, a get_choices method, and an options field. These definitions are evaluated when the class itself is defined. The definition of options calls get_choices(). However, there is no get_choices method in scope at that point, because the class is not yet defined.
Even if you somehow managed to sort out the scope issue, this would still not do what you want, because the definition of choices for options is done at define time. Even if you later call set_choices, options still has the value of get_choices that was returned when the field was defined.
So, what do you actually want to do? It seems like you want to set dynamic choices on the options field. So, you should override the __init__ method and define them there.
class Test(forms.Form):
options = forms.ChoiceField(choices=())
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices', None)
super(Test, self).__init__(*args, **kwargs)
if choices is not None:
self.fields['options'].choices = choices
Extending __init__ is a good way to add options to ChoiceField dynamically as Daniel Roseman explains in his answer.
Just to add to that ... adding options at run time is hackish (at best). Here is the note about best practices (straight from Django ChoiceField documentation) -
Finally, note that choices can be any
iterable object -- not necessarily a
list or tuple. This lets you construct
choices dynamically. But if you find
yourself hacking choices to be
dynamic, you're probably better off
using a proper database table with a
ForeignKey. choices is meant for
static data that doesn't change much,
if ever.

Difference between returning modified class and using type()

I guess it's more of a python question than a django one, but I couldn't replicate this behavior anywhere else, so I'll use exact code that doesn't work as expected.
I was working on some dynamic forms in django, when I found this factory function snippet:
def get_employee_form(employee):
"""Return the form for a specific Board."""
employee_fields = EmployeeFieldModel.objects.filter(employee = employee).order_by ('order')
class EmployeeForm(forms.Form):
def __init__(self, *args, **kwargs):
forms.Form.__init__(self, *args, **kwargs)
self.employee = employee
def save(self):
"Do the save"
for field in employee_fields:
setattr(EmployeeForm, field.name, copy(type_mapping[field.type]))
return type('EmployeeForm', (forms.Form, ), dict(EmployeeForm.__dict__))
[from :http://uswaretech.com/blog/2008/10/dynamic-forms-with-django/]
And there's one thing that I don't understand, why returning modified EmployeeForm doesn't do the trick?
I mean something like this:
def get_employee_form(employee):
#[...]same function body as before
for field in employee_fields:
setattr(EmployeeForm, field.name, copy(type_mapping[field.type]))
return EmployeeForm
When I tried returning modified class django ignored my additional fields, but returning type()'s result works perfectly.
Lennart's hypothesis is correct: a metaclass is indeed the culprit. No need to guess, just look at the sources: the metaclass is DeclarativeFieldsMetaclass currently at line 53 of that file, and adds attributes base_fields and possibly media based on what attributes the class has at creation time. At line 329 ff you see:
class Form(BaseForm):
"A collection of Fields, plus their associated data."
# This is a separate class from BaseForm in order to abstract the way
# self.fields is specified. This class (Form) is the one that does the
# fancy metaclass stuff purely for the semantic sugar -- it allows one
# to define a form using declarative syntax.
# BaseForm itself has no way of designating self.fields.
__metaclass__ = DeclarativeFieldsMetaclass
This implies there's some fragility in creating a new class with base type -- the supplied black magic might or might not carry through! A more solid approach is to use the type of EmployeeForm which will pick up any metaclass that may be involved -- i.e.:
return type(EmployeeForm)('EmployeeForm', (forms.Form, ), EmployeeForm.__dict__)
(no need to copy that __dict__, btw). The difference is subtle but important: rather than using directly type's 3-args form, we use the 1-arg form to pick up the type (i.e., the metaclass) of the form class, then call THAT metaclass in the 3-args form.
Blackly magicallish indeed, but then that's the downside of frameworks which do such use of "fancy metaclass stuff purely for the semantic sugar" &c: you're in clover as long as you want to do exactly what the framework supports, but to get out of that support even a little bit may require countervailing wizardry (which goes some way towards explaining why often I'd rather use a lightweight, transparent setup, such as werkzeug, rather than a framework that ladles magic upon me like Rails or Django do: my mastery of deep black magic does NOT mean I'm happy to have to USE it in plain production code... but, that's another discussion;-).
I just tried this with straight non-django classes and it worked. So it's not a Python issue, but a Django issue.
And in this case (although I'm not 100% sure), it's a question of what the Form class does during class creation. I think it has a meta class, and that this meta class will finalize the form initialization during class creation. That means that any fields you add after class creation will be ignored.
Therefore you need to create a new class, as is done with the type() statement, so that the class creation code of the meta class is involved, now with the new fields.
It's worth noting that this code snippet is a very poor means to the desired end, and involves a common misunderstanding about Django Form objects - that a Form object should map one-to-one with an HTML form. The correct way to do something like this (which doesn't require messing with any metaclass magic) is to use multiple Form objects and an inline formset.
Or, if for some odd reason you really want to keep things in a single Form object, just manipulate self.fields in the Form's __init__ method.

Categories