My main problem is that my code never goes into the for loop though within the debugger I can see that hardware exists. The for loop gets just skipped and I can´t figure out why this is the case.
Models:
class Hardware(models.Model):
name = models.CharField(max_length=30)
description = models.TextField()
class Bundle(models.Model):
name = models.CharField(max_length=30)
description = models.TextField()
devices = models.ManyToManyField(Hardware)
class BundleForm(ModelForm):
class Meta:
model = Bundle
fields = ('name', 'description', 'devices')
labels = {
'name': _('Bundlename'),
'description': _('Beschreibung des Bundle'),
'devices': _('Hardware im Bundle')
}
Views:
elif request.method == 'POST' and 'newbundle' in request.POST:
form = BundleForm(request.POST)
if form.is_valid():
bundle = form.save(commit=False)
bundle.save()
for hardware in bundle.devices.all():
print(hardware)
messages.success(request, 'Erfolg! Die Daten wurden erfolgreich gespeichert.')
return redirect('/knowledgeeditor/bundle/', {'messages': messages})
Your question is not about iterating many-to-many fields, but saving them.
The modelforms documentation has this to say about using commit=False with a many-to-many field:
Another side effect of using commit=False is seen when your model has a many-to-many relation with another model. If your model has a many-to-many relation and you specify commit=False when you save a form, Django cannot immediately save the form data for the many-to-many relation. This is because it isn’t possible to save many-to-many data for an instance until the instance exists in the database.
To work around this problem, every time you save a form using commit=False, Django adds a save_m2m() method to your ModelForm subclass. After you’ve manually saved the instance produced by the form, you can invoke save_m2m() to save the many-to-many form data.
As noted there, you could use save_m2m() to save the field, but instead of doing that you should ask yourself why you are using commit=False here at all. There is no reason to do so, so you should omit that parameter and the subsequent separate save, and just let the form save itself in one go.
Related
Explanation:
I have a model that has two foreign keys:
class Book(models.Model):
name = models.CharField(max_length=100)
author = models.ForeignKey(Author, on_delete=CASCADE)
created_by = models.ForeignKey(User, blank=True, null=True, on_delete=CASCADE)
I have a modelform for it:
class BookForm(modelForm):
class Meta:
model = Book
exclude = ('author', 'created_by',)
In my business logic, I want to save a new author and a book with it in the same view, and the created_by could be null in some cases. So, before saving an instance of a BookForm, I will create an author first. So, suppose that I have created an author, then I will do the below to save a book:
f = BookForm(post_data)
if form.is_valid():
instance = f.save(commit=False)
instance.author = author
instance.created_by = user
instance.save()
What I did so far is working fine, but the foreign key fields are not part of the validation. As you can see from the model class, author field is mandatory, but created_by field is optional. So, if I want to add some validations for them, I have to add them explicitly by customizing the is_valid() method, or by adding some if conditions before instance.author = author.
Question:
The bigger picture is that I have a model with much more foreign keys; some of them are mandatory, and the others are not. So, using the same approach I did above, I have to add a custom validation for each FK explicitly, which I consider it duplication and unclean as those FK fields validation is defined in my model class.
So, what is the best practice to validate automatically those multiple foreign keys while saving by checking the models validation directly and without mentioning each FK one by one?
Since the foreign key fields are not included in your form, there won't be any form validation on them. What you need is a model validation.
Django provides the model.full_clean() method, which performs all model validation steps.
You will need to call the method manually, since save() method does not invoke any model clean method.
See documentation here.
For example,
f = BookForm(post_data)
if form.is_valid():
instance = f.save(commit=False)
instance.author = author
instance.created_by = user
try:
instance.full_clean()
except ValidationError as e:
# Do something based on the errors contained in e.message_dict.
# Display them to a user, or handle them programmatically.
pass
instance.save()
If you want to display the error messages to the user, simply pass e to the context and loop through the messages in your template, displaying them wherever you want. This seems the cleaner approach than adding errors to the form, because they are model validation errors not form errors.
In your view:
except ValidationError as e:
context = {'e':e} # add other relevant data to context
return render(request, 'your_template', context)
In your template:
{% for key, values in e.message_dict %}
{{key}}: {{value}}
{% endfor %}
If you want the errors added to the form, then use form.add_error:
except ValidationError as e:
for key, value in e.items():
# this adds none-field errors to the form for each model error
form.add_error(None, f'{key}: {value}')
My issue is that my app is not allowing me to update a OneToOneField field. Here's my explanation of what I'm trying to do.
I am building an inventory app that keeps track of instruments that have been loaned to students. There will always be a one-to-one database relationship between students and instruments. So an individual student can't ever have more than one instrument and vice versa.
I therefore created an Intrument model that looks like this:
class Instrument(models.Model):
instrument_type = models.CharField(max_length=100)
needs_repairs = models.BooleanField()
inventory_id = models.CharField(max_length=100)
student = models.OneToOneField(Student, null=True, blank=True, default = None)
I have created a form that allows me to update existing students, and I'm trying to use as much built-in stuff as possible so that I don't need to re-write validation code or HTML. So I'm using a ModelForm object and validating my input using the is_valid() method.
Here's an example of a POST request to update an instrument:
csrfmiddlewaretoken=xyUBhVuQZus6XmeV2DhCmpJHwIXVmdHm&instrument_type=Viola&inventory_id=abcde&student=3
Please note that the only field with a uniqueness constraint is student.
So finally, here's the problem: when I call the is_valid() method it always fails with an error saying that the student has already been assigned to an instrument.
My first thought was to use the framework to add some pre-validation code that didn't error if the student pkey didn't change. This certainly seems easy enough, but it seems to be a bit hacky to me. I assumed that one-to-one relationships would "just work" like all of the other Model fields and that no special validation would be required.
But then I read the API docs for the OneToOneField class and it doesn't seem to address one-to-one database relationships - it seems to address one-to-one OO relationships. So I may be using the wrong Model field type all together. And since this is such a simple app, I'm not performing a ton of OO modeling - I'm just worried about proper data modeling :-)
So am I using the wrong field, or is the "proper" way to fix this to add pre-validation code to my Student model?
Updates From Comments
Here's the closest thing that I have to a stack trace:
>>> data = {'instrument_type': 'Viola', 'inventory_id': 'abcde', 'student': 3, 'repairer': 1}
>>> form = InstrumentForm(data)
>>> form.is_bound
True
>>> form.is_valid()
False
>>> form.errors
{'student': [u'Instrument with this Student already exists.']}
I use a single view method to display Instrument detail and update a single Instrument. Here's that:
def instrument_detail(request, instrument_id):
try:
instrument = Instrument.objects.get(pk=instrument_id)
except Instrument.DoesNotExist:
raise Http404
# Default if not a POST
form = InstrumentForm(instance=instrument)
if request.method == 'POST':
form = InstrumentForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('instruments.views.instruments_index'))
# otherwise...
t = loader.get_template('instruments/details.html')
c = RequestContext(request, {
'instrument': instrument,
'form': form,
})
return HttpResponse(t.render(c))
You're not passing the instance when instantiating the form on POST.
if request.method == 'POST':
form = InstrumentForm(request.POST, instance=instrument)
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}
(...)
I had a model which i converted it to form as below
models.py
class Book(models.Model):
name = models.CharField()
description = models.TextField()
class College(models.Model):
name = models.CharField()
books = models.ManyToManyField(Book)
place = models.CharField()
form.py
from django.forms import ModelForm
from myapp.models import College
class CollegeForm(ModelForm):
class Meta:
model = College
views.py
def college_view(request):
form = CollegeForm()
if request.method == 'POST':
form = CollegeForm(request.POST):
if form.is_valid():
college_obj = College.objects.create(name=request.POST['name'],
place=request.POST['place'],
books= ???????)
return HttpResponseRedirect(reverse('index'))
return render_to_response('college-detail.html', {'form':form})
So what all i am doing above is
Created a model called College that has a many to many link to Book
Converted the model in to form using ModelForm and rendered as html
When the form gets submitted with values, in view i am getting that values from
request.POST and trying to save in to the College model.
but as you can observe the college form has one many to many field and two CharField,
so i had saved the char fields directly in to the table, but i am stuck near saving the
many to many field books in the table(that is model College)
so can anyone please let me know in brief on how to save manytomany fields in to the database tables i mean models that has manytomany fields
Thanks in advance.....
So, if you read the documentation on ModelForms, you will notice that they are equipped with a save() method, which already provides the functionality you want.
You can use
if form.is_valid():
form.save()
or, if you want to add more values to the model, you can
if form.is_valid():
instance = form.save(commit=False)
instance.custom_field = custom_value
instance.save()
After calling form.is_valid(), calling form.save() will do job of saving many to many data for you as well. If in case you want to change/add data to form objects before saving you can do
f = form.save(commit=False)
After calling save with commit=False django makes available a save_m2m() method on your form object. Notice form not f. After you're done manipulating the f object, you can go ahead and call save_m2m() to save many to many data of that form.
form.save_m2m()
Reference: https://docs.djangoproject.com/en/2.0/topics/forms/modelforms/#the-save-method
deals_formset_factory = modelformset_factory(Deal, form=DealCForm, extra=1)
attached_deals_formset = deals_formset_factory(request.POST, prefix='deals')
Since some fields of my Deal model are not shown in the form and hence can't be set by the user (but the M2M field is shown and can be set by the user), I can't just do a
for fm in attached_deals_formset:
if fm.has_changed():
fm.save()
since it would break.
So theoretically the idea in such situations is to do
deal = fm.save(commit=False)
...
deal.save()
but this doesn't save my M2M field inside deal. The Through table remains untouched. What is the best approach to solve this?
class Deal(models.Model):
deal_id = UUIDField()
....
sales_item = models.ManyToManyField(SalesItem)
I found the solution, there is no need to override the save method.
Another side effect of using commit=False is seen when your model has
a many-to-many relation with another model. If your model has a
many-to-many relation and you specify commit=False when you save a
form, Django cannot immediately save the form data for the
many-to-many relation. This is because it isn't possible to save
many-to-many data for an instance until the instance exists in the
database.
To work around this problem, every time you save a form using
commit=False, Django adds a save_m2m() method to your ModelForm
subclass. After you've manually saved the instance produced by the
form, you can invoke save_m2m() to save the many-to-many form data
Source
After deal.save() simply:
fm.save_m2m()