Passing kwargs to nested forms in Django inline formsets - django

I'm new to Django and I am trying to implement a change tracking system that allows users to assign change types, reasons, and comments to any changes that occur on item instances of a Django model class. I modeled my problem after this example https://github.com/philgyford/django-nested-inline-formsets-example I have a model FK hierarchy of Item -> ItemHistory -> Item Change Reasons (can be multiple reasons per change). I would like to see all changes for a given item and all reasons and comments for all changes for that item in the same page. For the change type and reason, I have implemented a dependent dropdown using JQuery - the queryset for change reasons depend on the selection for change type. The problem I'm facing is displaying the saved change types and change reasons for an existing change on the update view. I'm trying to pass in different querysets to the ChangeReasonsForm form, which are implemented as nested fields in the BaseVersionReasonsFormset formset. I able to pass the querysets from the view up to BaseVersionReasonsFormset but can't seem to figure out how to pass it to the nested forms. Appreciate any help and suggestions in advance!
Forms.py
class ChangeReasonsForm(forms.ModelForm):
change_reason = forms.ModelChoiceField(queryset=ChangeReason.objects.none())
class Meta:
model = ChangeReasons
fields = ('change_type_id', 'change_reason', 'comment')
def __init__(self, *args, **kwargs):
qs_dict = kwargs.pop('change_reasons')
index = kwargs.pop('index')
qs = list(qs_dict.values())[index]
super(ChangeReasonsForm, self).__init__(*args, **kwargs)
self.fields['change_reason'].queryset = ChangeReason.objects.none()
self.fields['change_type_id'].empty_label = ""
self.fields['change_reason'].empty_label = ""
self.helper = FormHelper(self)
if self.instance.pk:
self.fields['change_reason'].queryset = qs
ChangeReasonsFormSet = inlineformset_factory(
ItemHistoryJoiner,
ChangeReasons,
form=ChangeReasonsForm,
extra=1,
can_delete=True,
)
class BaseVersionReasonsFormset(BaseInlineFormSet):
"""
The base formset for editing Changes belonging to a Item, and the
Reasons belonging to those Changes.
"""
def get_form_kwargs(self, index):
kwargs = super(BaseVersionReasonsFormset, self).get_form_kwargs(index)
change_reasons_list = kwargs.pop('change_reasons_list')
print('index: {}'.format(index))
print('reasons list: {}'.format(change_reasons_list))
print('list index i: {}'.format(change_reasons_list[index]))
self.change_reasons = change_reasons_list[index]
print('self.change_reasons: {}'.format(self.change_reasons))
return kwargs
def add_fields(self, form, index):
super().add_fields(form, index)
# Save the formset for a Change's Reasons in the nested property.
form.nested = ChangeReasonsFormSet(
instance=form.instance,
data=form.data if form.is_bound else None,
files=form.files if form.is_bound else None,
prefix='change_reason-%s-%s' % (
form.prefix,
ChangeReasonsFormSet.get_default_prefix()
),
form_kwargs={'change_reasons': self.change_reasons,
'index': index,
},
)
def is_valid(self):
"""
Also validate the nested formsets.
"""
result = super().is_valid()
if self.is_bound:
for form in self.forms:
if hasattr(form, 'nested'):
result = result and form.nested.is_valid()
return result
def clean(self):
"""
If a parent form has no data, but its nested forms do, we should
return an error, because we can't save the parent.
For example, if the Change form is empty, but there are Reasons.
"""
super().clean()
for form in self.forms:
if not hasattr(form, 'nested') or self._should_delete_form(form):
continue
if self._is_adding_nested_inlines_to_empty_form(form):
form.add_error(
field=None,
error=('You are trying to add reason(s) to a change which '
'does not yet exist. Please add information '
'about the change and specify the reason(s) again.'))
def save(self, commit=True):
"""
Also save the nested formsets.
"""
result = super().save(commit=commit)
for form in self.forms:
if hasattr(form, 'nested'):
if not self._should_delete_form(form):
form.nested.save(commit=commit)
return result
def _is_adding_nested_inlines_to_empty_form(self, form):
"""
Are we trying to add data in nested inlines to a form that has no data?
e.g. Adding reasons to a new version whose data we haven't entered?
"""
if not hasattr(form, 'nested'):
# A basic form; it has no nested forms to check.
return False
if is_form_persisted(form):
# We're editing (not adding) an existing model.
return False
if not is_empty_form(form):
# The form has errors, or it contains valid data.
return False
# All the inline forms that aren't being deleted:
non_deleted_forms = set(form.nested.forms).difference(
set(form.nested.deleted_forms)
)
# At this point we know that the "form" is empty.
# In all the inline forms that aren't being deleted, are there any that
# contain data? Return True if so.
return any(not is_empty_form(nested_form) for nested_form in non_deleted_forms)
# This is the formset for the versions belonging to a Item and the
# reasons belonging to those versions.
# You'd use this by passing in a Item:
VersionsWithCommentsFormset = inlineformset_factory(
Item,
ItemHistory,
formset=BaseVersionReasonsFormset,
# We need to specify at least one ItemHistory field:
fields=('version',),
widgets={
'version': forms.HiddenInput()
},
extra=0,
max_num=0,
# If you don't want to be able to delete Items:
can_delete=False,
)
views.py
class ChangesUpdateView(SingleObjectMixin, FormView):
"""
For assigning reasons and comments to changes
"""
model = Item
template_name = 'app/Item_changes_update.html'
def get(self, request, *args, **kwargs):
# The Item we're editing:
self.object = self.get_object(queryset=Item.objects.all())
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
# The Publisher we're uploading for:
self.object = self.get_object(queryset=Item.objects.all())
return super().post(request, *args, **kwargs)
def get_form(self, form_class=None):
"""
Use our big formset of formsets, and pass in the Item object.
"""
item = self.object
item_history_set = item.history_set.all()
version_reasons_dict = OrderedDict({'version_id_{}'.format(version.version_id):
{'comment_id_{}'.format(
comment.pk): comment.change_type_id.change_reasons.all()
for comment in version.reasons.all()
}
for version in item_history_set
})
version_reasons_list = list(version_reasons_dict.values())
version_comment_formset = VersionsWithCommentsFormset(
**self.get_form_kwargs(),
instance=self.object,
form_kwargs={'change_reasons_list': version_reasons_list}
)
return version_comment_formset
def form_valid(self, form):
"""
If the form is valid, redirect to the supplied URL.
"""
# if self.request.POST.get('submit'):
if form.is_valid():
form.save()
messages.add_message(
self.request,
messages.SUCCESS,
'Changes were successfully saved.'
)
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
return reverse('app:change_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
# Call the base implementation first to get a context
context = super().get_context_data(**kwargs)
# Add in a QuerySet of all the historical versions
context['item_history'] = get_item_history(self.object)
return context

Related

Setting field value 'user' in view before form validation

I'm trying to set field value 'user' in view before validation as you can see in my example below. But I still get the validation message user is required, suggesting it's not being set. What I'm I doing wrong?
thanks,
view.py
def add_batch(request):
# If we had a POST then get the request post values.
if request.method == 'POST':
form = BatchForm(data=request.POST, initial={'user': request.user})
# Check we have valid data before saving trying to save.
if form.is_valid():
# Clean all data and add to var data.
data = form.cleaned_data
groups = data['groups'].split(",")
for item in groups:
batch = Batch(content=data['content'],
group=Group.objects.get(pk=item),
user=request.user
)
batch.save()
return redirect(batch.get_send_conformation_page())
else:
context = {'form': form}
return render_to_response('sms/sms_standard.html', context, context_instance=RequestContext(request))
form.py
class BatchForm(forms.ModelForm):
class Meta:
model = Batch
def __init__(self, user=None, *args, **kwargs):
super(BatchForm, self).__init__(*args,**kwargs)
if user is not None:
form_choices = Group.objects.for_user(user)
else:
form_choices = Group.objects.all()
self.fields['groups'] = forms.ModelMultipleChoiceField(
queryset=form_choices
)
As the documentation explains, initial values are not used to set data in forms, they are only used for displaying, well, initial values.
If you don't want to display the user but want to set it automatically, the best thing to do is to exclude the user field from the ModelForm altogether and set it in the view when you save. Alternatively, since you are passing it in as a parameter for other reasons, you could perhaps add that to the POST data:
def __init__(self, user=None, *args, **kwargs):
super(BatchForm, self).__init__(*args,**kwargs)
if user is not None:
if self.data:
self.data['user'] = user
form = BatchForm(request.user, request.POST)
# Check we have valid data before saving trying to save.
if form.is_valid():
[.........]

form_valid method in django-extra-views. In reality form(s)_valid

I am trying to use Django Extra Views pack to create new entry based on model + inline formset + extra information from the USER model. I know how to do it via function based views but now trying to decrease amount of the code:
I have 2 models + user model:
Model1: # primary model
author = models.ForeignKey("ExtraUser", )
+some fileds
Model2 # secondary model
photo = models.ForeignKey("Model1", )
+ some fields
# user model
Model ExtraUser(AbstractBaseUser)
+ some fileds
I use following VIEW to render and save it all together:
class ItemInline(InlineFormSetFactory):
model = Model2
fields = ["somefiled"]
class CreateBoatView(SuccessMessageMixin, LoginRequiredMixin, CreateWithInlinesView):
model = Model1
inlines = [ItemInline]
fields = ["list of the fields here"]
template_name = 'create.html'
def get_success_url(self):
return reverse('app:url', args=(self.object.pk, ))
It all work except 1 thing: I cant appoint current user as an entry author, that is author = models.ForeignKey("ExtraUser", ) is always NULL
in ancestor function based view I used to do following:
if form1.is_valid():
prim = form1.save(commit=False)
prim.author = request.user # that is I connect this entry to the current user.
# + do something + save(commit=True) finally.
How to do same stuff in CreateWithInlinesView?
tried following. Doenst work
def dispatch(self, request, *args, **kwargs):
self.user = request.user
return CreateWithInlinesView.dispatch(self, request, *args, **kwargs)
def form_valid(self, form): #(self, form, inlines)??
self.object = form.save(commit=False)
self.object.author = self.request.user
self.object.save()
return HttpResponseRedirect(self.get_success_url())
# super-class form_valid method (for reference)
def forms_valid(self, form, inlines):
"""
If the form and formsets are valid, save the associated models.
"""
self.object = form.save()
for formset in inlines:
formset.save()
return HttpResponseRedirect(self.get_success_url())
Well, thanks a lot to authors of the Django Extra Views , as they have added special method instead of the form_valid....
guess how it called???
forms_valid . You do need few seconds to get the difference, right??
for me it took around 5 hours to find it out.
FINALY:
def forms_valid(self, form, inlines): #yes, f%%ng form(s)_valid, yeh...
"""
If the form and formsets are valid, save the associated models.
"""
self.object = form.save(commit=False)
self.object.author = self.request.user
form.save(commit=True)
for formset in inlines:
formset.save()
return HttpResponseRedirect(self.get_success_url())

Show a paginated ListView and an UpdateView on the same template page

I am trying to create a Django page where something can be updated and something can be viewed in a paginated table. The model looks like this:
class CostGroup(models.Model):
name = models.CharField(max_length=200)
description = models.CharField(max_length=200)
def get_absolute_url(self):
return reverse(
'costgroup_detail',
kwargs={
'costgroup_pk': self.pk,
}
)
class Cost(models.Model):
cost_group = models.ForeignKey(CostGroup)
amount = models.DecimalField(max_digits=50, decimal_places=2)
def get_absolute_url(self):
return reverse(
'cost_detail',
kwargs={
'cost_pk': self.pk,
}
)
So the edit form is for the name and description fields of the CostGroup model and the table should show a list of the 'amounts`
I previously had it working by just having an UpdateView for the form and the table included in the form template. Now though, as I want to include pagination on the table, I need to use two views on the same page. The page I have designed should look something like this in the end:
I am not worried about the styling at the moment my main focus at the moment is getting the form and the table on the same page. In its current state the only thing that I don't have is the pagination for the table:
The view currently looks like this:
class CostDetail(UpdateView):
model = models.Cost
pk_url_kwarg = 'cost_pk'
template_name = 'main/cost_detail.html'
form_class = forms.CostDetailEditForm
success_url = reverse_lazy('cost_list')
I have a feeling that leveraging the underlying mixins that the Django CBVs use is probably the way to go but I am not sure how to begin with this.
Any help would be much appreciated
Thanks for your time
(This clarification seemed to work better as a new answer)
It looks like you're dealing with both of the tables. The object level is using CostGroup, while the List view is showing the child records from Cost linked to a CostGroup. Assuming that is true, here's how I would proceed:
class CostDetail(ModelFormMixin, ListView):
model = CostGroup # Using the model of the record to be updated
form_class = YourFormName # If this isn't declared, get_form_class() will
# generate a model form
ordering = ['id']
paginate_by = 10
template_name = 'main/cost_detail.html' # Must be declared
def get_queryset(self):
# Set the queryset to use the Cost objects that match the selected CostGroup
self.queryset = Cost.objects.filter(cost_group = get_object())
# Use super to add the ordering needed for pagination
return super(CostDetail,self).get_queryset()
# We want to override get_object to avoid using the redefined get_queryset above
def get_object(self,queryset=None):
queryset = CostGroup.objects.all()
return super(CostDetail,self).get_object(queryset))
# Include the setting of self.object in get()
def get(self, request, *args, **kwargs):
# from BaseUpdateView
self.object = self.get_object()
return super(CostDetail,self).get(request, *args, **kwargs)
# Include the contexts from both
def get_context_data(self, **kwargs):
context = ModelFormMixin.get_context_data(**kwargs)
context = ListView.get_context_data(**context)
return context
# This is the post method found in the Update View
def post(self, request, *args, **kwargs):
# From BaseUpdateView
self.object = self.get_object()
# From ProcessFormView
form = self.get_form()
self.form = form
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def put(self, *args, **kwargs):
return self.post(*args, **kwargs)
I haven't tried to run this, so there may be errors. Good luck!
(Remember ccbv.co.uk is your friend when digging into Class-based Views)
An app I'm working on now uses a similar approach. I start with the ListView, bring in the FormMixin, and then bring in post() from the FormView.
class LinkListView(FormMixin, ListView):
model = Link
ordering = ['-created_on']
paginate_by = 10
template_name = 'links/link_list.html'
form_class = OtherUserInputForm
#=============================================================================#
#
# Handle form input
#
def post(self, request, *args, **kwargs):
"""
Handles POST requests, instantiating a form instance with the passed
POST variables and then checked for validity.
"""
form = self.get_form()
self.form = form
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def put(self, *args, **kwargs):
return self.post(*args, **kwargs)
def get_success_url(self):
return reverse('links')
You may also wish to override get_object(), get_queryset(), and get_context().

Filtering a model in a CreateView with get_queryset

I'm trying to filter a model with get_queryset() and it seems to work in the view but not in the template.
My view :
class FolderCreate(CreateView):
fields = ['name', 'parent']
template_name = 'Form/folder_create.html'
def get_queryset(self):
folders = Folder.objects.filter(owner=self.request.user)
print folders # ==> return [<Folder: Folder>, <Folder: Another folder>]
return folders
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.owner = self.request.user
return super(FolderCreate, self).form_valid(form)
def get_initial(self):
if self.request.method == 'GET':
foldersUrl = self.request.META['HTTP_REFERER'].split('/')
foldersUrl.pop()
folder = urllib2.unquote(foldersUrl[-1])
try:
return {'parent' : Folder.objects.get(name=folder, owner=self.request.user)}
except Folder.DoesNotExist:
pass
As you can see, folders return two objects related to the session user in get_queryset() : 'Folder' and 'Another folder
Infortunately, the combobox of my template get all the folders, without any filtering.
Any idea ?
The issue here is that get_queryset is not used in a CreateView, as it's meant for filtering the models returned for display in a list or detail view. You want something completely different: you want to filter the choices available in a form field.
To do that you will need to create a custom ModelForm that accepts a user kwarg and filters the queryset accordingly:
class FolderForm(forms.ModelForm):
class Meta:
model = Folder
fields = ['name', 'parent']
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super(FolderForm, self).__init__(*args, **kwargs)
self.fields['parent'].queryset = Folder.objects.filter(user=user)
and then change your view to use that form and pass in the user parameter:
class FolderCreate(CreateView):
template_name = 'Form/folder_create.html'
form_class = FolderForm
def get_form_kwargs(self):
kwargs = super(FolderCreate, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs

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.

Resources