I have a number of models that extend from feincms.models.Base, and use the FeinCMS item editor in the admin site (i.e. they all use feincms.admin.item_editor.ItemEditor as their ModelAdmin).
The models have some shared functionality that I want to be able to define in a shared ModelAdmin class that I can then extend for each model.
The problem is, this doesn't play well with FeinCMS extensions, causing
unexpected results, such as duplicate tabs, where the extensions add things to the ModelAdmins more than once.
Is there a way to do this without messing up the extensions?
This is possible, but you have to adopt slightly different syntax. First, an explanation.
The reason that straightforward inheritance of ModelAdmins is broken is because of the two issues with the way FeinCMS extensions manipulate the ModelAdmin classes:
First, any lists or dictionaries attached to the ModelAdmin (e.g. SharedModelAdmin.list_display) are passed by reference, and so shared between multiple ModelAdmins. This means that the extensions can end up performing an operation twice on the same list (even though it's attached to a different ModelAdmin).
While in our admin.py we define the ModelAdmin's settings at the class level, FeinCMS manipulates the ModelAdmin's instance.
So, in order to get it working, we can use the following mixin:
class Faked(object):
"A fake class to use to stand in for True in ExtendableModelAdminMixin."
pass
class ExtendableModelAdminMixin(object):
"""ModelAdmin mixin to allow ModelAdmins to be extended (i.e.
subclassed) without messing
up the Feincms extension registering mechanism.
Technical note: the reason we do this is because otherwise references
get preserved across different ModelAdmins, which means the ModelAdmins
fail Django's checks.
The straightforward declarative syntax of ModelAdmins sets
attributes at the class level, but FeinCMS's
initialize_extensions() method overrides them on the
instance level. So in our mixin we do a deepcopy of any
instance level attributes before initializing the extensions.
"""
def __init__(self, *args, **kwargs):
# Set the _extensions_initialized attribute to prevent
# extensions being initialized just yet
self._extensions_initialized = Faked
super(ExtendableModelAdminMixin, self).__init__(*args, **kwargs)
# Before running extensions, copy any lists so we don't
# preserve references across different ModelAdmin subclasses
# TODO - include any other ModelAdmin properties that
# are causing issues.
for attr_name in ('list_display',
'fieldsets',
'search_fields', 'list_filter'):
original = getattr(self, attr_name, [])
copied_attr = deepcopy(original)
setattr(self, attr_name, copied_attr)
# Now we're ready to initialize extensions
del(self._extensions_initialized)
self.initialize_extensions()
Usage:
class SharedModelAdmin(ExtendableModelAdmin, ItemEditor):
# Declare some defaults here, as usual
list_display = ['field_one', 'field_two']
class MyModelAdmin(SharedModelAdmin):
def __init__(self, *args, **kwargs):
super(MyModelAdmin, self).__init__(*args, **kwargs)
# Override things at the instance level
self.list_display += ['field_three']
Related
I have two Django models, one that stores promotion codes and another that tracks who redeemed a particular promotion code. I'm trying to create an instance method that determines whether or not a specific user has redeemed a specific code. The problem is that I'm not seeing one of my PromotionManager methods, 'redeemed_by_user'. Here are my classes:
from django.contrib.auth.models import User
from django.db import models
class PromotionManager(models.Manager):
def redeemed_by_user(self, promotion, user):
redemption_count = PromotionRedeemed.objects.filter(promotion=promotion, redeemer=user).count()
if redemption_count == 1:
return True
elif redemption_count == 0;
return False
else:
raise ValueError('Invalid redemption count')
class Promotion(models.Model):
code = models.CharField(max_length=16)
objects = PromotionManager()
class PromotionRedeemed(models.Model):
promotion = models.ForeignKey('Promotion')
user = models.ManyToManyField(User)
If I start the the Django extension shell_plus and do the following:
In [1]: user = User.objects.get(username='smith')
In [2]: promotion = Promotion.objects.get(code='bigsale')
and then I do this:
In [3]: dir(promotion)
I don't see the redeemed by user method. I was under the impression that I could move methods like this from my class to a custom manager class. Is that not the case? If so, can anyone explain why? As I understand it, class manager methods are supposed to act on table-level queries and class intance methods on row-level objects. Isn't objects.filter acting on the table level? I tried moving the method back to the Promotion class and I can see it there but I'd just like to understand why I can't see it in the manager class.
Whatever you are seeing is absolutely correct but there is small correction that you should make. When you do a dir(some_instance) then you see a property named objects .
objects = PromotionManager()
This line sets all the manager methods to the objects property so if you try to access the method via some_instance.objects.method_name then you will be able to access it although you can't use it because Django doesn't allow this. You will see an error like manager methods are not accessible from instances. dir is supposed to show only those methods which are accessible from your model instance.
From the docs,
A Manager is the interface through which database query operations are provided to Django models. By default, Django adds a Manager with the name "objects" to every Django model class.
A model’s manager is an object through which Django models perform database queries. Each Django model has at least one manager, and you can create custom managers in order to customize database access.
Adding extra manager methods(custom managers) is the preferred way to add “table-level” functionality to your models whereas for “row-level” functionality use model methods.
Objects is a special attribute through which you query your database. It’s an instance of the class django.db.models.Manager; it’s where all the default methods for performing queries against the entire model class — all(), get(), filter(), etc.
The dir() function, with an argument, attempt to return a list of valid attributes for that object.
If you dir(promotion), promotion is an instance of Promotion Model object. It returns the attributes of a Promotion instance, which includes the objects attribute. But, you defined objects as PromotionManager(), and the redeemed_by_user() is a method of the Manager instance.
If you dir(promotion.objects) , django would raise an error, AttributeError: Manager isn't accessible via Poke instances. Because, its true. objects is a Manager available at the class level, not to the instances.
From the docs,
Managers are accessible only via model classes, rather than from model instances, to enforce a separation between “table-level” operations and “record-level” operations.
So, if you dir(Promotion.objects), you could see all custom methods defined in the Manager instance of the model.
You use dir on the wrong object.
Moreover, you replaced default manager with yours.
The first manager applied to a model class has special meaning for Django, and is a default one, so add own manager this way, please:
objects = models.Manager()
<your_custom_name> = PromotionManager()
The point of my question is the following. I have Django form with a field, which inherits the Selet2 field:
class Select2ModelField(MyBaseSelect2ModelField, AutoModelSelect2Field):
'''
Select2ModelField, that uses ajax to get autocomplete options.
Should be used by default.
'''
widget = Select2ChoiceWidget
class LimitedDepartmentChoiceField(Select2ModelField):
def __init__(self, *args, **kwargs):
super(LimitedDepartmentChoiceField, self).__init__(*args, **kwargs)
And then I use it in my form, creating this field in a views.py, because the content of this field depends on the request data:
form = RepresentativeCreateEditForm(request.POST)
form.fields['department'] = LimitedDepartmentChoiceField(label=u'Department',
queryset=Department.objects.filter(
id__in=all_deps_ids))
The problem is that when two different users enter this page at the same time, they both have the same list of options, exactly the one which the user, who first load the page, has. And this behaviour is incorrect, they should have the different lists of options.
Please, could anyone tell me how I can solve this problem?
It sounds like a value is getting set as a class attribute (somewhere, on one of your classes), rather than as an attribute of a particular instance of a class. There's a lot of inheritance going on, so you might have to do some digging to see exactly where the problem is. My guess is that it's the Select2ChoiceWidget class.
From your code example it looks like all instances of Select2ModelField and its subclasses are sharing a single Select2ChoiceWidget class between themselves. I would think this would be the cause of the problem.
I don't know a whole lot about the Django classes you're using, but maybe try something along these lines?
class Select2ModelField(MyBaseSelect2ModelField, AutoModelSelect2Field):
'''
Select2ModelField, that uses ajax to get autocomplete options.
Should be used by default.
'''
def __init__(self, *args, **kwargs):
# Not sure if this is the proper way to instantiate this class,
# but doing so would help avoid leaking data across the instances
# of Select2ModelField and its subclasses.
self.widget = Select2ChoiceWidget()
# Do the parent class(es) for good measure.
super(Select2ModelField, self).__init__(self, *args, **kwargs)
I'm wondering if it's possible to alter or change a form class before it's instantiated.
As a concrete example, I have a payment form which needs to be modified based on the payment system being used.
Ideally I'd rather not create different form classes and then choose different ones based on the payment system; instead the payment system object will "tell" the form class what changes it need to make -- for example, making certain fields optional or instructing them to use different widgets.
Is this possible? The alternative is to pass an object representing the payment system into the form, and then having it modify the form after instantiation, but that just seems clumsy somehow to have it run in the form class rather than the view. I feel like the Django "view" is closer to a controller, and it seems like this is where something like this should happen. I also feel like modifying it'd be better to modify the form_class object rather than the form instance; I'm not even sure if when you add fields after the fact like this it will handle validation and form fill-in correctly. Will it?
Anywhere, here's some sample code of how it would work passing the payment object into a form instantiation call:
payment_system.py:
class ExamplePaymentSystem(BasePaymentSystem):
def modify_form(self, form):
for fld in self.optional_fields:
form.fields[fld].required = False
…etc…
forms.py:
class ModifiablePaymentForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.payment_system = kwargs.pop("payment_system", None)
super(ModifiablePaymentSystem, self).__init__(*args, **kwargs)
self.payment_system.modify_form(self)
You should not modify global static data (classes defined at module scope are static), because if you run your code in many threads per process (which is often done) one thread may modify form class used by the other threads.
If you your payment systems are static (you do not add new on the fly, while server is running) I'd define one form per payment system.
If not, you can always, define new form types on the fly like that:
def get_form_type(payment_system):
class DynamicForm(BasePaymentForm):
... add change fields etc...
return DynamicForm
or modify instances like that:
class PaymentForm(BasePaymentForm):
def __init__(self, ..., payment_system):
self.fields['foo'].required = False # <--- I'm writing code
#from the memory, so possibly you'll need t edit it
#but this is doable and easy to do.
How to remove field in forms (per OP request).
When you subclass:
This is hard and I think you'll need to browse through form internals and modify them by hand after subclass creation. This is a wild guess...
def get_form_type(payment_system):
class DynamicForm(BasePaymentForm):
... add change fields etc...
del DynamicForm.base_fields['foo']
return DynamicForm
When you modify instance:
I'm not 100% sure, but I peeked into django source code (unfortunately these details are not in docs). But i guess that you should:
class PaymentForm(BasePaymentForm):
def __init__(self, ..., payment_system):
del self.fields['foo']
The fields are a dict (or I guess -- OrderedDict for that matter) and to delete field you need to remove whole key-vaule mapping.
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.
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.