I have the following models:
class Recipe(models.Model):
fields...
class Ingredient(models.Model):
fields...
class UsesIngredient(models.Model):
recipe = models.ForeignKey(Recipe)
ingredient = models.ForeignKey(Ingredient)
amount = models.FloatField()
group = models.CharField()
I have a view which lets the user add any number of 'UsesIngredient' models for a certain recipe through a dynamic formset. The group attribute is automatically filled in an hidden from the user.
The problem is that when the users adds a new form in the formset, but doesn't fill in any of the fields, I don't want that form saved. However, django still tries to save the form because the 'group' attribute has 'changed' (because it has been automatically filled in when the extra form was created).
Is there any way to get around this?
Thanks!
Well, I still didn't feel completely comfortable with Tim Edgar's solution, so I kept looking. I guess I found what I was looking for.
The 'Form' class, has two undocumented methods that are of use in this case: 'has_changed()' and '_get_changed_data'.
During ModelFormSet validation, every form checks 'has_changed()'. If the form did not changed, validation is skipped and a correct form is assumed.
Likewise, during ModelFormSet saving, the save_new_objects checks every form to see if it has changed. If it didn't change, the form isn't saved.
So my solution was to override the has_changed() method to return False if only the 'group' attribute has changed, and all other fields are empty. This is my implementation:
class UsesIngredientForm(forms.ModelForm):
class Meta:
model = UsesIngredient
def has_changed(self, *args, **kwargs):
self._get_changed_data(*args, **kwargs)
# If group is in changed_data, but no other fields are filled in, remove group so
# the form will not be validated or saved
if 'group' in self._changed_data and len(self._changed_data) == 1:
contains_data = False
for name in ['ingredient', 'amount', 'unit']:
field = self.fields[name]
prefixed_name = self.add_prefix(name)
data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
if data_value:
contains_data = True
break
if not contains_data:
self._changed_data.remove('group')
return bool(self._changed_data)
Hope this helps anybody in the future!
EDIT:
I edited this answer to reflect Tim Edgars comment.
I realize that this implementation still uses 'private' methods, but I haven't found a cleaner implementation using just the publicly documented methods. But then maybe that is just my own incompetence :).
You could try making all your fields to require a value by setting blank=False. See more here. It should require validation that the values that you care about are not left blank.
If that doesn't work, you can try creating your own custom save method that does the validation that you care about.
def save(self, *args, **kwargs):
# Do your checks on the properties such as self.group, self.amount, etc
# If it is fine then call
super(UsesIngredient, self).save(*args, **kwargs)
Related
I've a simple model with a boolean field in it, and the related admin view:
# in models.py
class MyModel(models.Model):
...
my_field = models.BooleanField(...)
# in admin.py
class MyModelAdmin(admin.ModelAdmin):
readonly_fields ("my_field", ...)
My problem is that now my boolean field appears always empty, independently from the actual value of the field itself.
I didn't find any solution to this problem, does it happen only to me?
I don't know if it may be relevant, but I'm using grappelli == 2.4.5
Thanks
Ok,
after some searching I've found a solution (perfectible, but a good starting point). I've simply overridden the get_form(...) model in my concretization of ModelAdmin:
def get_form(self, *args, **kwargs):
form = super(SupplierAdmin, self).get_form(*args, **kwargs)
for field_name in self.fake_readonly_fields:
form.base_fields[field_name].widget.attrs["disabled"] = "disabled"
return form
I renamed the list of my readonly fields to fake_readonly_fields, in order not to mess with Django readonly_fields. This works for textboxes, checkboxes and selects (I guess also for radio buttons, but I didn't verify it...). Now I'm looking for a solution for upload file inputs ...
Btw I don't know if this solution can cause "security" problems (e.g. some crafted message to the server can overcome my html-disabled fields, and pass new data to overwrite old values ...) but that's a different (still relevant) topic
I have this model
class Env(models.Model):
functional_count = models.PositiveIntegerField()
current_count = models.PositiveIntegerField()
Now i want functional_count to always be less than current_count.
So during create,
def form_valid(self, form):
form.instance.current_count = 0
This is because i want current_count during initialization.Then my python code never allows current_count to go above functional_count.
The problem comes in Update.
class EnvUpdate(UpdateView):
model = Capacity.models.Envapps
fields = ['functional_count']
template_name_suffix = '_update_form'
So do i include a validator? If yes, where and how?
Or other option is to verify in get_success_url().
Any other solution?
Assuming your updates come through a form (as suggested by the use of form_valid(), perhaps you can use the form clean() method, as described in the documentation. This allows you to perform checks for fields that depend on each other. The documentation also has an example which should get you further.
Update
From your comments, I understand that you tried to use clean() inside EnvUpdate, which inherits from the class-based UpdateView view. All the mixins provided through UpdateView do apparently not provide a clean() method, so you can't override it.
I am actually referring to the clean() in a form class (as follows from the link). So, it looks like you'd need to create your own ModelForm class, something like:
class EnvappsForm(forms.ModelForm):
class Meta:
model = Capacity.models.Envapps
fields = ['functional_count']
def clean(self):
cleaned_data = super(ContactForm, self).clean()
if cleaned_data['functional_count'] >= form.instance.current_count:
raise ValidationError('too large')
return cleaned_data
and then in your view:
class EnvUpdate(UpdateView):
model = Capacity.models.Envapps
template_name_suffix = '_update_form'
form_class = EnvappsForm
Note: this is completely untested! I don't know if the comparison in the clean() works (i.e., if form.instance.current_count can be found), and wheter EnvUpdate will override the form_class (it shouldn't, but I've never tried). It just might be even possible that you can remove the meta subclass, and provide the model and fields from EnvUpdate, as you do yourself above. That's just something you can easily try out.
If functional_count should always be less than current_count you should check it in the clean() method on the model and not some random ModelForm. The model clean() will be called during normal ModelForm validation.
I am displaying a modelformset and I would like the forms to be ordered by the contents of one of its fields. So I want to use the equivalent of SomeModel.objects.filter(whatever).order_by('somefield') for a (model)formset in a template.
How can I do this?
Note that can_order does not do what I want (it must be automatic, not user specified). I have also tried other things, like the dictsort filter, but that produces unpredictable output (i.e. not ordered by the specified field).
I even tried {% regroup formset by somefield as sorted_formset %}, but the resulting sorted_formset cannot be used (iterated) as a normal formset.
To complete the answers. There are two ways how you can control the order of forms in the formset: formsets respect order of given queryset (this is shown in other replies). Another way (if you want to have the order of the forms fully under control) is to define custom formset class and override __iter__ and __getitem__ methods:
from django.forms import BaseModelFormSet
class MyModelBaseFormset(BaseModelFormSet):
def __iter__(self):
"""Yields the forms in the order they should be rendered"""
return ...
def __getitem__(self, index):
"""Returns the form at the given index, based on the rendering order"""
return ...
MyModelFormset = modelformset_factory(model=MyModel, formset=MyModelBaseFormset, queryset=...)
This approach is described in the Django documentation:
Iterating over the formset will render the forms in the order they
were created. You can change this order by providing an alternate
implementation for the __iter__() method.
Formsets can also be indexed into, which returns the corresponding
form. If you override __iter__, you will need to also override
__getitem__ to have matching behavior.
https://docs.djangoproject.com/en/dev/topics/forms/formsets/#django.forms.formsets.BaseFormSet
The example of implementing these methods is for example in this SO thread: modelformset __iter__ overloading problem.
if you didn't defined Formset, this is the "inline code" version:
FS=inlineformset_factory(ParentClass,ChildClass)
formset=FS(instance=parentobject,
queryset=parentobject.childobject_set.order_by("-time_begin")
)
Thanks to #john-peters answer above, for pointing me in the right direction. But here is a better way:
MyFormset(inlineformset_factory(...)):
def get_queryset(self):
return super(MyFormset, self).get_queryset().order_by('myfieldname')
This way you do not have to copy or mess with django's code and potentially cause breakage down the road.. just take the queryset that django gives you and override the ordering. I have used this in my own code, it works.
EDIT. After working on this a bit I realize that although it appears to work fine, it somehow messes up the logic in BaseInlineFormset.get_queryset(), resulting in duplicate database queries. However, in the hopes that someone will comment on this and correct it, I will leave it here. Meanwhile, I have another solution which DOES WORK and does not result in redundant queries.. as follows:
MyFormset(inlineformset_factory(...)):
def __init__(self, *args, **kwargs):
super(MyFormset, self).__init__(*args, **kwargs)
self.queryset = self.queryset.order_by('myfieldname')
This modifies the queryset at a safe time, before anything is done with it. In my code I am also doing .select_related() here, which greatly speeds up my large modelformsets!
Thanks to rantanplan's comment I found a solution. I could not use the method as described in the link above because I don't know what the queryset is going to be (this is a complex form with nested formsets).
Anyway, I found a solution by overriding the get_queryset method from Django's BaseInlineFormSet Class.
I copied it below, including my mod, in case googlers find it helpful.
def get_queryset(self):
'''
Copied this method from Django code and modified the ordering statement
'''
if not hasattr(self, '_queryset'):
if self.queryset is not None:
qs = self.queryset
else:
qs = self.model._default_manager.get_query_set()
# If the queryset isn't already ordered we need to add an
# artificial ordering here to make sure that all formsets
# constructed from this queryset have the same form order.
if not qs.ordered:
# MY MOD IS HERE:
# qs = qs.order_by(self.model._meta.pk.name)
qs = qs.order_by('order_index')
#/MOD
# Removed queryset limiting here. As per discussion re: #13023
# on django-dev, max_num should not prevent existing
# related objects/inlines from being displayed.
self._queryset = qs
return self._queryset
A more simpler way but it might not apply in every situation but if you can afford it just define ordering in models Meta class:
class ExampleModel(models.Model):
...
class Meta:
ordering = ("name", )
Formset will respect the queryset's ordering which will respect the Meta class variable's value.
I've been working through an ordered ManyToManyField widget, and have the front-end aspect of it working nicely:
Unfortunately, I'm having a great deal of trouble getting the backend working. The obvious way to hook up the backend is to use a through table keyed off a model with ForeignKeys to both sides of the relationship and overwrite the save method. This would work great, except that due to idiosyncrasies of the content, it is an absolute requirement that this widget be placed in a fieldset (using the ModelAdmin fieldsets property), which is apparently not possible.
I'm out of ideas. Any suggestions?
Thanks!
In regard to how to set up the models, you're right in that a through table with an "order" column is the ideal way to represent it. You're also right in that Django will not let you refer to that relationship in a fieldset. The trick to cracking this problem is to remember that the field names you specify in the "fieldsets" or "fields" of a ModelAdmin do not actually refer to the fields of the Model, but to the fields of the ModelForm, which we are free to override to our heart's delight. With many2many fields, this gets tricky, but bear with me:
Let's say you're trying to represent contests and competitors that compete in them, with an ordered many2many between contests and competitors where the order represents the competitors' ranking in that contest. Your models.py would then look like this:
from django.db import models
class Contest(models.Model):
name = models.CharField(max_length=50)
# More fields here, if you like.
contestants = models.ManyToManyField('Contestant', through='ContestResults')
class Contestant(models.Model):
name = models.CharField(max_length=50)
class ContestResults(models.Model):
contest = models.ForeignKey(Contest)
contestant = models.ForeignKey(Contestant)
rank = models.IntegerField()
Hopefully, this is similar to what you're dealing with. Now, for the admin. I've written an example admin.py with plenty of comments to explain what's happening, but here's a summary to help you along:
Since I don't have the code to the ordered m2m widget you've written, I've used a placeholder dummy widget that simply inherits from TextInput. The input holds a comma-separated list (without spaces) of contestant IDs, and the order of their appearance in the string determines the value of their "rank" column in the ContestResults model.
What happens is that we override the default ModelForm for Contest with our own, and then define a "results" field inside it (we can't call the field "contestants", since there would be a name conflict with the m2m field in the model). We then override __init__(), which is called when the form is displayed in the admin, so we can fetch any ContestResults that may have already been defined for the Contest, and use them to populate the widget. We also override save(), so that we can in turn get the data from the widget and create the needed ContestResults.
Note that for the sake of simplicity this example omits things like validation of the data from the widget, so things will break if you try to type in anything unexpected in the text input. Also, the code for creating the ContestResults is quite simplistic, and could be greatly improved upon.
I should also add that I've actually ran this code and verified that it works.
from django import forms
from django.contrib import admin
from models import Contest, Contestant, ContestResults
# Generates a function that sequentially calls the two functions that were
# passed to it
def func_concat(old_func, new_func):
def function():
old_func()
new_func()
return function
# A dummy widget to be replaced with your own.
class OrderedManyToManyWidget(forms.widgets.TextInput):
pass
# A simple CharField that shows a comma-separated list of contestant IDs.
class ResultsField(forms.CharField):
widget = OrderedManyToManyWidget()
class ContestAdminForm(forms.models.ModelForm):
# Any fields declared here can be referred to in the "fieldsets" or
# "fields" of the ModelAdmin. It is crucial that our custom field does not
# use the same name as the m2m field field in the model ("contestants" in
# our example).
results = ResultsField()
# Be sure to specify your model here.
class Meta:
model = Contest
# Override init so we can populate the form field with the existing data.
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance', None)
# See if we are editing an existing Contest. If not, there is nothing
# to be done.
if instance and instance.pk:
# Get a list of all the IDs of the contestants already specified
# for this contest.
contestants = ContestResults.objects.filter(contest=instance).order_by('rank').values_list('contestant_id', flat=True)
# Make them into a comma-separated string, and put them in our
# custom field.
self.base_fields['results'].initial = ','.join(map(str, contestants))
# Depending on how you've written your widget, you can pass things
# like a list of available contestants to it here, if necessary.
super(ContestAdminForm, self).__init__(*args, **kwargs)
def save(self, *args, **kwargs):
# This "commit" business complicates things somewhat. When true, it
# means that the model instance will actually be saved and all is
# good. When false, save() returns an unsaved instance of the model.
# When save() calls are made by the Django admin, commit is pretty
# much invariably false, though I'm not sure why. This is a problem
# because when creating a new Contest instance, it needs to have been
# saved in the DB and have a PK, before we can create ContestResults.
# Fortunately, all models have a built-in method called save_m2m()
# which will always be executed after save(), and we can append our
# ContestResults-creating code to the existing same_m2m() method.
commit = kwargs.get('commit', True)
# Save the Contest and get an instance of the saved model
instance = super(ContestAdminForm, self).save(*args, **kwargs)
# This is known as a lexical closure, which means that if we store
# this function and execute it later on, it will execute in the same
# context (i.e. it will have access to the current instance and self).
def save_m2m():
# This is really naive code and should be improved upon,
# especially in terms of validation, but the basic gist is to make
# the needed ContestResults. For now, we'll just delete any
# existing ContestResults for this Contest and create them anew.
ContestResults.objects.filter(contest=instance).delete()
# Make a list of (rank, contestant ID) tuples from the comma-
# -separated list of contestant IDs we get from the results field.
formdata = enumerate(map(int, self.cleaned_data['results'].split(',')), 1)
for rank, contestant in formdata:
ContestResults.objects.create(contest=instance, contestant_id=contestant, rank=rank)
if commit:
# If we're committing (fat chance), simply run the closure.
save_m2m()
else:
# Using a function concatenator, ensure our save_m2m closure is
# called after the existing save_m2m function (which will be
# called later on if commit is False).
self.save_m2m = func_concat(self.save_m2m, save_m2m)
# Return the instance like a good save() method.
return instance
class ContestAdmin(admin.ModelAdmin):
# The precious fieldsets.
fieldsets = (
('Basic Info', {
'fields': ('name', 'results',)
}),)
# Here's where we override our form
form = ContestAdminForm
admin.site.register(Contest, ContestAdmin)
In case you're wondering, I had ran into this problem myself on a project I've been working on, so most of this code comes from that project. I hope you find it useful.
I want to alter properties of a model field inherited from a base class. The way I try this below does not seem to have any effect. Any ideas?
def __init__(self, *args, **kwargs):
super(SomeModel, self).__init__(*args, **kwargs)
f = self._meta.get_field('some_field')
f.blank = True
f.help_text = 'This is optional'
So.. You need to change blank and help_text attributes.. And I assume that you want this feature just so the help_text is displayed in forms, and form does not raise "this field is required"
So do this in forms:
class MyForm(ModelForm):
class Meta:
model = YourModel
some_field = forms.CharField(required=False, help_text="Whatever you want")
OK, that's simply not possible, here is why:
http://docs.djangoproject.com/en/1.1/topics/db/models/#field-name-hiding-is-not-permitted
EDIT:
And by the way: don't try to change class properties inside a constructor, it's not a wise thing to do. Basically what you are trying to do, is to change the table, when you are creating a row. You wouldn't do that, if you were just using SQL, would you :)? Completely different thing is changing forms that way - I often dynamically change instance a form, but then I still change only this one instance, not the whole template (a class) of form to be used (for example to dynamically add a field, that is required in this instance of a form).