Populating a Django formset using a left join - django

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()

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: Create inline forms similar to django admin

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/

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.

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.

field choices() as queryset?

I need to make a form, which have 1 select and 1 text input. Select must be taken from database.
model looks like this:
class Province(models.Model):
name = models.CharField(max_length=30)
slug = models.SlugField(max_length=30)
def __unicode__(self):
return self.name
It's rows to this are added only by admin, but all users can see it in forms.
I want to make a ModelForm from that. I made something like this:
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=CHOICES),
}
but it doesn't work. The select tag is not displayed in html. What did I wrong?
UPDATE:
This solution works as I wanto it to work:
class ProvinceForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ProvinceForm, self).__init__(*args, **kwargs)
user_provinces = UserProvince.objects.select_related().filter(user__exact=self.instance.id).values_list('province')
self.fields['name'].queryset = Province.objects.exclude(id__in=user_provinces).only('id', 'name')
name = forms.ModelChoiceField(queryset=None, empty_label=None)
class Meta:
model = Province
fields = ('name',)
Read Maersu's answer for the method that just "works".
If you want to customize, know that choices takes a list of tuples, ie (('val','display_val'), (...), ...)
Choices doc:
An iterable (e.g., a list or tuple) of
2-tuples to use as choices for this
field.
from django.forms.widgets import Select
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=( (x.id, x.name) for x in CHOICES )),
}
ModelForm covers all your needs (Also check the Conversion List)
Model:
class UserProvince(models.Model):
user = models.ForeignKey(User)
province = models.ForeignKey(Province)
Form:
class ProvinceForm(ModelForm):
class Meta:
model = UserProvince
fields = ('province',)
View:
if request.POST:
form = ProvinceForm(request.POST)
if form.is_valid():
obj = form.save(commit=True)
obj.user = request.user
obj.save()
else:
form = ProvinceForm()
If you need to use a query for your choices then you'll need to overwrite the __init__ method of your form.
Your first guess would probably be to save it as a variable before your list of fields but you shouldn't do that since you want your queries to be updated every time the form is accessed. You see, once you run the server the choices are generated and won't change until your next server restart. This means your query will be executed only once and forever hold your peace.
# Don't do this
class MyForm(forms.Form):
# Making the query
MYQUERY = User.objects.values_list('id', 'last_name')
myfield = forms.ChoiceField(choices=(*MYQUERY,))
class Meta:
fields = ('myfield',)
The solution here is to make use of the __init__ method which is called on every form load. This way the result of your query will always be updated.
# Do this instead
class MyForm(forms.Form):
class Meta:
fields = ('myfield',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make the query here
MYQUERY = User.objects.values_list('id', 'last_name')
self.fields['myfield'] = forms.ChoiceField(choices=(*MYQUERY,))
Querying your database can be heavy if you have a lot of users so in the future I suggest some caching might be useful.
the two solutions given by maersu and Yuji 'Tomita' Tomita perfectly works, but there are cases when one cannot use ModelForm (django3 link), ie the form needs sources from several models / is a subclass of a ModelForm class and one want to add an extra field with choices from another model, etc.
ChoiceField is to my point of view a more generic way to answer the need.
The example below provides two choice fields from two models and a blank choice for each :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=([['','-'*10]]+[[x.id, x.__str__()] for x in Speakers.objects.all()]))
event = forms.ChoiceField(choices=( [['','-'*10]]+[[x.id, x.__str__()] for x in Events.objects.all()]))
If one does not need a blank field, or one does not need to use a function for the choice label but the model fields or a property it can be a bit more elegant, as eugene suggested :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=((x.id, x.__str__()) for x in Speakers.objects.all()))
event = forms.ChoiceField(choices=(Events.objects.values_list('id', 'name')))
using values_list() and a blank field :
event = forms.ChoiceField(choices=([['','-------------']] + list(Events.objects.values_list('id', 'name'))))
as a subclass of a ModelForm, using the one of the robos85 question :
class MixedForm(ProvinceForm):
speaker = ...