How can one accomplish class-based default value in following scheme? I mean, I would like to inherited classes set default value for "number" differently:
class OrderDocumentBase(PdfPrintable):
number = models.PositiveIntegerField(default=self.create_number())
#classmethod
def create_number(cls):
raise NotImplementedError
class Invoice(OrderDocumentBase):
#classmethod
def create_number(cls):
return 1
class CreditAdvice(OrderDocumentBase):
#classmethod
def create_number(cls):
return 2
I have looked at this stackoverflow question, but it doesn't address the same problem. The only thing I thought would work was overloading OrderDocumentBase's __init__ method like this:
def __init__(self, *args, **kwargs):
"""
Overload __init__ to enable dynamic set of default to number
"""
super(OrderDocumentBase, self).__init__(*args, **kwargs)
number_field = filter(lambda x: x.name == 'number', self._meta.fields)[0]
number = self.__class__.create_number()
number_field.default = number
This works, but only partially and behaves quite wierdly. In admin interface, I can see the default being set only after second or latter page refresh. On first try, None is being set :(
Second possibility is redefinition of number field in each class, but that doesn't seem too much pretty. Is there any other way?
Can someone help?
It does feel nicer to do this via default=, but anything you use there doesn't have a way to get at your class or specific model. To have it show up properly in places like the admin, you could set it in init() instead of save().
class OrderDocumentBase(PdfPrintable):
number = models.PositiveIntegerField()
def __init__(self, *args, **kwargs):
super(OrderDocumentBase, self).__init__(*args, **kwargs)
if not self.pk and not self.number:
self.number = self.DEFAULT_NUMBER
class Invoice(OrderDocumentBase):
DEFAULT_NUMBER = 2
class CreditAdvice(OrderDocumentBase):
DEFAULT_NUMBER = 3
There are a couple of problems here. First, the self.method is not going to work. There is no self in the context of the body of the class, which is where you are declaring the PositiveIntegerField.
Second, passing a callable will not work as the callable gets bound at compile time and does not change at runtime. So if you define say,
class OrderDocumentBase(PdfPrintable):
create_number = lambda: return 0
number = models.PositiveIntegerField(default=create_number)
class Invoice(OrderDocumentBase):
create_number = lambda: return 1
All Invoice instances will still get 0 as default value.
One way I can think of to tackle this is to override the save() method. You can check if the number has not been supplied and set it to a default before saving.
class OrderDocumentBase(PdfPrintable):
number = models.PositiveIntegerField()
def save(self, *args, **kwargs):
if not self.number:
self.number = self.DEFAULT
super(OrderDocumentBase, self).save(*args, **kwargs)
class Invoice(OrderDocumentBase):
DEFAULT = 2
class CreditAdvice(OrderDocumentBase):
DEFAULT = 3
I tested the above with a small change (made OrderDocumentBase abstract as I did not have PdfPrintable) and it worked as expected.
Related
I'm trying to create a form wizard that would contain variable number of questionnaires, depending on how many of them are present in the database and marked as active. For every one of them I am using this form, and the same template. This is my form:
class QuestionnaireForm(forms.Form):
def __init__(self, slug='', *args, **kwargs):
super(QuestionnaireForm, self).__init__(*args, **kwargs)
degrees = Questionnaire.objects.get(slug=slug).degrees
VALUES = ()
for i in range(1,degrees+1):
VALUES += ((i,i),)
items = Item.objects.filter(Q(scales__questionnaire__slug=slug)|Q(scales__slug=slug)).order_by('ord_number')
for item in items:
self.fields[unicode(item.id)] = forms.ChoiceField(
choices=VALUES,
required=True,
widget=RadioSelect,
error_messages={'required': 'Bro, you have to answer that.'},
label = item.name)
Now, since I need to provide a slug to this form in order to get different questionnaire every time, I tried something like this:
class Testing(SessionWizardView):
form_list = [QuestionnaireForm(slug=questionnaire.slug) for questionnaire in Questionnaire.objects.filter(active=True)]
template_name = 'index.html'
success_url = '/'
It gives me the following error: issubclass() arg 1 must be a class. Obviously, I'm not passing slug in the proper place, but I'm not sure where I should be passing it. My guess is that I should be overriding some SessionWizardView method, but I'm not having any luck (skill?) so far.
After doing some more research, I finally managed to do what I wanted. Solution proposed here did the trick, even though I'm not sure why is this working.
So, here's my modified code. Class generator from the cited answer:
def class_generator(cls, **additionalkwargs):
class ClassWithKwargs(cls):
def __init__(self, *args, **kwargs):
kwargs.update(additionalkwargs)
super(ClassWithKwargs, self).__init__(*args, **kwargs)
return ClassWithKwargs
Which I then applied like this:
class Testing(SessionWizardView):
form_list = [class_generator(QuestionnaireForm(slug=questionnaire.slug)) for questionnaire in Questionnaire.objects.filter(active=True)]
template_name = 'index.html'
success_url = '/'
Since I'm not a pro (more of like an advancing noob), if there's someone who could clarify what's happening here, that would be nice for the sake of future readers.
It seems like if a ModelForm is given an instance, it ignores any values you provide for initial and instead sets it to the value of the instance -- even if that instance is an empty model record.
Is there any way to create a form with an instance and have it set initial data?
I need it because I'm saving related records and they don't appear to save correctly unless the ModelForm is given an instance when created.
I'm sure the answer to this is straightforward and I'm just missing something obvious.
Here is the relevant code:
in the view:
form = form_class(person=person, conference=conference, initial=initial, instance=registration)
where form_class is RegistrationForm and then in the registration form:
class RegisterForm(forms.ModelForm):
... fields here ...
def __init__(self, *args, **kwargs):
... other code ...
self.person = kwargs.pop('person')
super(RegisterForm, self).__init__(*args, **kwargs)
for key, in self.fields.keys():
if hasattr(self.person, key):
self.fields[k].initial = getattr(self.person, key)
Then when I call the field, the related fields are empty.
Figured this out after a little bit of googling.
You have to set the initial value before calling super.
So instead of looping through self.fields.keys(), I had to type out the list of fields that I wanted and looped through that instead:
class RegisterForm(forms.ModelForm):
... fields here ...
initial_fields = ['first_name', 'last_name', ... ]
def __init__(self, *args, **kwargs):
... other code ...
self.person = kwargs.pop('person')
for key in self.initial_fields:
if hasattr(self.person, key):
self.fields[k].initial = getattr(self.person, key)
super(RegisterForm, self).__init__(*args, **kwargs)
#Daria rightly points out that you don't have self.fields before calling super. I'm pretty sure this will work:
class RegisterForm(forms.ModelForm):
... fields here ...
initial_fields = ['first_name', 'last_name', ... ]
def __init__(self, *args, **kwargs):
... other code ...
initial = kwargs.pop('initial', {})
self.person = kwargs.pop('person')
for key in self.initial_fields:
if hasattr(self.person, key):
initial[key] = initial.get(key) or getattr(self.person, key)
kwargs['initial'] = initial
super(RegisterForm, self).__init__(*args, **kwargs)
In this version, we use the initial argument to pass the values in. It's also written so that if we already have a value in initial for that field, we don't overwrite it.
Sounds to me that you may be looking for a bound form. Not entirely sure, I'm trying to unpick a similar issue:
Django forms can be instantiated with two arguments which control this kind of thing. As I understand it:
form = MyForm(initial={...}, data={...}, ...)
initial will set the possible values for the fields—like setting a queryset—data will set the actual (or selected) values of a form and create a bound form. Maybe that is what you want. Another, tangental, point you might find interesting is to consider a factory method rather than a constructor, I think the syntax is more natural:
class MyForm(forms.ModelForm):
...
#staticmethod
def makeBoundForm(user):
myObjSet = MyObject.objects.filter(some_attr__user=user)
if len(myObjSet) is not 0:
data = {'myObject': myObjSet[0]}
else:
raise ValueError()
initial = {'myObject': myObjSet}
return MyForm(initial=initial, data=data)
You can also pass extra variables to the class when initializing it. The values you pass can then override initial or POST data.
class RegisterForm(forms.ModelForm):
... fields here ...
def __init__(self, person, conference, *args, **kwargs):
... other code ...
super(RegisterForm, self).__init__(*args, **kwargs)
self.fields['person'] = person
self.fields['conference'] = conference
form = RegisterForm(person, conference, initial=initial, instance=registration)
Use ModelAdmin.get_changeform_initial_data. For example, if you add initial data for form field "report_datetime"
def get_changeform_initial_data(self, request):
initial_data = super().get_changeform_initial_data(request)
initial_data.update(report_datetime=<my_initial_datetime>)
return initial_data
Works for 3.2+. I'm not sure about older versions.
See django docs
I need to pass an instance variable (self.rank) to be used by a class variable (provider) (see the commented out line below).
Commented out, the code below works. But I'm pretty sure I shouldn't be trying to pass an instance variable up to a class variable anyway. So I'm dumbfounded as to how to accomplish my goal, which is to dynamically filter down my data in the ModelChoiceField.
As you can see, I already overrided ModelChoiceField so I could beautify the usernames. And I also subclassed my basic SwapForm because I have several other forms I'm using (not shown here).
Another way of saying what I need ... I want the value of request.user in my Form so I can then determine the rank of that user and then filter out my Users by rank to build a smaller ModelChoiceField (that looks good too). Note that in my views.py, I call the form using:
form = NewSwapForm(request.user)
or
form = NewSwapForm(request.user, request.POST)
In forms.py:
from myapp.swaps.models import Swaps
from django.contrib.auth.models import User
class UserModelChoiceField(forms.ModelChoiceField):
""" Override the ModelChoiceField to display friendlier name """
def label_from_instance(self, obj):
return "%s" % (obj.get_full_name())
class SwapForm(forms.ModelForm):
""" Basic form from Swaps model. See inherited models below. """
class Meta:
model = Swaps
class NewSwapForm(SwapForm):
# Using a custom argument 'user'
def __init__(self, user, *args, **kwargs):
super(NewSwapForm, self).__init__(*args, **kwargs)
self.rank = User.objects.get(id=user.id).firefighter_rank_set.get().rank
provider = UserModelChoiceField(User.objects.all().
order_by('last_name').
filter(firefighter__hirestatus='active')
### .filter(firefighter_rank__rank=self.rank) ###
)
class Meta(SwapForm.Meta):
model = Swaps
fields = ['provider', 'date_swapped', 'swap_shift']
Thanks!
You can't do it that way, because self doesn't exist at that point - and even if you could, that would be executed at define time, so the rank would be static for all instantiations of the form.
Instead, do it in __init__:
provider = UserModelChoiceField(User.objects.none())
def __init__(self, user, *args, **kwargs):
super(NewSwapForm, self).__init__(*args, **kwargs)
rank = User.objects.get(id=user.id).firefighter_rank_set.get().rank # ??
self.fields['provider'].queryset = User.objects.order_by('last_name').filter(
firefighter__hirestatus='active', firefighter_rank__rank=rank)
I've put a question mark next to the rank line, because rank_set.get() isn't valid... not sure what you meant there.
I am trying to add dynamically new form fields (I used this blog post), for a form used in admin interface :
class ServiceRoleAssignmentForm(forms.ModelForm):
class Meta:
model = ServiceRoleAssignment
def __init__(self, *args, **kwargs):
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)
self.fields['test'] = forms.CharField(label='test')
class ServiceRoleAssignmentAdmin(admin.ModelAdmin):
form = ServiceRoleAssignmentForm
admin.site.register(ServiceRoleAssignment, ServiceRoleAssignmentAdmin)
However, no matter what I try, the field doesn't appear on my admin form ! Could it be a problem related to the way admin works ? Or to ModelForm ?
Thank for any help !
Sébastien
PS : I am using django 1.3
When rendering your form in template, fields enumerating from fieldsets variable, not from fields. Sure you can redefine fieldsets in your AdminForm, but then validations will fail as original form class doesn't have such field. One workaround I can propose is to define this field in form definition statically and then redefine that field in form's init method dynamically. Here is an example:
class ServiceRoleAssignmentForm(forms.ModelForm):
test = forms.Field()
class Meta:
model = ServiceRoleAssignment
def __init__(self, *args, **kwargs):
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)
# Here we will redefine our test field.
self.fields['test'] = forms.CharField(label='test2')
I actually have a the same issue which I'm working through at the moment.
While not ideal, I have found a temporary workaround that works for my use case. It might be of use to you?
In my case I have a static name for the field, so I just declared it in my ModelForm. as normal, I then override the init() as normal to override some options.
ie:
def statemachine_form(for_model=None):
"""
Factory function to create a special case form
"""
class _StateMachineBaseModelForm(forms.ModelForm):
_sm_action = forms.ChoiceField(choices=[], label="Take Action")
class Meta:
model = for_model
def __init__(self, *args, **kwargs):
super(_StateMachineBaseModelForm, self).__init__(*args, **kwargs)
actions = (('', '-----------'),)
for action in self.instance.sm_state_actions():
actions += ((action, action),)
self.fields['_sm_action'] = forms.ChoiceField(choices=actions,
label="Take Action")
if for_model: return _StateMachineBaseModelForm
class ContentItemAdmin(admin.ModelAdmin):
form = statemachine_form(for_model=ContentItem)
Now as I mentioned before, this is not entirely 'dynamic', but this will do for me for the time being.
I have the exact same problem that, if I add the field dynamically, without declaring it first, then it doesn't actually exist. I think this does in fact have something to do with the way that ModelForm creates the fields.
I'm hoping someone else can give us some more info.
Django - Overriding get_form to customize admin forms based on request
Try to add the field before calling the super.init:
def __init__(self, *args, **kwargs):
self.fields['test'] = forms.CharField(label='test')
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)
I've created a model, and I'm rendering the default/unmodified model form for it. This alone generates 64 SQL queries because it has quite a few foreign keys, and those in turn have more foreign keys.
Is it possible to force it to always (by default) perform a select_related every time one of these models are returned?
You can create a custom manager, and simply override get_queryset for it to apply everywhere. For example:
class MyManager(models.Manager):
def get_queryset(self):
return super(MyManager, self).get_queryset().select_related('foo', 'bar')
(Prior to Django 1.6, it was get_query_set).
Here's also a fun trick:
class DefaultSelectOrPrefetchManager(models.Manager):
def __init__(self, *args, **kwargs):
self._select_related = kwargs.pop('select_related', None)
self._prefetch_related = kwargs.pop('prefetch_related', None)
super(DefaultSelectOrPrefetchManager, self).__init__(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
qs = super(DefaultSelectOrPrefetchManager, self).get_queryset(*args, **kwargs)
if self._select_related:
qs = qs.select_related(*self._select_related)
if self._prefetch_related:
qs = qs.prefetch_related(*self._prefetch_related)
return qs
class Sandwich(models.Model):
bread = models.ForeignKey(Bread)
extras = models.ManyToManyField(Extra)
# ...
objects = DefaultSelectOrPrefetchManager(select_related=('bread',), prefetch_related=('extras',))
Then you can re-use the manager easily between model classes. As an example use case, this would be appropriate if you had a __unicode__ method on the model which rendered a string that included some information from a related model (or anything else that meant a related model was almost always required).
...and if you really want to get wacky, here's a more generalized version. It allows you to call any sequence of methods on the default queryset with any combination of args or kwargs. There might be some errors in the code, but you get the idea.
from django.db import models
class MethodCalls(object):
"""
A mock object which logs chained method calls.
"""
def __init__(self):
self._calls = []
def __getattr__(self, name):
c = Call(self, name)
self._calls.append(c)
return c
def __iter__(self):
for c in self._calls:
yield tuple(c)
class Call(object):
"""
Used by `MethodCalls` objects internally to represent chained method calls.
"""
def __init__(self, calls_obj, method_name):
self._calls = calls_obj
self.method_name = method_name
def __call__(self, *method_args, **method_kwargs):
self.method_args = method_args
self.method_kwargs = method_kwargs
return self._calls
def __iter__(self):
yield self.method_name
yield self.method_args
yield self.method_kwargs
class DefaultQuerysetMethodCallsManager(models.Manager):
"""
A model manager class which allows specification of a sequence of
method calls to be applied by default to base querysets.
`DefaultQuerysetMethodCallsManager` instances expose a property
`default_queryset_method_calls` to which chained method calls can be
applied to indicate which methods should be called on base querysets.
"""
def __init__(self, *args, **kwargs):
self.default_queryset_method_calls = MethodCalls()
super(DefaultQuerysetMethodCallsManager, self).__init__(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
qs = super(DefaultQuerysetMethodCallsManager, self).get_queryset(*args, **kwargs)
for method_name, method_args, method_kwargs in self.default_queryset_method_calls:
qs = getattr(qs, method_name)(*method_args, **method_kwargs)
return qs
class Sandwich(models.Model):
bread = models.ForeignKey(Bread)
extras = models.ManyToManyField(Extra)
# Other field definitions...
objects = DefaultQuerysetMethodCallsManager()
objects.default_queryset_method_calls.filter(
bread__type='wheat',
).select_related(
'bread',
).prefetch_related(
'extras',
)
The python-mock-inspired MethodCalls object is an attempt at making the API more natural. Some might find that a bit confusing. If so, you could sub out that code for an __init__ arg or kwarg that just accepts a tuple of method call information.
Create a custom models.Manager and override all the methods (filter, get etc.) and append select_related onto every query. Then set this manager as the objects attribute on the model.
I would recommend just going through your code and adding the select_related where needed, because doing select_related on everything is going to cause some serious performance issues down the line (and it wouldn't be entirely clear where it's coming from).