Edit related object using ModelFormSet - django

I was using a model formset to generate a table of forms for a list of objects.
Forms:
class UserTypeModelForm(ModelForm):
account_type = ChoiceField(label='User type',
choices=ACCOUNT_OPTIONS, required=False)
class Meta:
model = get_user_model()
fields = ('account_type',)
UserTypeModelFormSet = modelformset_factory(get_user_model(),
form=UserTypeModelForm,
extra=0)
View:
formset = UserTypeModelFormSet(queryset=users, prefix='formset')
Now my client wants to be able to modify a related field: user.employee_profile.visible.
I tryed to add a field to the form, and then passing "initial" and "queryset" to the formset, but It looks like it just takes one.
How would you guys do this?
Thanks

with model formsets, the initial values only apply to extra forms, those that aren’t bound to an existing object instance.
Django docs
The queryset provides the selected/entered values for the bound fields, the initial for the extra fields (in your case 0).
But you can override the initial value in e.g. your views when you created a field called employee in this case:
for form in forms:
# Don't override a selected value.
if not form.fields['employee'].initial:
form.fields['employee'].initial = my_init

Related

Django restrict options of ManyToMany field in ModelForm based on model instance

I want to show only options already stored in models' ManyToManyField.
I have model Order which I want to have a Model based form like this:
class OrderForm(ModelForm):
class Meta:
model = Order
fields = ['amount', 'color']
Now I do not want to display all colors as choices, but instead only color instances saved in ManyToManyField of another model. The other model is Design:
class Design(models.Model):
color = models.ManyToManyField('maker.Color')
# ...
Is this at all possible while using ModelForm?
Attempt
I have tried doing it by having a ModelForm of Design and setting instance:
class ColorForm(ModelForm):
class Meta:
model = Design
fields = ['color']
And then in view:
color_form = ColorForm(instance=design)
But I don't exactly understand what setting instance does, and I think instance is not what I am looking for as it still lists all colors.
The instance setting has nothing to do with limiting the choices. In essence, it simply populates the form's values with the ones from a specific record. You usually provide an instance in an edit operation, whereas you skip it in an add operation.
The representation of a models.ManyToManyField in the ModelForm is a forms.ChoiceField for which you can simply override its queryset property, and specify the queryset you desire.
Therefore, in your view:
form = OrderForm()
form.fields['color'].queryset = Design.object.all() # for example

Django ModelForm individual fields not validated when overriding clean() on Model

I am using standard Django Models and ModelForms.
On the Model, I am overriding the clean() method to check that 2 fields in combination are valid. I am having validators[], not nulls etc defined on other fields.
In the ModelForm, I am not modifying anything. I only set the corresponding Model in the ModelForm's Meta class.
When I try to create a new object through the form, the individual fields in the ModelForm/Model are not validated when I call form.is_valid() in the View. According to the docs (https://docs.djangoproject.com/en/1.6/ref/models/instances/#validating-objects) the is_valid() method on the Form should call the Model's clean_fields() method (first).
This doesn't seem to work when you submit a form without a Model instance (or a new instance not in the db). When I'm editing an existing object, all is well. It nicely triggers invalid values in individual fields before calling the Model's clean() method.
When I remove the overridden clean() method from my Model, all is well. Individual fields are validated both when creating new objects and editing existing ones.
I have also tested this with the admin module's forms. It has exactly the same behaviour.
So the question is, why does overriding the clean() method on my Model prevent the ModelForm validating the individual fields before calling the clean() method to test additional cross-field stuff???
Note that I am not validating the ModelForm. All validation is on the Model itself.
Model:
class Survey(models.Model):
from_date = models.DateField(null=False, blank=False, validators=[...])
to_date = models.DateField(null=False, blank=False, validators=[...])
(...)
def clean(self):
errors = []
# At this point I expect self.to_date already to be validated for not null etc.
# It isn't for new Model instances, only when editing an existing one
if self.to_date < self.from_date:
errors.append(ValidationError("..."))
ModelForm:
class TestForm(ModelForm):
class Meta:
model = Survey
View (to render blank form for new Model data entry):
(...)
if request.method == "POST":
survey_form = TestForm(request.POST)
if '_save' in request.POST:
if survey_form.is_valid():
survey_form.save()
return HttpResponseRedirect(next_url)
else:
return HttpResponseRedirect(next_url)
else:
survey_form = TestForm()
context = {'form': survey_form}
(...)

Django formset validation: automatically fix form validation errors

In an my model, I've the following
--- models.py ---
class A(models.Model):
my_Bs = models.ManyToManyField('B', through='AlinksB')
...
class B(models.Model):
...
class AlinksB(models.Model):
my_A = models.ForeignKey(A)
my_B = models.models.ForeignKey(B)
order = models.IntegerField()
So is the corresponding admin (A admin view has an inline to link B instances, and I prepared the required to custom this inline's formset and forms):
--- admin.py ---
class AlinksBInlineForm(forms.ModelForm):
class Meta:
model = AlinksB
class AlinksBInlineFormset(forms.models.BaseInlineFormSet): # there also is a BaseModelFormset
form = AlinksBInlineForm
class AlinksBInline(admin.TabularInline):
formset = AlinksBInlineFormset
model = AlinksB
class AAdmin(admin.ModelAdmin):
form = AForm
inlines = (AlinksBInline,)
...
class BAdmin(admin.ModelAdmin):
...
Now to custom the forms validation, nothing difficult: just override the "clean" method of the form object. If you want many different forms in the formset, I think you just have to change some manually in the "init" method of the formset. But what about programatically validating all the forms when we clean the formset, and that only under some conditions.
In my case: how to automatically set the "order" field (in the inline of A admin view) with an autoincrement if all the orders (inline rows to remove excluded) are empty ?!
I just spent a lot of time Googling about trying to perform automatic form cleaning during a formset validation in Django Framework. After a few days a couldn't figure a solution so I started looking right into Django's source code to see how work fields, widgets, forms and formsets.
Here is what I understood:
-All the data POSTed by the user when he submits the formset it stored in the "data" attribute of the formset. This attribute is very ugly and cannot be directly used.
- The form is just a wrapper for fields (it calls all the fields' clean methods and fill error buffers, and only a few more)
-The form fields have a widget. This widget allow getting back the field's raw value from the "data" attribute of the formset
form.add_prefix('field name') # returns the 'field prefix', the key of formset.data used to retrieve the field's raw value
form.fields['field name'].widget.value_from_datadict(form.data, form.files, 'field prefix') # returns the raw value
-The form fields also have a method to transform the raw value into a right python value (in my case: order is an integer, or None if the field has been left empty)
form.fields['field name'].to_python(raw_value) # returns a value with the right type
-You can change the value of one of the fields from the formset with the following code
form.data.__setitem__('field prefix', value) # code to update an iterable knowing the key to change
-Once you have modified the fields value, you can call the "full_clean" method of the forms to retry cleaning them (this will remove the previous errors).
-Once you have validated again the forms, you can retry validating the formset with its "full_clean" method too. But take care to avoid infinite loops
-The forms clean data can only be used has a read-only data, to add more error messages in the form or the formset
An other solution would be to manually change the "form.clean_data" attribute, and clean the formset.errors and all the form.errors
Hope it could help somebody in the same situation as me !
Ricola3D

django: use a queryset as modelform initial data

I'm making a settings interface which works by scanning for a settings folder in the installed applications, scanning for settings files, and finally scanning for ModelForms.
I'm at the last step now. The forms are properly found and loaded, but I now need to provide the initial data. The initial data is to be pulled from the database, and, as you can imagine, it must be limited to the authenticated user (via request.user.id).
Keep in mind, this is all done dynamically. None of the names for anything, nor their structure is known in advanced (I really don't want to maintain a boring settings interface).
Here is an example settings form. I just pick the model and which fields the user can edit (this is the extent to which I want to maintain a settings interface).
class Set_Personal_Info(forms.ModelForm):
class Meta:
model = UserProfile
fields = ('nick_name', 'url')
I've looked at modelformset_factory which almost does what I want to do, but it only seems to work with results of two or more. (Here, obj is one of the settings forms)
Formset = modelformset_factory(obj.Meta.model, form=obj)
Formset(queryset=obj.Meta.model.objects.filter(id=request.user.id))
I can't filter the data, I have to get one, and only one result. Unfortunately I can't use get()
Formset = modelformset_factory(obj.Meta.model, form=obj)
Formset(queryset=obj.Meta.model.objects.get(id=request.user.id))
'User' object has no attribute 'ordered'
Providing the query result as initial data also doesn't work as it's not a list.
Formset = modelformset_factory(obj.Meta.model, form=obj)
Formset(initial=obj.Meta.model.objects.get(id=request.user.id))
'User' object does not support indexing
I have a feeling that the answer is right in front of me. How can I pull database from the database and shove it into the form as initial values?
I'm not really sure I understand what you're trying to do - if you're just interested in a single form, I don't know why you're getting involved in formsets at all.
To populate a modelform with initial data from the database, you just pass the instance argument:
my_form = Set_Personal_Info(instance=UserProfile.objects.get(id=request.user.id))
Don't forget to also pass the instance argument when you're instantiating the form on POST, so that Django updates the existing instance rather than creating a new one.
(Note you might want to think about giving better names to your objects. obj usually describes a model instance, rather than a form, for which form would be a better name. And form classes should follow PEP8, and probably include the word 'form' - so PersonalInfoForm would be a good name.)
Based on what I've understand ... if you want to generate a form with dynamic fields you can use this:
class MyModelForm(forms.ModelForm):
def __init__(self, dynamic_fields, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
self.fields = fields_for_model(self._meta.model, dynamic_fields, self._meta.exclude, self._meta.widgets)
class Meta:
model = MyModel
Where dynamic_fields is a tuple.
More on dynamic forms:
http://www.rossp.org/blog/2008/dec/15/modelforms/
http://jacobian.org/writing/dynamic-form-generation/
http://dougalmatthews.com/articles/2009/dec/16/nicer-dynamic-forms-django/
Also Daniel's approach is valid and clean ... Based on your different ids/types etc you can you use different Form objects
forms.py
class MyModelFormA(forms.ModelForm):
class Meta:
model = MyModel
fields = ('field_a','field_b','field_c')
class MyModelFormB(forms.ModelForm):
class Meta:
model = MyModel
fields = ('field_d','field_e','field_f')
views.py
if request.method == 'POST':
if id == 1:
form = MyModelFormA(data=request.POST)
elif id == 2:
form = MyModelFormB(data=request.POST)
else:
form = MyModelFormN(data=request.POST)
if form.is_valid():
form.save() else:
if id == 1:
form = MyModelFormA()
elif id == 2:
form = MyModelFormB()
else:
form = MyModelFormN()

Inline formset in Django - removing certain fields

I need to create an inline formset which
a) excludes some fields from MyModel being displayed altogether
b) displays some some fields MyModel but prevents them from being editable.
I tried using the code below, using values() in order to filter the query set to just those values I wanted returned. However, this failed.
Anybody with any idea?
class PointTransactionFormset(BaseInlineFormSet):
def get_queryset(self):
qs = super(PointTransactionFormset, self).get_queryset()
qs = qs.filter(description="promotion feedback")
qs = qs.values('description','points_type') # this does not work
return qs
class PointTransactionInline(admin.TabularInline):
model = PointTransaction
#formset = points_formset()
#formset = inlineformset_factory(UserProfile,PointTransaction)
formset = PointTransactionFormset
One thing that doesn't seem to be said in the documentation is that you can include a form inside your parameters for model formsets. So, for instance, let's say you have a person modelform, you can use it in a model formset by doing this
PersonFormSet = inlineformset_factory(User, Person, form=PersonForm, extra=6)
This allows you to do all the form validation, excludes, etc on a modelform level and have the factory replicate it.
Is this a formset for use in the admin? If so, just set "exclude = ['field1', 'field2']" on your InlineModelAdmin to exclude fields. To show some fields values uneditable, you'll have to create a simple custom widget whose render() method just returns the value, and then override the formfield_for_dbfield() method to assign your widget to the proper fields.
If this is not for the admin, but a formset for use elsewhere, then you should make the above customizations (exclude attribute in the Meta inner class, widget override in __init__ method) in a ModelForm subclass which you pass to the formset constructor. (If you're using Django 1.2 or later, you can just use readonly_fields instead).
I can update with code examples if you clarify which situation you're in (admin or not).
I just had a similar issue (not for admin - for the user-facing site) and discovered you can pass the formset and fields you want displayed into inlineformset_factory like this:
factory = inlineformset_factory(UserProfile, PointTransaction,
formset=PointTransactionFormset,
fields=('description','points_type'))
formset = factory(instance=user_profile, data=request.POST)
where user_profile is a UserProfile.
Be warned that this can cause validation problems if the underlying model has required fields that aren't included in the field list passed into inlineformset_factory, but that's the case for any kind of form.