Django: Create inline forms similar to django admin - django

this has been asked before, but not answered (from what I could find).
I have two models;
class Invoice(models.Model):
invoice_number = models.CharField(max_length=30)
invoice_date = models.DateField()
class LineItem(models.Model):
description = models.CharField(max_length=50)
unit_price = models.DecimalField(max_digits=9, decimal_places=2)
quantity = models.PositiveSmallIntegerField()
invoice = models.ForeignKey(Invoice)
In the django admin interface I have LineItem as an Inline to Invoice. This works well because I want to add my invoice line items at the same time as raising my invoice.
Are there example patterns on how similar nested/inline forms have been achieved in django applications? The view I would like to present to the user is similar to the admin interface where lineitems can be entered at the time of invoice creation.
Appreciate any guidance.

I've researched the few suggestions I received here, and have decided on a solution that is slightly different but very much on track with #rix suggestion of using django formsets; model based inlineformset.
I ended up using the django inlineformset_factory for LineItems and kept Invoice as a standard ModelForm.
My forms;
#forms.py
...
class LineItemForm(ModelForm):
class Meta:
model = LineItem
fields = ['description','unit_price','quantity']
class InvoiceForm(ModelForm):
class Meta:
model = Invoice
fields = ['invoice_date','invoice_number']
My view;
#views.py
...
def add_invoice(request):
LineItemFormSet = inlineformset_factory(Invoice, LineItem, form=LineItemForm, extra=5)
if request.method == 'POST':
invoice_form = InvoiceForm(request.POST, prefix='invoice')
lineitem_formset = LineItemFormSet(request.POST, request.FILES, prefix='line')
if invoice_form.is_valid() and lineitem_formset.is_valid():
invoice = invoice_form.save()
# I recreate my lineitem_formset bound to the new invoice instance
lineitem_formset = LineItemFormSet(request.POST, request.FILES, prefix='line', instance=invoice)
# I have to validate (again - so I'm confident) to access clean data
lineitem_formset.is_valid()
lineitem_formset.save()
if 'submit_more' in request.POST:
return HttpResponseRedirect(reverse('invoices:add_invoice'))
else:
return HttpResponseRedirect(reverse('invoices:get_invoices'))
else:
return render(request, 'invoices/invoice_add.html', {
'message' : "Check your form",
'invoice_form' : invoice_form,
'lineitem_formset' : lineitem_formset,
})
else:
invoice_form = InvoiceForm(prefix='invoice')
lineitem_formset = LineItemFormSet(prefix='line')
return render(request, 'invoices/invoice_add.html', {
'invoice_form' : invoice_form,
'lineitem_formset' : lineitem_formset,
})
Now, as suggested by #tr33hous, I'm playing with JS to add new inline forms.
I'm pretty happy with the outcome.
However, i'm wondering if there is a better way then creating and validating my inline formset to make sure it's all valid before saving my invoice and then recreating the formset object bound to the new invoice instance, not a major overhead, but it doesn't look as clean as it could.

I think you might be looking for formsets:
https://docs.djangoproject.com/en/dev/topics/forms/formsets/

Related

Fill Foreign Key Field in Form Field with Related Record in Django

I have 2 models Office & Meeting.
class Office(models.Model):
name = models.CharField(verbose_name=u"name",max_length=255)
class Meeting(models.Model):
meeting_office = models.ForeignKey(Registration,verbose_name=u"Office", on_delete=models.DO_NOTHING, related_name='meeting_office')
date = models.DateField(verbose_name=u"Date", null=False, blank=False)
I have a form that creates the blank meeting successfully
class MeetingForm(ModelForm):
class Meta:
model = Meeting
fields = (
'date',
'meeting_office'
)
widgets = {
'date' :forms.DateInput(attrs={'type': 'date'}),
'meeting_office' :forms.Select(attrs={'class': 'form-control'}),
When I want to have a prefilled form, i have a view that is below
def office_add_meeting(request, office_id):
print("office_id"+ office_id) # produces correct foreign key
office = Office.objects.get(pk=office_id)
form = MeetingForm(request.POST or None)
if form.is_valid():
form.instance.meeting_office = office
form.save()
messages.success(request, "Insert Successfull")
return HttpResponseRedirect('/office_main')
return render(request,
'Office/meeting-form.html',
{"form": form,
"office_id": office_id})
But the form does not prefill the foreign key field. Confirmed the office_id has been passed to the view successfully. Idon't see why the form is not using the defined instance. Any ideas what could be causing this? Or is there a better way?
To set the initial fields on a form, you can use a dictionary matching to the attribute names of the fields.
Try the following:
form = MeetingForm(initial={"meeting_office":your_office})
For editing existing forms, you can also use:
my_meeting = Meeting.objects.all().first() # Example of model
MeetingForm(instance=my_meeting) # Also use instance where you're accepting the form.

Django form - what's the right form/template to create a many to many object inline

I have two models Directors and JobProject. A JobProject can have multiple directors through a many-to-many relationship.
Currently, when I create a new JobProject I choose from the directors I have saved who the director will be. However, I am trying to understand how can I code a form/view for creating a JobProject where I can also create a director in-line (in case I don't have the director already saved in the DB).
Ideally, the user flow would be:
1) Start entering a JobProject details.
2) If the director already exists in the DB, pick it.
3) If the director doesn't exist, allow users to enter the details of a new director object in-line
4) Users go ahead and finish entering JobProject details.
5) Users click save and the BE first saves the new director and then saves the new project with the director pointing at the newly created director.
I basically have 1,2,4,5 figured out but I can't understand how to do 3. Any help? This is my code right now.
Model
class Director(models.Model):
...
name_surname = models.CharField(max_length=60)
class JobProject(models.Model):
...
director = models.ManyToManyField(Director, blank=True, )
Form
class DirectorForm(forms.ModelForm):
class Meta:
model = Director
fields = '__all__'
exclude = ('id', 'owner', 'matching_user')
class JobProjectForm(forms.ModelForm):
class Meta:
model = JobProject
fields = '__all__'
exclude = ('id', 'owner')
...
def __init__(self, *args, **kwargs):
...
if 'director' in self.data:
self.fields['director'].queryset = Director.objects.all()
elif self.instance:
self.fields['director'].queryset = self.instance.director
View
def new_jobproject(request):
form = JobProjectForm()
if request.is_ajax():
term = request.GET.get('term')
source = request.GET.get('source')
response_content = []
...
elif source == 'director':
directors_retrieved = Director.objects.filter(Q(name_surname__istartswith=term)
& (Q(owner__isnull=True) | Q(owner=request.user))
)
response_content = list(directors_retrieved.values())
# if this is a POST request we need to process the form data
if request.method == 'POST':
# create a form instance and populate it with data from the request:
form = JobProjectForm(request.POST)
# check whether it's valid:
if form.is_valid():
# process the data in form.cleaned_data as required
jobproject_added = form.save()
jobproject_added.owner = request.user
jobproject_added.save()
# redirect to precedent url:
return redirect('action:dashboard')
# if a GET (or any other method) we'll create a blank form
return render(request, 'action/forms/jobproject_form.html', {'form': form, 'source': 'new'})
Try to use django-taggit: https://django-taggit.readthedocs.io/en/latest/ with django-autocomplete-light: https://django-autocomplete-light.readthedocs.io/en/master/taggit.html. With these, you can add new many-to-many relationships to the form.

Model Formset validation error

I want to use a model formset with a custom form. I am using a different view for the GET and a different one for the POST functions, cause my html page consists of three different model formsets. My model is one
class Image(models.Model):
customer = models.ForeignKey(Customer)
doctor = models.ForeignKey(Doctor)
date = models.DateField()
desc = models.CharField(max_length=30)
type = models.CharField(choices=TITLE,max_length=20)
image = models.ImageField(upload_to='/pictures/', verbose_name='Image')
XRAY=[
('---------','---------'),
('PA Ceph', 'PA Ceph'),
('Lateral Ceph', 'Lateral Ceph'),
('Panoramic', 'Panoramic'),
]
class XrayImageForm(ModelForm):
desc = forms.ChoiceField(choices=XRAY,required=True, widget=Select(attrs={"class":"form-control input-sm"}))
class Meta:
model = Image
exclude = ('customer', 'date','type', 'doctor',)
widgets = {
'desc':Select(attrs={'class':'form-control input-sm'}),
'date': TextInput(attrs={'class':'form-control input-sm datepicker input-append date',
'readonly':''}),
}
def save(self, commit=True):
model = super(XrayImageForm, self).save(commit=False)
model.desc = self.cleaned_data['desc'];
if commit:
model.save()
return model
class InternalImageForm(ModelForm):
desc = form.ChoiceField(....) # I have to do this cause different ModelForm has different choices in the desc field
class Meta:
model = Image
exclude = ('customer',)
My get view is the following
def images(request, customer_id):
images = Image.objects.all().order_by('date')
pictures = {}
for image in images:
date = image.date.stformat("%d/%M/%Y")
if not pictures.has_key(date):
pictures[date] = {image.title:[image,]}
else:
if pictures[date].has_key(image.title):
pictures[date][image.title].append(image)
else:
pictures[date] = {image.title:[image,]}
xray_formset = modelformset_factory(Image, form=XrayImageForm,extra=4)
xray_formset(queryset=Image.objects.none())
internal_form = InternalImageForm()
external_form = ExternalImageForm()
args = dict(pictures=pictures, xray_formset=xray_formset, internal_form=internal_form, external_form=external_form, customer_id=customer_id)
return render_to_response('customer/images.html', args, context_instance=RequestContext(request))
I want to have them filtered by date and each date by a title (different Images could have the same title, and same date)
my post view
def upload_xray(request, customer_id):
customer = Customer.objects.get(pk=customer_id)
if request.method == 'POST':
XrayFormSet = modelformset_factory(Image, form=XrayImageForm, extra=4)
xray_formset = XrayFormSet(request.POST, request.FILES)
print xray_formset
return redirect('customer-images', customer_id=customer_id)
But when I post the data i get a
ValidationError
Exception Value:[u'ManagementForm data is missing or has been tampered with']
I don't do any actual saving, just wanted to see if it works. Also All fields are required but i don't fill all fields in the formset on my page (Suppose the user can upload 4 pictures but he might not want to. ). Hope I am making a bit of sense...Why is that error?
Make sure you are including the management form in your template.
{{ xray_formset.management_form }}
If that doesn't work, then update your question to include your template.

Populating a Django formset using a left join

I have the following models: Topic, UserProfile, UserSubscribedToTopic
The last of these looks like this:
class UserSubscribedToTopic(models.Model):
topic = models.ForeignKey(Topic)
user_profile = models.ForeignKey(UserProfile)
start_date = models.DateField(null=True, blank=True)
I want to show a list of topics to the user, with a checkbox by each. If the user checks a checkbox then I'll use JavaScript to show the 'start date' text field (so for the purposes of this question I just need to show a text field next to the checkbox). If the user has already saved their selection and is revisiting the page I want to populate the form accordingly when it is first rendered.
I've attempted to do this using formsets:
class SubscribeToTopicForm(ModelForm):
class Meta:
model = UserSubscribedToTopic
fields = ('topic','start_date')
widgets = {'topic': CheckboxInput(attrs={'class': 'topic-checkbox'}),
'start_date': TextInput(attrs={'class': 'date','placeholder': 'Start date'})}
SubscribeToTopicFormSetBase = modelformset_factory(
UserSubscribedToTopic,
form=SubscribeToTopicForm,
extra = 0)
class SubscribeToTopicFormSet(SubscribeToTopicFormSetBase):
def add_fields(self, form, index):
super(SubscribeToTopicFormSet, self).add_fields(form, index)
I almost get what I want if I add the following to my view:
topics_formset = SubscribeToTopicFormSet(queryset=UserSubscribedToTopic.objects.filter(user_profile=user.get_profile()))
However, obviously this will only show the topics to which the user has already subscribed. To show all the topics I really need to do is a LEFT JOIN on the Topic table. I can't see how to do this in Django without resorting to raw.
My questions:
Am I right in thinking it is not possible to specify a queryset for
the formset that is generated from a left join?
Would it be better to
give up on ModelForm and use a formset that I populate manually?
Any better approaches?!
You should create the form on the Topic model, then use the user_set manager to see if the current user has subscribed to the topic.
Once the form is submitted, if any fields are checked, you can then create individual UserSubscribedToTopic objects in your view.
I ended up separating out the checkboxes from the date fields, so I could use a forms.ModelMultipleChoiceField in a Form and a manually-created formset to deal with the date fields.
Here's the code:
Forms:
class SubscribedToTopicForm(ModelForm):
subscribed_to_topic = forms.ModelMultipleChoiceField(required=False,queryset=Topic.available_topics, widget=forms.CheckboxSelectMultiple(attrs={'class': 'topic-checkbox'}))
class Meta:
model = UserProfile
fields = ("subscribed_to_topic",)
def get_checked_topics(self):
return self.cleaned_data['subscribed_to_topic']
class StartDateForm(forms.Form):
topic_id = forms.CharField(widget=forms.HiddenInput,required=False)
start_date = forms.DateField(required=False,label='')
StartDateFormSetBase = formset_factory(form=StartDateForm,extra = 0)
class StartDateFormSet(StartDateFormSetBase):
def get_start_date(self, topic_id):
for i in range(0, self.total_form_count()):
form = self.forms[i]
form_topic_id=long(form.cleaned_data['topic_id'])
if topic_id == form_topic_id:
return form.cleaned_data['start_date']
return ''
View:
GET:
topics_form = SubscribedToTopicForm()
subscribed_to_topic=None
if request.user.is_authenticated():
subscribed_to_topics = SubscribedToTopic.objects.filter(user_profile=request.user.get_profile())
initial_data = []
for topic in Topic.available_topics.all():
start_date=''
if subscribed_to_topics:
for subscribed_to_topic in subscribed_to_topics:
if subscribed_to_topic.topic.id==topic.id:
start_date=subscribed_to_topic.start_date
initial_data.append({'topic_id':topic.id, 'start_date': start_date})
start_date_formset = StartDateFormSet(initial=initial_data)
POST:
start_date_formset = StartDateFormSet(request.POST)
topics_form = SubscribedToTopicForm(request.POST)
start_date_formset.is_valid()
topics_form.is_valid()
for topic in topics_form.get_checked_topics():
start_date = start_date_formset.get_start_date(topic.id)
subscribed_to_topic = SubscribedToTopic()
subscribed_to_topic.topic=topic
subscribed_to_topic.start_date=start_date
subscribed_to_topic.save()

django forms give: Select a valid choice. That choice is not one of the available choices

I am unable to catch the values from the unit_id after the selection is done by the user and data is posted. Can someone help me to solve this.
The values of the unit_id drop down list is obtained from another database table (LiveDataFeed). And once a value is selected and form posted, it gives the error:
Select a valid choice. That choice is not one of the available choices.
Here is the implementation:
in models.py:
class CommandData(models.Model):
unit_id = models.CharField(max_length=50)
command = models.CharField(max_length=50)
communication_via = models.CharField(max_length=50)
datetime = models.DateTimeField()
status = models.CharField(max_length=50, choices=COMMAND_STATUS)
In views.py:
class CommandSubmitForm(ModelForm):
iquery = LiveDataFeed.objects.values_list('unit_id', flat=True).distinct()
unit_id = forms.ModelChoiceField(queryset=iquery, empty_label='None',
required=False, widget=forms.Select())
class Meta:
model = CommandData
fields = ('unit_id', 'command', 'communication_via')
def CommandSubmit(request):
if request.method == 'POST':
form = CommandSubmitForm(request.POST)
if form.is_valid():
form.save()
return HttpResponsRedirect('/')
else:
form = CommandSubmitForm()
return render_to_response('command_send.html', {'form': form},
context_instance=RequestContext(request))
You're getting a flat value_list back which will just be a list of the ids, but when you do that, you're probably better off using a plain ChoiceField instead of a ModelChoiceField and providing it with a list of tuples, not just ids. For example:
class CommandSubmitForm(ModelForm):
iquery = LiveDataFeed.objects.values_list('unit_id', flat=True).distinct()
iquery_choices = [('', 'None')] + [(id, id) for id in iquery]
unit_id = forms.ChoiceField(iquery_choices,
required=False, widget=forms.Select())
You could also leave it as a ModelChoiceField, and use LiveDataFeed.objects.all() as the queryset, but in order to display the id in the box as well as have it populate for the option values, you'd have to subclass ModelChoiceField to override the label_from_instance method. You can see an example in the docs here.
Before you call form.is_valid(), do the following:
unit_id = request.POST.get('unit_id')
form.fields['unit_id'].choices = [(unit_id, unit_id)]
Now you can call form.is_valid() and your form will validate correctly.