Django form save related object only if form is valid - python

I think I have a rather uncommon feature here, at least I couldn't find a answer.
What I'd like to achieve is a text input with autocomplete on a related model with its label field. the given text should then get_or_create the related model. this already works but the problem is, that the new related model instance is saved on form submit no matter if the form is_valid or not.
given the following situation and implementation (shortened for better overview)
class Correspondent(models.Model):
label = models.CharField(_("label"), max_length=100, unique=True)
class Document(BaseModel):
correspondent = models.ForeignKey(
Correspondent,
verbose_name=_("correspondent"),
on_delete=models.PROTECT,
related_name="documents",
)
with the following form:
class DocumentForm(forms.ModelForm):
correspondent = forms.CharField(
label=_("correspondent"), widget=forms.TextInput(attrs={"class": "awesomplete"})
)
class Meta:
model = Document
exclude = ["created_by", "modified_by"]
def clean_correspondent(self, *args, **kwargs):
# ToDo: don't save the FKed instance if complete form isn't valid
user = self.initial["user"]
data = self.cleaned_data["correspondent"]
obj = Correspondent.objects.get_or_create(label=data)
return obj
so the problem here is obj.save() which is called before form.is_valid() and I couldn't figure a way yet how to solve this. I'd like to prevent the creation of a new Correspondent instance if the form isn't valid yet.
Let me know if I can improve this question and thx for any hints.

Thx to Dipen Dadhaniya for pushing me in the right direction.
I needed to exclude the actual field from the ModelForm so I could just clean in a more simple way as CharField needs it. Afterwards in the View I can do the logic, after the whole form is valid indeed.
the form looses the complete custom clean_correspondent method.
class DocumentForm(forms.ModelForm):
correspondent_label = forms.CharField(
label=_("correspondent"), widget=forms.TextInput(attrs={"class": "awesomplete"})
)
class Meta:
model = Document
exclude = ["correspondent", "created_by", "modified_by"]
the logic is now in the DocumentCreateView in the form_valid() method.
def form_valid(self, form):
form.instance.modified_by = self.request.user
form.instance.correspondent, created = Correspondent.objects.get_or_create(
label=form.cleaned_data["correspondent_label"]
)
return super().form_valid(form)
Bingo! :)

Related

M2M field widget to add or update data as inline formset

I spent a lot of hours searching for a feature which I think should be quite a basic functionality in Django. But I just can't get it working,
I am unable to find a widget which will function same as m2m widget of django, but will also create new model instance if it doesn't exists.
Note: Here model instance already exists means that data entered in inline widget already exists in database.
E.g.
If I had models as:
class Outcome(models.Model):
outcome = models.CharField(max_length=255)
outcome_short_name = models.CharField(max_length=10, blank=True, null=True)
class Course(models.Model):
course_title = models.CharField(
verbose_name=COURSE_SINGULAR + " title", max_length=200, unique=True
)
course_outcome = models.ManyToManyField(
Outcome, verbose_name=COURSE_SINGULAR + " outcome", blank=True
)
Then I want "Outcomes" shown as this image while creating course:
Image of adding new course with inline outcomes
Now, If the outcomes data added by user already exists, then it should only map them to course. Otherwise it should first store outcomes into database and then map them to course.
Any guidance in right direction will be highly appreciated.
Thanks,
EDIT:
As suggested by #dirkgroten to use modelformset, I changed my FormView as:
class CourseFormView(FormView):
template_name = "course/course_form.html"
form_class = CourseForm
success_url = "/admin/"
def get_context_data(self, **kwargs):
context = super(CourseFormView, self).get_context_data(**kwargs)
if self.request.POST:
context["outcomes"] = OutcomeFormSet(self.request.POST)
else:
context["outcomes"] = OutcomeFormSet(queryset=Outcome.objects.none())
return context
def form_valid(self, form, **kwargs):
super(CourseFormView, self).get_context_data(**kwargs)
context = self.get_context_data()
outcomes_formset = context["outcomes"]
if not outcomes_formset.is_valid():
return super().form_invalid(form)
cleaned_data = form.cleaned_data
cleaned_data.pop("course_outcome")
course = Course.objects.create(**cleaned_data)
course.save()
outcomes_formset.instance = course
outcomes_formset.save()
course.course_outcome.set(Outcome.objects.filter(course_outcome=course))
return super().form_valid(form)
Everything looks fine except my model_formset is not validated if form data in formset already exists in database.
E.g. if I enter (outcome="test_outcome", outcome_short_name="test_short") in formset and same data already exists in outcome table, then my formset gives error:
Outcome with this Outcome and Outcome short name already exists.
Is there any way to tackle this situation or I am doing something wrong.
You can test above at: http://code.gdy.club:8001/course/add/
outcomes_list: http://code.gdy.club:8001/outcome/
Thanks,
--
Suraj
https://hacksj4u.wordpress.com
https://github.com/SurajDadral
You need to handle the case where an Outcome already exists yourself. The default when validating the form is to assume a new object will be created, so if your fields are set to be unique_together, then the individual form will not validate.
You could do it on the OutcomeFormset's clean() method like this:
from django.core.exceptions import NON_FIELD_ERRORS
def clean(self):
super().clean()
for i in range(0, self.total_form_count()):
form = self.forms[i]
if form.non_field_errors() and len(form.errors) == 1:
# the only error is probably due to unique_together constraint
try:
form.instance = Outcome.objects.get(
outcome=form.data.get(form.add_prefix('outcome')),
outcome_short_name=form.data.get(form.add_prefix('outcome_short_name')))
except Outcome.DoesNotExist:
pass # some other error so we should keep it
else:
del form._errors[NON_FIELD_ERRORS]
Then in your view, when saving the formset, you should loop through all forms and not save the ones where the instance has a pk:
for form in outcomes_formset:
outcome = form.instance
if not outcome.pk:
outcome = form.save()
course.course_outcome.add(outcome)

Django ModelForm ManyToManyField initial value

I'm using Django 1.11.2 to develop a website. I use ModelForms to edit my model instances on my website. Every field of the form gets the fitting value of the instance I want to edit via 'initial' in my view. It works fine for all fields except ManyToManyFields.
The relevant code looks like this:
models.py:
class model1(models.Model):
name = models.CharField(max_length=45, blank=False, null=False)
class model2(models.Model):
name = models.CharField(max_length=45, blank=False, null=False)
relation = models.ManyToManyField(model1)
the ModelForm in forms.py:
class model2_form(forms.ModelForm):
class Meta:
model = model2
fields = '__all__'
and the view I use to edit model2 intances:
def model2_edit(request, objectid):
link = 'Model2'
model2_inst = model2.objects.get(id=objectid)
form = model2_form(initial={'name': model2_inst.name,
'relation': ???})
if request.method == 'POST':
f = model2_form(request.POST, instance=model2_inst)
if f.is_valid():
f.save()
return HttpResponseRedirect('/model2')
return render(request, "edit_db.html",
{"form": form, "link":link})
Everytime I edit an instance of model2 via the ModelForm, the 'relations' of the instance that already exist aren't preselected ('initial' isn't working). If I save the form like this without selecting the relations again, they get deleted and that instance of model2 has no relations anymore.
At the place of the '???' in my code I tried many ways to get those relations already selected in the form, but I couldn't find a working way.
I hope I managed to describe my problem, thanks in advance for any help or ideas.
form = model2_form(initial={'name': model2_inst.name,
'relation': [i.id for i in model2_inst.relation.all()]})
You should provide the instance for GET and POST requests. This way, you do not need to provide initial data - Django will get the values from the instance automatically.
model2_inst = model2.objects.get(id=objectid)
form = model2_form(instance=model2_inst)

Django. Get id of ForeignKey object in CreateView

I study the CBV in django.
I want a user can upload images to a certain card of an apartment.
So The model was created:
class Photo(models.Model):
photo_path = models.ImageField(verbose_name='Фотография')
photo_user = models.ForeignKey(User, verbose_name='Агент, добавивший фото')
photo_flat = models.ForeignKey(Flat, verbose_name='Квартира')
photo_description = models.CharField(verbose_name='Описание', max_length=200, null=True, blank=True)
The initial data of the form are photo_user and the photo_flat.
When I try to save one or multiple pictures through a form on the page I've got AttributeError.
'NoneType' object has no attribute 'pk'
My ModelForm looks like this:
class PhotoUploadModelForm(forms.ModelForm):
class Meta:
model = Photo
fields = ['photo_path','photo_user','photo_flat','photo_description'
My CreateView:
class PhotoUploadView(CreateView):
form_class = PhotoUploadModelForm
def get_initial(self):
return {'photo_user': self.request.user,
'photo_flat': self.object.pk
}
def get_success_url(self):
return reverse('ha:flatdetail')
and my urls.py
url(r'^dev/flat/(?P<flat_id>[0-9]+)/$', views_dev.flat_ajax, name='flatdetail'),
url(r'^dev/photo-update/$', views_dev.PhotoUploadView.as_view(), name='image-update')
I understand the error but has not enough skills to handle it. am I correct that self.object.pk would be the id of the picture but not the apartment?
Can I get the id of the apartment?
You're misinterpreting the error, but that is partly because what you're doing doesn't make a lot of sense.
self.object can only refer to the object that this view is concerned with, which in this case is the Photo; but of course that photo doesn't exist yet when you do get_initial, because it hasn't been created. It seems that you want to use the ID of a specific Flat object; in which case you would need to pass that ID to the view, ie via the URL. You do this already in your flat_ajax view; you should do exactly the same thing here, and then you will be able to reference the ID via self.kwargs['flat_id'].
Note that get_initial isn't really the best place to be doing this, anyway. The User and the Flat object are not values that you want to pass to the form so that they populate fields initially but let the user change them; they are values you want to automatically associate with the object on save. So, this should really be done in form_valid, as shown in the documentation:
def form_valid(self, form):
form.instance.photo_user = self.request.user
form.instance.photo_flat_id = self.kwargs['flat_id']
return super(PhotoUploadView, self).form_valid(form)
If you do it this way, you should remove photo_user and photo_flat from the fields listed in the form.

Django: combine two ForeignKeys into one field

I need to implement the following:
The user shall be presented with a form that will have a drop down choice menu consisting of property names. There are two types of properties: general properties, i.e. properties common for all users and custom properties, i.e. properties that each user has defined prior to that. The models would look something like that:
class GeneralPropertyName(models.Model):
name = models.CharField(max_length=20)
class CustomPropertyName(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=20)
The drop down menu should have all general properties and only those custom properties that pertain to the user.
First question: how to define such a model?
I need to: 1. somehow unify both properties, 2. take only those items from CustomPropertyName that pertain to the user
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = ForeignKey(GeneralPropertyName) ??UNIFY??? ForeignKey(CustomPropertyName)
Second, is there anything special that needs to be done with ModelForm?
class SpecDataForm(ModelForm):
class Meta:
model = SpecData
And the 3rd question is what needs to be done in the view? I will need to use inline formsets since I will have a few dynamic forms like that.
def index(request):
user = User.objects.get(username=request.user.username)
specdataFormSet = inlineformset_factory(User, SpecData, form=SpecDataForm, extra=30)
...
specdata_formset = specdataFormSet(instance=user, prefix='specdata_set')
...
Thanks.
EDIT: Adjusted juliocesar's suggestion to include formsets. Somehow I am getting the following error message: Cannot resolve keyword 'property' into field. Choices are: id, name, selection_title, user
def index(request):
user = User.objects.get(username=request.user.username)
user_specdata_form = UserSpecDataForm(user=user)
SpecdataFormSet = inlineformset_factory(User, SpecData, form=user_specdata_form, extra=30)
You can use a GenericForeignKey to handle it, but you still need more to solve your further questions about forms and view.
I have made an example of how you solve your problem (logged user can select from General properties and his Custom properties, non-logged user only can select General properties). I used model inheritance for the properties (In your sample code it seems that a CustomPropertyName is a PropertyName with other fields). I think inheritance is an easier and a more basic concept than ContentTypes and it fits to your needs.
NOTE: I remove some code like imports to simplify the code.
1) models.py file:
class PropertyName(models.Model):
name = models.CharField(max_length=20)
def __unicode__(self):
return self.name
class CustomPropertyName(PropertyName): # <-- Inheritance!!
user = models.ForeignKey(User)
def __unicode__(self):
return self.name
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = models.ForeignKey(PropertyName)
NOTES: The field SpecData.property points to PropertyName since all properties are saved in the PropertyName's database table.
2) forms.py file:
from django import forms
from django.db.models import Q
from models import SpecData, PropertyName
def UserSpecDataForm(user=None):
UserPropertiesQueryset = PropertyName.objects.filter(Q(custompropertyname__user=None) | Q(custompropertyname__user__id=user.id))
class SpecDataForm(forms.ModelForm):
property = forms.ModelChoiceField(queryset=UserPropertiesQueryset)
class Meta:
model = SpecData
exclude = ('user',)
return SpecDataForm
NOTES: The trick here is to generate the form SpecDataForm dynamically, by filtering properties according the user specified in the parameter.
3) views.py file:
from forms import UserSpecDataForm
def index(request):
if request.POST:
form = UserSpecDataForm(request.user)(request.POST) # instance=user
if form.is_valid():
spec_data = form.save(commit=False)
spec_data.user = request.user
spec_data.save()
else:
form = UserSpecDataForm(request.user)()
return render_to_response('properties.html', {'form': form}, context_instance=RequestContext(request))
NOTES: Nothing special here, just a call to form.UserSpecDataForm(request.user) that returns the form class and then instantiate. Also setted the logged-in user to the object returned on save since It was excluded in the form to not show in front-end.
Following this basic example you can do the same with formsets if you need it.
UPDATE:
Formset can be used by adding following code to the view:
user_specdata_form = UserSpecDataForm(user=request.user)
SpecdataFormSet = inlineformset_factory(User, SpecData, form=user_specdata_form, extra=30)
The complete project sample can be downloaded from http://ge.tt/904Wg7O1/v/0
Hope this helps
1a) have you looked into django's ContentType framework this will allow you to have generic foreign keys and you can put restrictions on what types of models are acceptable to store in.
1b) I think that the validation for accepting what type of foreign key is acceptable shouldn't be in your model but should be part of your form validation before saving.
2) If you do use a model form you're going to have to define your own custom widget for the propery field. This means you're probably going to have to write you're own render function to render the html from the field. You should also define your own validation function on the form to make sure that only the appropriate data is acceptable to save.
3) I don't think you'll have to do anything you aren't already doing in the views
Use GenericForeignKey:
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
property = GenericForeignKey('content_type', 'object_id')
You can use this to combine the two fields(type & id) into a single choice field.
One way is that you have only one model, make user nullable:
class PropertyName(models.Model):
user = models.ForeignKey(User, null=True, blank=True)
name = models.CharField(max_length=20)
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = ForeignKey(PropertyName)
So, if user is not set, it is a general property. If it is set, it is related to this user.
However, please note that if you need unique property names, that NULL != NULL.
Of course, the suggested GenericForeignKey solution is better for some cases.
Also, you can easily make the normal (non-model) form with that you describe and separate form logic from model logic.

form.is_valid() always returning false

Model class
class Fuzz_Engine(models.Model):
id = PositiveTinyIntField(primary_key = True)
engine_name = models.CharField(max_length=16)
version = models.CharField(max_length = 16)
class Meta:
db_table = 'fuzz_engine'
unique_together = ('engine_name', 'version')
class AddFuzzEngineForm(ModelForm):
class Meta:
model = Fuzz_Engine
View Class
def addengine(request)
if request.method == 'POST':
form = AddFuzzEngineForm(request.POST)
# input validation for add phone model form
if form.is_valid():
fuzzEngineToAdd = Fuzz_Engine (engine_name = request.POST['engine_name'], version = request.POST['version'])
fuzzEngineToAdd.save(force_insert=True)
return render_to_response('fuzz/fuzz_cengine_results.html', {'fid': fuzzEngineToAdd.id,'fe': fuzzEngineToAdd,},context_instance=RequestContext(request))
else:
form = AddFuzzEngineForm()
return render_to_response('fuzz/add_fuzz_engine.html', {'form': form},context_instance=RequestContext(request))
I have looked into a few similar questions on this issue, tried to print out the errors but doesn't seem to appear.
Django Formsets - form.is_valid() is False preventing formset validation
form.is_valid() always returning false
I have a feeling that the cause of the error lies in the structure of my model form class.
The .is_valid is false as I have placed a code in that if statement and it doesn't run, however if I have an else statement(which is not here) for if it is not valid, it will appear.
Can anyone provide another way of debugging this kind of error?
Couple of issues, it's hard to debug the code if the code you paste in isn't formatted well. The indentations were a mess so I'm not sure if that's causing a problem.
It seems like you are manually assigning a foreign key for your model. I would suggest just letting django handle the id for the model:
class Fuzz_Engine(models.Model):
engine_name = models.CharField(max_length=16)
version = models.CharField(max_length = 16)
class Meta:
db_table = 'fuzz_engine'
unique_together = ('engine_name', 'version')
Your form looks fine:
class AddFuzzEngineForm(ModelForm):
class Meta:
model = Fuzz_Engine
Some problems I see in your views include:
you shouldn't use request.POST['field_names'] directly. you should be getting the cleaned_data from your form.
you can save the form directly because it is a ModelForm. if you need the instance that you just created, that is what is returned from the save method, you can set a variable and use that as shown below.
def addengine(request)
if request.method == 'POST':
form = AddFuzzEngineForm(request.POST)
if form.is_valid():
instance = form.save()
return render_to_response('fuzz/fuzz_cengine_results.html', {'fid': instance.id,'fe': instance,},context_instance=RequestContext(request))
else:
form = AddFuzzEngineForm()
return render_to_response('fuzz/add_fuzz_engine.html', {'form': form},context_instance=RequestContext(request))
With your original view, it looks like you are trying to save a Fuzz_Engine instance with no id.
DTing has some great points, but I suspect your actual problem is related to your explicit definition of the id field on your model. Unless you have a really good reason, you should never do this - Django defines an autoincrement field automatically, and there is rarely any point overriding this, unless you are using a legacy db that can't be changed.
In your case, you have defined it as a tinyint without autoincrement. That means that field is going to be required on any form, as it needs to be specified manually every time you create a new instance. You haven't shown the template you're using to display the form so it's impossible to be sure, but I imagine you're not showing this field at all.
If you really really want to carry on doing it this way, you will need to specify exclude = ('id',) on the form Meta. Then in your is_valid clause, taking on board DTing's recommendations, you'll need to do this:
if form.is_valid():
instance = form.save(commit=False)
instance.id = some_function_for_calculating_id()
instance.save()
But as I say, you shouldn't be doing that at all.

Categories

Resources