Django formset validating all forms - django

I have made a formset like this.
class MixedObsCollsForm(forms.Form):
def __init__(self, *args, **kwargs):
project_id = kwargs.pop('project_id')
super(MixedObsCollsForm, self).__init__(*args, **kwargs)
project = get_object_or_404(Project, pk=project_id)
self.fields['photos_upload'] = forms.ImageField(label="Photos", required=False)
taxon = forms.CharField(max_length=80, required=False)
growth_form = forms.ChoiceField(choices = growthFormChoice, required=False)
height = forms.DecimalField(max_value=99, min_value=0, decimal_places=2, required=True)
density = forms.IntegerField(max_value=100, min_value=1, required=False)
And I have a weird way to display it in a view using a class to handle all the processing. But its like this...
class ObservationFormView(object):
def __init__(self, request=None, project_id=None, observation_id=None):
self.request = request
self.ObservationFormSet = formset_factory(form=MixedObsCollsForm, extra=5)
self.ObservationFormSet.form = staticmethod(curry(MixedObsCollsForm, project_id=project_id))
self.project = get_object_or_404(Project, pk=project_id)
self.user = request.user
self.use_type = 'create'
self.formset = self.ObservationFormSet(self.request.POST or None)
def isValid(self):
return self.formset.is_valid() & self.locationForm.is_valid()
def render(self):
return render(self.request, 'observation_form.html', {'use_type': self.use_type, 'formset': self.formset, 'locationForm':self.locationForm, 'project_photos': self.project.photos.all(),})
def processForm(self):
for form in self.formset:
if form.is_valid() == True:
if form.cleaned_data['is_collection'] == True:
collection = self.getOrMakeCollection(form.cleaned_data, self.locationForm.cleaned_data['location'], False)
if form.cleaned_data['is_collection'] == False:
collection = self.getOrMakeCollection(form.cleaned_data, self.locationForm.cleaned_data['location'], True)
observation = self.saveObservation(form.cleaned_data, self.locationForm.cleaned_data['location'], collection)
return observation
The problem is when I render this form in a view even the forms within the formset which are empty don't validate. Reporting "This field is required." next to all the height fields. Even the empty ones.
It was my understanding that the empty forms should always pass validation. I have looked at the management form data and it all looks fine.
If I change required to False it means I end up with IntegrityErrors when I try to save the formset to the database.

I finally worked out what it was so here it is for anyone else who has this problem, its easy to overlook.
It wasn't even included in the code in the question so there wasn't much help anyone could offer.
This line:
growth_form = forms.ChoiceField(choices = growthFormChoice, required=False)
Used the growthFormChoice variable which was a tuple of choices like this.
growthFormChoice = (('0', 'Select one'),
('T', 'tree'),
('S', 'shrub'))
Because of this the default value ('0':'Select One') resulted in passing a 0 to every form which wasn't (intentionally) altered and triggering validation for the rest of the fields. Simply changing it to ('':'Select One') was all it needed.

Related

Django - forms trying to clean unique together, CrispyError

I have a model with a unique together and I want to validate this condition in my modelform. The unique together includes a field that is passed to the form in an init method, the user, and a field that is in the form. I'm having problems with validating a unique together condition.
EDIT
I have modified the code to what you see below
model:
class Objective(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
course = models.ForeignKey(Course, on_delete=models.CASCADE)
objective_name = models.CharField(max_length=10)
description = models.CharField(max_length=300)
mode = models.CharField(max_length=2, default='LA')
class Meta:
unique_together = ['user', 'objective_name', 'course']
ordering = ['objective_name']
def __str__(self):
return self.objective_name
The view:
def addobjective(request, course_id):
this_course = get_object_or_404(Course, pk=course_id)
user = request.user
all_courses = Course.objects.filter(user=user)
objective_list = Objective.objects.filter(
course=this_course).order_by('objective_name')
context = {'objective_list': objective_list}
if request.method == 'POST':
form = ObjectiveForm(user, request.POST, my_course=this_course)
if form.is_valid():
obj = form.save(commit=False)
obj.course = this_course
obj.user = user
obj.save()
form = ObjectiveForm(user, my_course=this_course)
context['form'] = form
return redirect('gradebook:addobjective', course_id=this_course.id)
else:
form = ObjectiveForm(user, my_course=this_course)
context['form'] = form
context['this_course'] = this_course
context['all_courses'] = all_courses
return render(request, 'gradebook/objective-form.html', context)
forms.py:
class ObjectiveForm(ModelForm):
def __init__(self, user, *args, **kwargs):
self.request = kwargs.pop('request', None)
my_course = kwargs.pop('my_course')
self.objs = Objective.objects.filter(user=user, course=my_course)
super(ObjectiveForm, self).__init__(*args, **kwargs)
class Meta:
model = Objective
fields = ('objective_name', 'description', 'mode',)
def clean(self):
super(ObjectiveForm, self).clean()
objective_name = self.cleaned_data.get("objective_name")
description = self.cleaned_data.get("description")
mode = self.cleaned_data.get("mode")
if self.objs.filter(objective_name=objective_name).count() > 0:
print("error")
del self.cleaned_data["objective_name"]
del self.cleaned_data["description"]
del self.cleaned_data["mode"]
raise ValidationError(
"This course already has a learning objective with this name.")
return self.cleaned_data
EDIT
The error I know get is |as_crispy_field got passed an invalid or inexistent field. This occurs when I enter in a value for objective_name that is a duplicate. error is printed to the console and then I get the above error. I do not get the ValidationError.
The full traceback can be seen here.
Maybe with the form I do not need the unique together constraint in the model?
Yes, my_course field is not defined in Objective model , so maybe you need to change this line:
form = ObjectiveForm(request.POST, my_course=this_course)
To
form = ObjectiveForm(request.POST, course=this_course)
It turns out that the problem was caused by improper indentation of return redirect('gradebook:addobjective', course_id=this_course.id) after the if form.is_valid():. The return redirect has to be a part of the POST request.

Form not saving to database "through" intermediate tables

I have a custom form that is not saving to the database. I do not get any errors but the values do not save to the database. Any ideas?
views.py
def diseasestateoption(request, disease_id, state_id):
state = get_object_or_404(State, pk=state_id)
disease = get_object_or_404(Disease, pk=disease_id)
if request.method == "POST":
form = UpdateStateWithOptionsForm(request.POST, instance=state)
if form.is_valid():
for option_id in request.POST.getlist('options'):
state_option = StateOption.objects.create(partstate=state, partoption_id=int(option_id))
state_option.save()
return HttpResponseRedirect(reverse('success'))
else:
form = UpdateStateWithOptionsForm(instance=state)
models.py
class Option(models.Model):
relevantdisease = models.ForeignKey(Disease)
option = models.CharField(max_length=300)
class State(models.Model):
state = models.CharField(max_length=300, verbose_name='state')
relevantdisease = models.ForeignKey(Disease, verbose_name="disease")
relevantoption = models.ManyToManyField(Option, through='StateOption')
class StateOption(models.Model):
parttstate = models.ForeignKey(State)
partoption = models.ForeignKey(Option)
forms.py
class UpdateStateWithOptionsForm(forms.ModelForm):
class Meta:
model = State
exclude = ['state', 'relevantdisease']
def __init__(self, *args, **kwargs):
super(UpdateStateWithOptionsForm, self).__init__(*args, **kwargs)
self.fields['relevantoption']=forms.ModelMultipleChoiceField(queryset=Option.objects.all(),required=True, widget=forms.CheckboxSelectMultiple)
I think Problem is with getting option from POST, use-
request.POST.getlist('relevantoption')
in stead of
request.POST.getlist('options')
apart, why to use form here for single multiple choice field, even where you are modifying choices also and not using form.save too.

Django - MultipleCheckBoxSelector with m2m field - How to add object instead of save_m2m()

I use inlineformset_factory with a custom form option in order to change the queryset and the widget of a m2m field, ie: ezMap. I want the form to give the user the option to add or remove the current selected_map to the m2m field with CheckBoxSelectMultiple widget. However, I dont want to give the user the ability to remove other objects that were already there. The problem is when I save the formset with formset.save_m2m(), it overides the field and erase all objects that were already saved.
How could I just add a new object without erasing others?
models: (some of unecessary fields were removed)
class Shapefile(models.Model):
filename = models.CharField(max_length=255)
class EzMap(models.Model):
map_name = models.SlugField(max_length=50)
layers = models.ManyToManyField(Shapefile, verbose_name='Layers to display', null=True, blank=True)
class LayerStyle(models.Model):
styleName = models.SlugField(max_length=50)
layer = models.ForeignKey(Shapefile)
ezMap = models.ManyToManyField(EzMap)
forms:
class polygonLayerStyleFormset(forms.ModelForm):
add_to_map = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
self.map_selected = kwargs.pop("map_selected", None)
super(polygonLayerStyleFormset, self).__init__(*args, **kwargs)
self.fields['conditionStyle'].help_text = "Put * if you want to select the entire table"
self.fields['ezMap'].widget = forms.CheckboxSelectMultiple()
self.fields['ezMap'].queryset = EzMap.objects.filter(id=self.map_selected.id)
self.fields['ezMap'].help_text =""
class Meta:
model = LayerStyle
def save(self, *args, **kwargs):
instance = super(polygonLayerStyleFormset, self).save(*args, **kwargs)
instance.add_to_map = self.cleaned_data['add_to_map']
return instance
ftlStylePolygonFormset = inlineformset_factory(Shapefile, LayerStyle, can_delete=True, extra=1, max_num=5,
fields = ['styleName', 'conditionStyle', 'fillColor', 'fillOpacity', 'strokeColor', 'strokeWeight', 'ezMap'], form=polygonLayerStyleFormset)
views:
def setLayerStyle(request, map_name, layer_id):
map_selected = EzMap.objects.get(map_name=map_name, created_by=request.user)
layer_selected = Shapefile.objects.get(id=layer_id)
layerStyle_selected = LayerStyle.objects.filter(layer=layer_selected)
styleFormset = ftlStylePolygonFormset
if request.POST:
formset = styleFormset(request.POST, instance=layer_selected)
if formset.is_valid():
instances = formset.save()
for instance in instances:
if instance.add_to_map:
instance.ezMap.add(map_selecte)
else:
instance.ezMap.remove(map_selected)
save_link = u"/ezmapping/map/%s" % (map_name)
return HttpResponseRedirect(save_link)
else:
formset = styleFormset(instance=layer_selected)
#set initial data for add_to_map
for form in formset:
if form.instance.pk:
if map_selected in form.instance.ezMap.all():
form.fields['add_to_map'].initial = {'add_to_map': True}
I am confused as to what you're doing with the ezMap form field. You set its queryset to a single-element list, then use a CheckboxSelectMultiple widget for it. Are you setting up to let the user deselect that matching map, but not add new ones?
To do this at initialization, you need to define a custom base formset class and pass that in as the formset argument to your factory.
from django.forms.models import BaseInlineFormSet
class polygonLayerStyleForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.map_selected = kwargs.pop("map_selected", None)
super(polygonLayerStyleForm, self).__init__(*args, **kwargs)
self.fields['conditionStyle'].help_text = "Put * if you want to select the entire table"
self.fields['ezMap'].widget = forms.CheckboxSelectMultiple()
self.fields['ezMap'].queryset = EzMap.objects.filter(id=self.map_selected.id)
self.fields['ezMap'].help_text =""
class Meta:
model = LayerStyle
class polygonLayerStyleFormset(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
self.map_selected = kwargs.pop("map_selected", None)
super(polygonLayerStyleFormset, self).__init__(*args, **kwargs)
def _construct_form(self, i, **kwargs):
kwargs['map_selected'] = self.map_selected
return super(polygonLayerStyleFormset, self)._construct_form(i, **kwargs)
ftlStylePolygonFormset = inlineformset_factory(Shapefile, LayerStyle, formset=polygonLayerStyleFormset, form=polygonLaterStyleForm, # and other arguments as above
)
It might be simpler to just go through the formset forms and directly change the field's queryset after creating it in your view:
formset = ftlStylePolygonFormset(instance=layer_selected)
for form in formset.forms:
form.fields['ezMap'].queryset = EzMap.objects.filter(id=map_selected.id)
Speaking of which, the usual convention is to split the POST and GET cases in the view:
from django.shortcuts import render
def setLayerStyle(request, map_name, layer_id):
map_selected = EzMap.objects.get(map_name=map_name, created_by=request.user)
layer_selected = Shapefile.objects.get(id=layer_id)
layerStyle_selected = LayerStyle.objects.filter(layer=layer_selected)
if request.method == 'POST':
formset = ftlStylePolygonFormset(request.POST, instance=layer_selected, map_selected=map_selected)
if formset.is_valid():
instances = formset.save()
save_link = u"/ezmapping/map/%s" % (map_name)
return HttpResponseRedirect(save_link)
else:
formset = ftlStylePolygonFormset(instance=layer_selected, map_selected=map_selected)
return render(request, "ezmapping/manage_layerStyle.html", {'layer_style': layerStyle_selected, 'layerStyleformset': formset, 'layer': layer_selected})
And it's better to use the redirect shortcut to reverse lookup a view for your redirect on success rather than hardcode the target URL. And to use get_object_or_404 or some equivalent when accessing objects based on URL arguments - right now a bogus URL will trigger an exception and give the user a 500 status error, which is undesirable.
To conditionally add to the ezMap relationship:
class polygonLayerStyleForm(forms.ModelForm):
add_to_map = forms.BooleanField()
def save(self, *args, **kwargs):
instance = super(polygonLayerStyleForm, self).save(*args, **kwargs)
instance.add_to_map = self.cleaned_data['add_to-map']
return instance
Then in the view:
instances = formset.save()
for instance in instances:
if instance.add_to_map:
instance.ezMap.add(map_selected)
You could also do the add call in the save method, but then you'd have to set the map as member data sometime previously - and more importantly, deal with the commit=False case.

Django modelformset_factory() filtering

I'm needing to filter out a significant amount of objects from my query. Currently, it is grabbing all objects in the class, and I want to filter it to the relevant ones which are in a querystring. How can I do this? When I try, I get an Attribute Error stating
''QuerySet' object has no attribute '__name__'.'
The code that works, but very slowly is:
formset = modelformset_factory(Transaction, form=PaidDateForm, extra=0, can_delete=False)
Also, the formset:
formset = formset(request.POST, Transaction.objects.filter(pk__in=qs))
The QueryString that I am wanting to filter by is called 'qs.'
class PaidDateForm(forms.ModelForm):
formfield_callback = jquery_datefield
Amount",max_digits=14,decimal_places=2,required=False)
date_cleared = forms.DateField(label="Cleared Date",widget=JQueryDateWidget(), input_formats=settings.DATE_INPUT_FORMATS, required=False)
class Meta:
model = Transaction
include = ('date_time_created')
def __init__(self, *args, **kwargs):
self.queryset = Transaction.objects.filter(pk__in=qs)
super(PaidDateForm, self).__init__(*args, **kwargs)
for field in self.fields:
if field != 'date_cleared':
self.fields[field].widget = forms.HiddenInput()
self.fields['paid_amount'].widget.attrs['size'] = 12
self.initial['paid_amount'] = '%.2f' % (self.instance.usd_amount)
Look at the example in Django documentation:
https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#changing-the-queryset
If I understand your question correctly there is two approach to your problem:
First:
TransactionFormset = modelformset_factory(Transaction,form=PaidDateForm, extra=0, can_delete=False)
formset = TransactionFormset(queryset=Transaction.objects.filter(pk__in=qs))
Second options is to create BaseTransactionFormset
class BaseTransactionFormSet(BaseModelFormSet):
def __init__(self, *args, **kwargs):
super(BaseTransactionFormSet, self).__init__(*args, **kwargs)
#create filtering here whatever that suits you needs
self.queryset = Transaction.objects.filter()
formset = modelformset_factory(Transaction, formset=BaseTransactionFormSet,form=PaidDateForm, extra=0, can_delete=False)
Does this code help you?
I hesitate you still need it, and yet this code works for me
FormSet = modelformset_factory(YourModel, fields=(...))
form_set = FormSet(queryset = YourModel.objects.filter(...))

Validating Unique Fields in Django

I don't know if I'm approaching the problem in the right way. The intended outcome is to have a form that displays only name and description. Once the user submits the form I want to add the current user as owner and check if there's already an entry that has the same name and user. If there is, I want to return the form with errors. If not, I want to save Status.
My model:
class Status(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
owner = models.ForeignKey(User)
active = models.BooleanField(default=True)
class Meta:
unique_together = ('name','owner')
My View:
def settings_status(request):
status_form = StatusForm()
if request.method == 'POST':
status_form = StatusForm(request.POST)
if status_form.is_valid():
new_status = Status()
new_status.name = status_form.cleaned_data['name']
new_status.description = status_form.cleaned_data['description']
new_status.owner = request.user
new_status.save()
return render_to_response('base/settings_status.html',{
'status_form' : status_form,
}, context_instance=RequestContext(request))
I have tried numerous things, but I keep running into the problem that if I add owner to the object separately then it isn't available to the model's clean function and therefore can't be used to check if name and owner are unique.
Several ways to do this:
for example, passing in the user (owner) to the form:
forms.py:
class StatusForm(forms.Form):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user','')
super(StatusForm, self).__init__(*args, **kwargs)
self.fields['name'] = forms.CharField(label='Name')
self.fields['description'] = CharField(label='Description', widget=forms.Textarea)
def clean(self):
cleaned_data = self.cleaned_data
name = cleaned_data.get('name')
if Status.objects.filter(name=name, owner=self.user).exists():
self._errors['name'] self.error_class(['Status with this name exists'])
return cleaned_data
views.py:
def settings_status(request):
if request.method == 'POST':
status_form = StatusForm(request.POST, user=request.user)
if status_form.is_valid():
new_status = Status()
new_status.name = status_form.cleaned_data['name']
new_status.description = status_form.cleaned_data['description']
new_status.owner = request.user
new_status.save()
else:
status_form = StatusForm(user=request.user)
context = {'status_form':status_form,}
return render_to_response('base/settings_status.html', context,
context_instance=RequestContext(request))
Also look at setting initial data depending on your form setup and consider using a ModelForm.