Use InlineForm for ForeignKey - django

With a ForeignKey relationship, is it possible display the form of the object being pointed to inline? The documentation shows how to do the reverse, with inline formsets; but I was unable to find how to replace the default selection box with an inline form.
For example, consider the case:
class Address(models.Model):
line_1 = models.CharField(max_length=100)
line_2 = models.CharField(max_length=100)
town = models.CharField(max_length=50)
state = models.ForeignKey(State,
on_delete=models.PROTECT)
post_code = models.CharField(max_length=4)
class Person(models.Model):
# ...
residential_address = models.ForeignKey(Address, unique=True)
postal_address = models.ForeignKey(Address, unique=True)
By default, the form for Person will display two drop-down selections: one for the residential address and one for the postal address. Could I display the form instead? Perhaps through a widget and/or custom field?
So, since there has been no answer yet, I've been working on making my own field and widgets. The display aspect (mostly) works fine now, but I get errors when I try and save the object as the referenced object needs to be created beforehand; however, I'm not sure if I can (or should?) save the object or not.
Here's what I have:
model.py
class Address(models.Model):
# as above
class AddressField(models.OneToOneField):
description = "An address"
def __init__(self, **kwargs):
"""The foreign key should always be to an Address class."""
kwargs['to'] = 'address.Address'
kwargs['related_name'] = '+'
super().__init__(**kwargs)
def formfield(self, **kwargs):
"""See https://docs.djangoproject.com/en/1.10/ref/models/fields/#django.db.models.Field.formfield.
Change the default form field.
"""
from address.forms import AddressField as AddressFormField
defaults = dict(form_class=AddressFormField)
defaults.update(kwargs)
return super(AddressField, self).formfield(**defaults)
forms.py
class AddressField(fields.MultiValueField):
"""Address (multi-value) field."""
def __init__(self, *args, **kwargs):
error_messages = {
'incomplete': _("Please ensure all required fields are completed.")
}
sub_fields = (
# Unique key
models.ModelChoiceField(queryset=kwargs.pop('queryset'),
limit_choices_to=kwargs.pop('limit_choices_to'),
to_field_name=kwargs.pop('to_field_name'),
required=False,
error_messages={
'invalid': _("Invalid address.")
}),
# Line 1
fields.CharField(max_length=256,
error_messages={
'incomplete': _("Enter a first address line.")
}),
# Line 2
fields.CharField(max_length=256,
required=False),
# Town
fields.CharField(max_length=100,
error_messages={
'incomplete': _("Enter a town name.")
}),
# State
models.ModelChoiceField(queryset=State._default_manager.get_queryset(),
error_messages={
'incomplete': _("Select a state."),
'invalid': _("Invalid state.")
}),
# Post code
fields.CharField(max_length=4,
validators=[
RegexValidator(
regex=r"^[0-9]{4}$",
message=_("Invalid post code."),
code='invalid-postcode'
)
],
error_messages={
'incomplete': _("Enter a post code.")
}),
)
super().__init__(
error_messages=error_messages,
fields=sub_fields,
require_all_fields=False,
widget=AddressWidget,
*args, **kwargs
)
# TODO: Need to set the State choices
def compress(self, values):
"""See https://docs.djangoproject.com/en/1.10/ref/forms/fields/#django.forms.MultiValueField.compress.
Converts a list of values into an Address instance, unless no values
were given in which case None is returned.
"""
if not values:
return None
# If we have been given a PK, then fetch the object and updates its values
if values[0]:
try:
address = Address.objects.get(pk=values[0])
address.line_1 = values[1]
address.line_2 = values[2]
address.town = values[3]
address.state = values[4]
address.post_code = values[5]
except Address.DoesNotExist:
# TODO Handle this properly (if it every comes up)
raise Exception("Tried to get the address with key '{}', but could not find it.")
# Otherwise, instantiate a new Address
else:
address = Address(
line_1=values[1],
line_2=values[2],
town=values[3],
state=values[4],
post_code=values[5]
)
return address
class AddressWidget(widgets.MultiWidget):
"""Address widget."""
# TODO: Need ot handle the setting of choices (or not?)
choices = None
def __init__(self, *args, **kwargs):
sub_widgets = (
widgets.HiddenInput,
widgets.TextInput(attrs={
'title': _("Line 1"),
'size': 50,
'required': True,
'class': "address-line address-line1",
}),
widgets.TextInput(attrs={
'title': _("Line 2"),
'size': 50,
'required': False,
'class': "address-line address-line2",
}),
widgets.TextInput(attrs={
'title': _("Town"),
'size': 30,
'required': True,
'style': "flex-grow: 4;",
'class': "address-town",
}),
widgets.Select(attrs={
'title': _("State"),
'size': 3,
'required': True,
'style': "flex-grow: 1;",
'class': "address-state",
}),
widgets.TextInput(attrs={
'title': _("Post Code"),
'size': 4,
'required': True,
'style': "flex-grow: 1;",
'class': "address-postcode",
}),
)
super().__init__(
widgets=sub_widgets,
*args, **kwargs
)
self.widgets[4].choices=[[1, 2]]
print("widgets: {}".format(self.widgets))
def decompress(self, value):
"""See https://docs.djangoproject.com/en/1.10/ref/forms/widgets/#django.forms.MultiWidget.decompress.
Converts an Address object into a list of values; that is, performs the
converse of `compress` above.
"""
if not value:
return [None] * 6
if isinstance(value, Address):
return [
value.pk,
value.line_1,
value.line_2,
value.town,
value.state,
value.post_code
]
raise Exception("Unable to decompress the given value.")
def format_output(self, rendered_widgets):
"""See https://docs.djangoproject.com/en/1.10/ref/forms/widgets/#django.forms.MultiWidget.format_output.
Ensure that line 1 and 2 are on their own line, and place the town,
state and post code on the third line.
"""
print("choices: {}".format(self.choices))
return """
<div class="address-widget" style="display: flex; flex-direction: column;">
{pk}
{line_1}
{line_2}
<div class="address-locality" style="display: flex; flex-direction: row; flex-wrap: wrap">
{town}
{state}
{post_code}
</div>
</div>
""".format(
pk=rendered_widgets[0],
line_1=rendered_widgets[1],
line_2=rendered_widgets[2],
town=rendered_widgets[3],
state=rendered_widgets[4],
post_code=rendered_widgets[5],
)

Related

Django form is saying field is required despite providing value

I have a form with a required field customer_phone_number and despite passing a phone number in with the data the form isn't valid..
Submitting the form through a view works but not in the shell.
class OrderForm(forms.ModelForm):
class Meta:
model = models.Order
fields = (
'date',
'customer_phone_number',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['customer_phone_number'].required = True
self.fields['customer_phone_number'].widget = PhoneNumberPrefixWidget()
data = {
'date': '2020-04-20',
'customer_phone_number': '+17168567800',
}
form = OrderForm(data=data)
form.is_valid()
print(form.errors) # {'customer_phone_number': ['This field is required.']}
The only thing I can think of is that I'm using a custom widget but I have no idea how to correct this error.
from phonenumber_field.phonenumber import PhoneNumber
from django.forms import Select, TextInput
from django.forms.widgets import MultiWidget
class PhonePrefixSelect(Select):
initial = '+1'
def __init__(self, attrs=None):
choices = [('+1', '+1')]
super().__init__(attrs, choices=sorted(choices, key=lambda item: item[1]))
def render(self, name, value, *args, **kwargs):
return super().render(
name, value or self.initial, *args, **kwargs)
class PhoneNumberPrefixWidget(MultiWidget):
"""
A Widget that splits phone number input into:
- a country select box for phone prefix
- an input for local phone number
"""
def __init__(self, attrs=None):
widgets = (
PhonePrefixSelect(attrs={
'class': 'form-control w-25 mr-2',
'tabindex': '-1'
}),
TextInput(attrs={
'class': 'form-control w-75'
}),
)
super().__init__(widgets, attrs)
def decompress(self, value):
if value:
if type(value) == PhoneNumber:
if value.country_code and value.national_number:
return ["+%d" % value.country_code, value.national_number]
else:
return value.split('.')
return [None, ""]
def value_from_datadict(self, data, files, name):
values = super().value_from_datadict(
data, files, name)
if all(values):
return '%s.%s' % tuple(values)
return ''
Solved by splitting customer_phone_number into two fields: customer_phone_number_0 and customer_phone_number_1

Django formset is_valid fails Cannot assign "u'username'": "contact" must be a "User" instance

I have a django inline formset that populates a bunch of forms. One of the field is a contact entry which would a be user name. User can select the user name and data is sent in Post call.
Problem is the formset.is_valid call fails because of the error.
I specifically converted the contact value in request object to specified user object but Django somehow later again converts it a unicode string and i get the error.
I could see that some internal to_python() call seems to convert the user object value to unicode.
Not sure whats happening here.
Model:
class ATest(models.Model):
contact = models.ForeignKey('auth.User', related_name='test_contact', null=True, blank=True)
View (Post Call):
request.POST['atest-0-contact'] = GetOrCreateUserMixin.get_or_create_user(request.POST['atest-0-contact'])
ctx['formset_test'] \
= formset_test \
= TestAFormSet(
request.POST,
instance = test
)
is_valid = formset_test.is_valid()
Form:
class ATestForm(forms.ModelForm, GetOrCreateUserMixin):
contact = forms.CharField(
label='Contact',
widget=forms.TextInput(attrs={
'autocomplete': 'off',
'class': 'js-widget-user-typeahead',
'style': 'width: 95%'
}),
# help_text='Begin typing user name',
required=False
)
class Meta:
model = models.ATest
fields = (
'contact',
)
def __init__(self, *args, **kwargs):
self.conflict = {}
self.user = kwargs.pop('user')
super(ATestForm, self).__init__(*args, **kwargs)
self.original = {}
self.fk_display_fields = (
# contact,
)
if contact in self.fields:
self.fields['contact'].help_text = ''
self.fields['contact'].label = ''
def clean_contact(self):
if not self.cleaned_data['contact']:
return None
return self.get_or_create_user(self.cleaned_data['contact'])
TestAFormSet = inlineformset_factory(models.Test, models.ATest, form=ATestForm, formset=BaseAutomationFormSet, extra=0, help_texts=None, labels=None, can_delete=True)

Django get_or_create only if form constraints are met

I have a form that asks for a song's Artist, Title and Mix. Artist and Title are required fields but Mix is not. The form should only save if Artist, Title and Mix does not exists. If the form has either empty Artist or Title field it should show "This field is required" on submit. The issue I'm having is if the Title field is empty but Artist is populated, it'll still create the Artist object with get_or_create (See ###forms.py below). How do I only create Artist object if the form is valid?
###########models.py
class Artist (models.Model):
name = models.CharField(max_length=100)
class Track (models.Model):
artist = models.ForeignKey(Artist, blank=True, null=True, on_delete=models.SET_NULL, verbose_name="Artist")
user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, verbose_name="Submitted by", default=1)
title = models.CharField(max_length=100, verbose_name="Title")
mix = models.CharField(max_length=100, blank=True, verbose_name="Mix")
###########views.py
class TrackCreateView(SuccessMessageMixin, AjaxCreateView):
form_class = ProfileForm
success_message = "Thank you for submitting track: %(artist)s - %(title)s - %(mix)s"
def get_initial(self):
self.initial.update({ 'user': self.request.user })
return self.initial
def get_success_message(self, cleaned_data):
return self.success_message % dict(cleaned_data,
artist=self.object.artist,
title=self.object.title,
)
###########forms.py
class ProfileForm(forms.ModelForm):
class Meta:
model = Track
fields = [
"artist",
"title",
"mix",
]
artist = forms.CharField(widget=forms.TextInput(attrs={'maxlength': '100',}))
def __init__(self, *args, **kwargs):
self.user = kwargs['initial']['user']
super(ProfileForm, self).__init__(*args, **kwargs)
# Set layout for fields.
my_field_text= [
('artist', 'Artist', ''),
('title', 'Title', ''),
('mix', 'Mix', ''),
]
for x in my_field_text:
self.fields[x[0]].label=x[1]
self.fields[x[0]].help_text=x[2]
self.helper = FormHelper()
self.helper.layout = Layout(
Div(
Div('artist', css_class="col-sm-4"),
Div('title', css_class="col-sm-4"),
Div('mix', css_class="col-sm-4"),
css_class = 'row'
),
)
def save(self, commit=True):
obj = super(ProfileForm, self).save(False)
obj.user = self.user
commit and obj.save()
return obj
def clean(self):
cleaned_data = super(ProfileForm, self).clean()
artist = self.cleaned_data.get('artist')
title = self.cleaned_data.get('title')
mix = self.cleaned_data.get('mix')
if artist and title:
title = ' '.join([w.title() if w.islower() else w for w in title.split()])
if mix:
mix = ' '.join([w.title() if w.islower() else w for w in mix.split()])
if Track.objects.filter(artist=artist, title=title, mix=mix).exists():
msg = "Record with Artist and Title already exists."
if mix:
msg = "Record with Artist, Title & Mix already exists."
self.add_error('mix', msg)
self.add_error('artist', msg)
self.add_error('title', msg)
if not artist:
raise forms.ValidationError("Artist is a required field.")
else:
artist, created = Artist.objects.get_or_create(name=artist)
self.cleaned_data['artist'] = artist
self.cleaned_data['title'] = title
self.cleaned_data['mix'] = mix
return self.cleaned_data
How about changing your comparison, by first checking if your form is valid in clean()?
def clean(self):
...
if not artist:
raise ValidationError("artist is a required field")
if not title:
raise ValidationError("title is a required field")
...
The above makes it a two-step process for the user, since if a user leaves both artist and title blank, they ony get the artist notice.
You could make a better (sub) if statement and a combined ValidationError, or solve that by using clean_artist and clean_title, just for raising the ValidationError (not using get_or_create in the field clean methods):
def clean_artist(self):
# no get_or_create here
...
if not artist:
raise ValidationError("artist is a required field")
def clean_title(self):
# no get_or_create here
...
if not title:
raise ValidationError("title is a required field")
def clean(self):
...
if title and artist:
# get_or_create stuff here
...
This way, you should get both errors independently, but the get_or_create is still done in the main clean, only if title and artist are valid.

How to save Foreign Key text input in Django model form

My view passes an id to my form. This id is a foreign key from another table. I am not able to save the id in the database table.
(id : voucher_id, table in which i am saving the form : TmpPlInvoicedet)
What i want to do
Send voucher_id from (View) to ---> TmpFormDetForm (Form) ---> TmpPlInvoicedet (DB)
Trying to get instance from the table 'TmpPlInvoice' (which has voucher_id as PK) and save it in the form gives me
DoesNotExist at /new/ TmpPlInvoice matching query does not exist
What am i doing wrong?
Views.py
def new_invoic(request):
# Create a voucher id according to my criteria
temp_vid = TmpPlInvoice.objects.order_by().values_list("voucher_id", flat=True).distinct()
if not temp_vid:
voucher_id = str(1).zfill(4)
else:
voucher_id = str(int(max(temp_vid)) + 1).zfill(4)
# POST METHOD TRying to show the voucher_id in the form in readonly format
if request.method == 'POST':
form_pk = TmpForm(request.POST or None, voucher_id=voucher_id,initial={'voucher_id': voucher_id})
if form.is_valid():
form_pk.save()
form = TmpFormDetForm(request.POST or None, voucher=voucher_id, initial={'voucher': voucher_id})
# My assumption is that since i have save the voucher_id in the TmpInvoice table so i can get the PK voucher_id value and save it in the TmpInvoiceDetForm
form.save()
return HttpResponseRedirect('/new/')
else:
return render_to_response('test.html',{'form': form, 'form_pk': form_pk},context_instance=RequestContext(request))
else:
form_pk = TmpForm(voucher_id=voucher_id,initial={'voucher_id': voucher_id})
form = TmpFormDetForm(voucher=voucher_id, initial={'voucher': voucher_id})
return render_to_response('test.html',{'form': form, 'form_pk': form_pk},context_instance=RequestContext(request))
Forms.py
# This form contains the FK. This one is giving errors while saving.
class TmpFormDetForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
voucher = kwargs.pop('voucher', None)
super(TmpFormDetForm, self).__init__(*args, **kwargs)
self.fields['voucher'].initial = TmpPlInvoice.objects.get(voucher_id=voucher)
voucher = forms.CharField(widget=forms.TextInput(attrs={'size':'40'}))
class Meta:
model = TmpPlInvoicedet
exclude = ['emp_id','particulars','qty', 'rate' , 'itemtot', 'stock_code' ]
widgets = {
'voucher': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '', 'required': 'False', 'name': 'voucher','readonly': 'readonly'}),
'lineitem': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Add Total', 'required': 'False', 'blank': 'True'})}
# This form takes the PK. I save the PK here first.
class TmpForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
voucher_id = kwargs.pop('voucher_id', None)
super(TmpFor, self).__init__(*args, **kwargs)
self.fields['voucher_id'].initial = voucher_id
pos_code = MyModelChoiceField(queryset=Positions.objects.all(), widget=forms.Select(attrs={'class': 'select2_single form-control', 'blank': 'True'}))
cust = MyModelChoiceField(queryset=Custodian.objects.all(), to_field_name='acct_id',widget=forms.Select(attrs={'class': 'select2_single form-control', 'blank': 'True'}))
acct = MyModelChoiceField(queryset=Item.objects.all(), to_field_name='stock_code',widget=forms.Select(attrs={'class':'select2_single form-control', 'blank': 'True'}))
voucher_date = forms.DateField(widget=forms.TextInput(attrs={'tabindex': '-1', 'class': 'form-control has-feedback-left', 'id': 'single_cal1','aria-describedby': 'inputSuccess2Status'}))
class Meta:
model = TmpPlInvoice
exclude = ['net_amt', 'post_date', 'address', 'posted']
widgets = {
'voucher_id': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '', 'required':'False', 'name': 'voucher_id', 'readonly': 'readonly'}),
'voucher_date': forms.TextInput(attrs={'tabindex': '-1', 'class': 'form-control has-feedback-left', 'id': 'single_cal1','aria-describedby': 'inputSuccess2Status'}),
'particulars': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Add Particulars', 'required':'False'}),
}
Models.py
class TmpPlInvoicedet(models.Model):
stock_code = models.CharField(max_length=13, blank=True, null=True)
voucher = models.ForeignKey(TmpPlInvoice, db_column='voucher_id')
lineitem = models.CharField(max_length=6)
particulars = models.CharField(max_length=200, blank=True, null=True)
qty = models.FloatField(blank=True, null=True)
rate = models.FloatField(blank=True, null=True)
itemtot = models.FloatField(blank=True, null=True)
emp_id = models.CharField(max_length=8, blank=True, null=True)
class Meta:
managed = False
db_table = 'tmp_pl_invoicedet'
unique_together = (('voucher', 'lineitem'),)
Easy peesy.
def master_detail(request):
def get_new_voucher_id():
temp_vid = TmpPlInvoice.objects.order_by().values_list("voucher_id", flat=True).distinct()
logger.info('Voucher ID already present %s', temp_vid)
if not temp_vid:
voucher_id = str(1).zfill(4)
else:
voucher_id = str(int(max(temp_vid)) + 1).zfill(4)
return voucher_id
voucher_id = get_new_voucher_id()
author_form = TmpForm(initial={'voucher_id': voucher_id})
author = TmpPlInvoice()
BookFormSet = inlineformset_factory(TmpPlInvoice, TmpPlInvoicedet, exclude=('emp_id', 'itemtot', 'voucher', 'lineitem','id'),
form=TmpFormDetForm, extra=1)
formset = BookFormSet(instance=author)
if request.method == 'POST':
logger.info('*'*50)
author = TmpForm(request.POST, initial={'voucher_id': voucher_id})
if author.is_valid():
logger.info('Data for Author is %s', author.cleaned_data)
created_author = author.save()
formset = BookFormSet(request.POST, instance=created_author)
if formset.is_valid():
logger.info('Data for Book is %s', formset.cleaned_data)
formset.save()
else:
logger.info('Formset errors %s', formset.errors)
else:
logger.info('Master form errors %s', author.errors)
logger.info('*'*50)
return HttpResponseRedirect('/new/')
else:
logger.info('Formset from GET is %s', formset.errors)
return render_to_response('new_invoice.html',
{'form': author_form, 'formset': formset},context_instance=RequestContext(request))
You seem to be creating a new invoice ID and then, in your form, attempting to get the invoice matching that ID. But that invoice doesn't exist yet, of course, because you haven't created it.
You might want to use get_or_create to ensure that the invoice is created if it doesn't exist.

How to set default values in TabularInline formset in Django admin

How to set first default rows/values in django admin's inline?
class Employee(models.Model):
username = models.CharField(_('Username'), max_length=150, null=False, blank=False)
email = models.CharField(_('Email'), max_length=150, null=False, blank=False)
class Details(models.Model):
employee = models.ForeignKey(Employee, verbose_name=_('Employee'), blank=False, null=False)
label = models.CharField(_('Label'), max_length=150, null=False, blank=False)
value = models.CharField(_('Value'), max_length=150, null=False, blank=False)
class DetailsFormset(forms.models.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
self.initial = [
{ 'label': 'first name'},
{'label': 'last name'},
{'label': 'job',}]
super(DetailsFormset, self).__init__(*args, **kwargs)
class DetailsInline(admin.TabularInline):
model = Details
formset = DetailsFormset
fieldsets = [
['', {'fields': ['employee', 'label', 'value']}]
]
class EmployeeAdmin(admin.ModelAdmin):
inlines = [DetailsInline]
but this row doesn't work
self.initial = [
{ 'label': 'first name'},
{'label': 'last name'},
{'label': 'job',}]
How do I set default values using django admin?
from django.utils.functional import curry
class DetailsInline(admin.TabularInline):
model = Details
formset = DetailsFormset
extra = 3
def get_formset(self, request, obj=None, **kwargs):
initial = []
if request.method == "GET":
initial.append({
'label': 'first name',
})
formset = super(DetailsInline, self).get_formset(request, obj, **kwargs)
formset.__init__ = curry(formset.__init__, initial=initial)
return formset
From here: Pre-populate an inline FormSet?
If what you need is to define default values for the new forms that are created you can redefine the empty_form property of a InlineFormSet:
class MyDefaultFormSet(django.forms.models.BaseInlineFormSet):
#property
def empty_form(self):
form = super(MyDefaultFormSet, self).empty_form
# you can access self.instance to get the model parent object
form.fields['label'].initial = 'first name'
# ...
return form
class DetailsInline(admin.TabularInline):
formset = MyDefaultFormSet
Now, every time you add a new form it contains the initial data you provided it with. I've tested this on django 1.5.
I tried many suggestions from Stackoverflow (Django=4.x), not working for me.
Here is what I did.
class MilestoneFormSet(forms.models.BaseInlineFormSet):
model = Milestone
def __init__(self, *args, **kwargs):
super(MilestoneFormSet, self).__init__(*args, **kwargs)
if not self.instance.pk:
self.initial = [
{'stage': '1.Plan', 'description': 'Requirements gathering', },
{'stage': '2.Define', 'description': 'Validate requirement', },
]
class MilestoneInline(admin.TabularInline):
model = Milestone
formset = MilestoneFormSet
def get_extra(self, request, obj=None, **kwargs):
extra = 0 #default 0
if not obj: #new create only
extra = 2 #2 records defined in __init__
return extra
I hope this works for everyone.
To provide a static default for all instances in the inline, I found a simpler solution that just sets it in a form:
class DetailsForm(django_forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['label'] = 'first_name'
class DetailsInline(admin.TabularInline):
form = DetailsForm
# ...
I think this doesn't work for the OP's particular case because each form has a different value for the 'label' field, but I hope it can be useful for anyone coming to this page in the future.