That is indeed correct. The correct should be:
form_a_set = set(range(form_a_sr.lower, form_a_sr.upper))
form_b_set = set(range(form_b_sr.lower, form_b_sr.upper))
Still haven’t figured out why it doesn’t show me the validation error.
That is indeed correct. The correct should be:
form_a_set = set(range(form_a_sr.lower, form_a_sr.upper))
form_b_set = set(range(form_b_sr.lower, form_b_sr.upper))
Still haven’t figured out why it doesn’t show me the validation error.
Assuming you haven’t fixed handle_forms as mentioned previously, you’ve still got an issue with handling these errors. You’re still trying to catch these errors as an exception - but the is_valid
test handles the exceptions and does not propagate them.
The errors reported from forms is in the Form.errors
object, and not propagated as exceptions.
Nope, I already made the same* clean() method on both models (Bins and BinsHistory), I’m not getting IntegrityErrors anymore, but I’m not getting an expected form error (ValidationError) from the bins_formset.is_valid()/bins_history_formset.is_valid(). I’ve changed my formset clean method to raise ValidationErrors regardless, and while if formset.is_valid() fails, the form isn’t getting populated with any error message.
if bins_formset.is_valid():
bins = bins_formset.save(commit=False)
for bin_instance in bins:
bin_instance.parent_transaction = transaction_instance
bin_instance.located_at = transaction_instance.recieving_warehouse
bin_instance.save()
print('bin_instance is being saved' + bin_instance.errors)
print('After bins_formset.is_valid()' + str(bins_formset.errors))
This only outputs:
After bins_formset.is_valid() [{}, {}, {}, {}, {}, {}]
Then I would need to see current copies of all the code involved here. This would include the entire view, forms, formsets, models, and supporting functions. (I know you’ve been working on things along the way, so I don’t want to try and piece a picture together from the different elements posted previously.)
I’d also suggest that you run this through a debugger or start liberally sprinkling print statements through this to start getting a picture of the flow of events through this process.
Note: I’m aware that there’s no condition for the ValidationError, this is deliberate so I can see what’s failing with it (why it isn’t shown in the form). As said in my previous post, formset.is_valid() correctly returns false, but still formset.errors is empty (it doesn’t print the inside of bin_instance, but does print bin_formset).
View:
class DocumentCreationView(View):
template_name = "manager/document_creation_template.html"
creating_warehouse = Warehouse.objects.filter(
can_create=True).first()
contact_list = Contact.objects.filter(
deleted_at__isnull=True,
authorized_warehouse__isnull=True,
)
recieving_warehouse = Warehouse.objects.filter(can_recieve=True)
def get(self, request):
transaction_form = DocumentCreationForm()
bins_formset = BinsFormset()
context = {
'transaction_form': transaction_form,
'bins_formset': bins_formset,
}
return render(request, self.template_name, context)
@transaction.atomic
def handle_forms(self,
transaction_form,
bins_formset,
bins_history_formset):
if transaction_form.is_valid():
transaction_instance = transaction_form.save(commit=False)
transaction_instance.sending_warehouse = self.creating_warehouse
# Change for user
transaction_instance.started_by = transaction_instance.sending_contact
transaction_instance.save()
bins = None
if bins_formset.is_valid():
bins = bins_formset.save(commit=False)
for bin_instance in bins:
bin_instance.parent_transaction = transaction_instance
bin_instance.located_at = transaction_instance.recieving_warehouse
bin_instance.save()
print('bin_instance is being saved' +
bin_instance.errors)
print('After bins_formset.is_valid()' + str(bins_formset.errors))
if bins_history_formset.is_valid():
bins_history = bins_history_formset.save(commit=False)
for bin_history in bins_history:
bin_history.parent_transaction = transaction_instance
bin_history.save()
return HttpResponseRedirect(
reverse('manager:set_transfered_bins'))
print('After bins_history_formset.is_valid()' + str(bins_history_formset.errors))
def post(self, request):
transaction_form = DocumentCreationForm(request.POST)
bins_formset = BinsFormset(
request.POST,
prefix='bins_set')
bins_history_formset = BinsHistoryFormset(
request.POST,
prefix='bins_set')
self.handle_forms(
transaction_form,
bins_formset,
bins_history_formset)
context = {
'transaction_form': transaction_form,
'bins_formset': bins_formset,
}
return render(
request, self.template_name, context)
class OverlappingInlineFormSet(forms.BaseInlineFormSet):
def clean(self):
super().clean()
for form_a, form_b in combinations(self.forms, 2):
form_a_dt = form_a.cleaned_data['document_type']
form_b_dt = form_b.cleaned_data['document_type']
form_a_s = form_a.cleaned_data['series']
form_b_s = form_b.cleaned_data['series']
form_a_sr = form_a.cleaned_data['serial_range']
form_b_sr = form_b.cleaned_data['serial_range']
form_a_set = set(range(form_a_sr.lower, form_a_sr.upper))
form_b_set = set(range(form_b_sr.lower, form_b_sr.upper))
# This is deliberated here. I'm trying to make sure the error is shown.
raise forms.ValidationError(
_('Range %(form_b_range)s already exist for '
'the series %(form_a_series)s of the type '
'%(form_a_document)s is already used for'
'range %(form_b_range)s.'),
params={'form_a_series': form_a_s,
'form_a_range': form_a_sr,
'form_a_document': form_a_dt,
'form_b_series': form_b_s,
'form_b_range': form_a_sr,
'form_b_document': form_b_dt},
code='overlapping_form_ranges',
)
class BinsForm(forms.ModelForm):
class Meta:
model = Bins
fields = ['document_type', 'series',
'serial_range']
class BinHistoryForm(forms.ModelForm):
class Meta:
model = BinsHistory
fields = ['document_type', 'series',
'serial_range']
BinsFormset = forms.inlineformset_factory(
Transactions, Bins, form=BinsForm,
extra=6, can_delete=False, max_num=12,
formset=OverlappingInlineFormSet)
BinsHistoryFormset = forms.inlineformset_factory(
Transactions, BinsHistory, form=BinHistoryForm,
extra=60, can_delete=False, max_num=60,
formset=OverlappingInlineFormSet)
Models
class BinsHistory(models.Model):
class DocumentTypes(models.IntegerChoices):
BIRTH = 1
DEATH = 2
FETAL_DEATH = 3
class DocumentStatus(models.IntegerChoices):
EMPTY = 1
FILLED = 2
VOID = 3
document_type = models.IntegerField(
choices=DocumentTypes.choices)
document_status = models.IntegerField(
choices=DocumentStatus.choices,
default=DocumentStatus.EMPTY)
serial_range = IntegerRangeField()
series = models.PositiveIntegerField()
created_at = models.DateTimeField(auto_now_add=True)
parent_transaction = models.ForeignKey(
Transactions,
on_delete=models.PROTECT)
class Meta:
verbose_name = _('Bin history')
verbose_name_plural = _('Bins history')
constraints = [
ExclusionConstraint(
name='bin_history_ranges_overlap',
expressions=[
(models.F('parent_transaction'), RangeOperators.EQUAL),
(models.F('document_type'), RangeOperators.EQUAL),
('series', RangeOperators.EQUAL),
('serial_range', RangeOperators.OVERLAPS),
])]
def clean(self):
parent_transaction = self.parent_transaction
document_type = self.document_type
series = self.series
lower = self.serial_range.lower
upper = self.serial_range.upper
serial_range = NumericRange(
lower, upper, '[]')
if (
BinsHistory.objects.filter(
Q(parent_transaction=parent_transaction) &
Q(document_type=document_type) &
Q(series=series) &
Q(serial_range__overlap=serial_range))
.exists()
):
raise ValidationError(
_('Range %(range)s already exist for '
'the series %(series)s of the type '
'%(document)s.'),
params={'series': series,
'range': serial_range,
'document': document_type},
code='overlapping_history_ranges',
)
@property
def get_bin_size(self):
lower = self.serial_range.lower
upper = self.serial_range.upper
return 1 + upper - lower
@property
def get_bin_range(self):
lower = self.serial_range.lower
upper = self.serial_range.upper
return f'{lower}-{upper}'
def delete(self, *args, **kwargs):
return ProtectedError(_('Can''t delete history'), self)
def save(self, *args, **kwargs):
if self.serial_range.upper_inc:
super().save(*args, **kwargs)
return
lower = self.serial_range.lower
upper = self.serial_range.upper
self.serial_range = NumericRange(
lower, upper, '[]')
super().save(*args, **kwargs)
class Bins(models.Model):
class DocumentTypes(models.IntegerChoices):
BIRTH = 1
DEATH = 2
FETAL_DEATH = 3
class DocumentStatus(models.IntegerChoices):
EMPTY = 1
FILLED = 2
VOID = 3
document_type = models.IntegerField(
choices=DocumentTypes.choices)
document_status = models.IntegerField(
choices=DocumentStatus.choices,
default=DocumentStatus.EMPTY)
created_at = models.DateTimeField(auto_now_add=True)
series = models.PositiveIntegerField()
serial_range = IntegerRangeField()
located_at = models.ForeignKey(
Warehouse,
on_delete=models.PROTECT)
parent_transaction = models.ForeignKey(
Transactions,
on_delete=models.PROTECT)
objects = BinsManager()
def close_bounds(unbound_range):
lower_bound = unbound_range.lower
upper_bound = unbound_range.upper
return NumericRange(
lower_bound, upper_bound, '[]')
def clean(self):
document_type = self.document_type
series = self.series
lower = self.serial_range.lower
upper = self.serial_range.upper
serial_range = NumericRange(
lower, upper, '[]')
if (
Bins.objects.filter(
Q(document_type=document_type) &
Q(series=series) &
Q(serial_range__overlap=serial_range))
.exists()
):
raise ValidationError(
_('Range %(range)s already exist for '
'the series %(series)s of the type '
'%(document)s.'),
params={'series': series,
'range': serial_range,
'document': document_type},
code='overlapping_ranges',
)
def save(self, *args, **kwargs):
if self.serial_range.upper_inc:
super().save(*args, **kwargs)
return
lower = self.serial_range.lower
upper = self.serial_range.upper
self.serial_range = NumericRange(
lower, upper, '[]')
super().save(*args, **kwargs)
class Meta:
verbose_name = _('Bin')
verbose_name_plural = _('Bins')
constraints = [
ExclusionConstraint(
name='bin_ranges_overlap',
expressions=[
(models.F('document_type'), RangeOperators.EQUAL),
('series', RangeOperators.EQUAL),
('serial_range', RangeOperators.OVERLAPS),
],
),
]
Thanks. I’m going to recreate this in one of my test environments to see what I get.
Ahh, it just sunk in that you’re using an InlineFormset here - that needs to be created with the instance of the base model to which the individual entries are related. (See Inline Formsets)
I’m fairly certain that the Transactions instance that the Bins are going to relate to need to be saved first in this circumstance and then use that instance to create the instance of the InlineFormset objects.
Or, if you really want to process these “together”, you could probably replace the InlineFormset with a ModelFormset, and set that field specifically as you are processing that form.
Side note: In addition to printing the errors for the formset, you may also want to print the errors for each of the individual forms.
But I’m saving the transaction instance and setting it up before hand.
if transaction_form.is_valid():
transaction_instance = transaction_form.save(commit=False)
....
transaction_instance.save()
bins = bins_formset.save(commit=False)
if bins_formset.is_valid():
'''
This correctly returns False, because I'm unconditionally raising a ValidationError
from my formset definition.
'''
for bin_instance in bins:
bin_instance.parent_transaction = transaction_instance
bin_instance.save()
This above correctly processes the forms and saves all the models. It’s wrong? Why it works well except for ValidationErrors?
If I’m set on using inlineformsets do I really have to do something like:
transaction_form = DocumentCreationForm(request.POST)
if transaction_form.is_valid():
transaction_instance = transaction_form.save()
bins_formset = BinsFormset(request.POST,prefix='bins_set', instance=transaction_instance)
bins_history_formset = BinsHistoryFormset(request.POST,prefix='bins_set', instance=transaction_instance)
...
That would make keeping the entire operation atomic very convoluted, since now instead of creating all my objects upfront, I have to move request
object into the atomic operation since I need to save the Transaction model before I could create the Bins/BinsHistory instances. Could I make something like:
transaction_form = DocumentCreationForm(request.POST)
bins_formset = BinsFormset(request.POST,prefix='bins_set')
bins_history_formset = BinsHistoryFormset(request.POST,prefix='bins_set')
with transaction.atomic:
if transaction_form.is_valid():
transaction_instance = transaction_form.save(commit=False)
transaction_instance.save()
bins_formset(instance=transaction_instance)
if bins_formset.is_valid():
bins_formset.save()
But what I can’t comprehend yet is how would that make the ValidationError being properly populated into formset.errors? I’m not understanding that. That’s very unintuitive because they don’t seem to care what I’m doing with them, like presenting two forms with relations between them, but not declaring them outright, since Django seems to process them happily. Shouldn’t be instance
a required parameter to pass when I’m initializing?
No, you are not.
def post(self, request):
transaction_form = DocumentCreationForm(request.POST)
bins_formset = BinsFormset(
request.POST,
prefix='bins_set')
bins_history_formset = BinsHistoryFormset(
request.POST,
prefix='bins_set')
self.handle_forms(
transaction_form,
bins_formset,
bins_history_formset)
You are creating the formsets before you call handle_forms
.
Not necessarily. The creation of the forms within a formset is a “lazy” process. Those forms don’t exist until the first time they’re needed, allowing for that type of information to be supplied after the definition but before usage.
You are only going to get the understanding that you’re looking for by digging into exactly what’s happening here. There’s a point at which, when you’re looking for a degree of understanding of details that aren’t covered within the docs, there really isn’t anything better than going to the source to see how it’s all put together.
Spend the time in the source code of these functions, and run your code under more controlled and observable conditions. That will help provide the clarity you seek.
This seems to be a confusion about the difference between how forms vs formset behave. Formsets before 4.0 will not show their error in the rendered template, that changed after 4.0. So, while I can find form.errors, formset errors are simply not there. The only confusion I have is why errors is populated with two empty objects, but the documentation seems to hint that this is normal and expected.
For future reference, this is how it looks like, to check that the formset isn’t inserting the same data:
class CheckOverlappingInlineFormSet(forms.BaseInlineFormSet):
def clean(self):
super().clean()
if any(self.errors):
return
for form_a, form_b in combinations(self.forms, 2):
form_a_dt = form_a.cleaned_data['document_type']
form_a_s = form_a.cleaned_data['series']
form_a_sr = form_a.cleaned_data['serial_range']
form_a_set = set(range(form_a_sr.lower, form_a_sr.upper))
form_b_dt = form_b.cleaned_data['document_type']
form_b_s = form_b.cleaned_data['series']
form_b_sr = form_b.cleaned_data['serial_range']
form_b_set = set(range(form_b_sr.lower, form_b_sr.upper))
if form_a_dt != form_b_dt:
return
if form_a_s != form_b_s:
return
if not form_a_set.isdisjoint(form_b_set):
return
raise forms.ValidationError(
_('Range %(form_a_range)s already exist for '
'the series %(form_a_series)s of the type '
'%(form_a_document)s is already used for '
'range %(form_b_range)s.'),
params={'form_a_series': form_a_s,
'form_a_range': form_a_sr,
'form_a_document': form_a_dt,
'form_b_series': form_b_s,
'form_b_range': form_a_sr,
'form_b_document': form_b_dt},
code='overlapping_form_ranges',
)
BinsFormset = forms.inlineformset_factory(
Transactions, Bins, form=BinsForm,
extra=6, can_delete=False, max_num=12,
formset=CheckOverlappingInlineFormSet)
BinsHistoryFormset = forms.inlineformset_factory(
Transactions, BinsHistory, form=BinHistoryForm,
extra=60, can_delete=False, max_num=60,
formset=CheckOverlappingInlineFormSet)
In your model to verify that it doesn’t exist already on your database:
def clean(self):
document_type = self.document_type
series = self.series
lower = self.serial_range.lower
upper = self.serial_range.upper
serial_range = NumericRange(
lower, upper, '[]')
if (
Bins.objects.filter(
Q(document_type=document_type) &
Q(series=series) &
Q(serial_range__overlap=serial_range))
.exists()
):
raise ValidationError(
_('Range %(range)s already exist for '
'the series %(series)s of the type '
'%(document)s.'),
params={'series': series,
'range': serial_range,
'document': document_type},
code='overlapping_ranges',
)
The ValidationError message from your model is automatically added to the form, for the formset you can access it from your template if you use Django pre-4.0:
<form action="{% url 'manager:document_creation' %}"
method="post">
{% csrf_token %}
{{ transaction_form }}
<table>
{{ bins_formset.non_form_errors }}
{{ bins_formset.as_table }}
</table>
<input type="submit" value="Crear">
</form>
You have to pass the same object you applied formset.is_valid() in your view render context:
def post(self, request):
transaction_form = DocumentCreationForm(request.POST)
bins_formset = BinsFormset(
request.POST,
prefix='bins_set')
if transaction_form.is_valid() and bins_formset.is_valid() and bins_history_formset.is_valid():
#handle valid data
context = {
'transaction_form': transaction_form,
'bins_formset': bins_formset, # This can't be missing.
}
return render(
request, self.template_name, context)