Search code examples
djangoformsforeign-keysclass-attributes

Django Form: Calling foreign key field attributes in template


In a template I want to call a class attributes of a foreign-key form field item.pid like shown below

{%for item in form.item %}
{{item.pid}}
{% endfor %}

My question is simple and the solution should be as well but I am stuck here for way too long now. I have tried formsets, inlineformsets, clean and so many more things which are in the documentation and here on SO. It's even not possible to call a class method of a form field.

calling {{form.instance.item.pid}} should be the solution but I can't get it running.

model.py

class Event(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE, blank=True, null=True, default=0)
    item = models.ForeignKey(Item, on_delete=models.CASCADE, blank=True, null=True, default=0)
#many more

class Person(models.Model):
    person_number = models.IntegerField(null=True)
    name = models.CharField(max_length=200, null=True)
    surname = models.CharField(max_length=200, null=True)
#many more

class Item(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE, blank=True, null=True, default=0)
    pid = models.IntegerField(null=True) 
#many more

    def save(self, *args, **kwargs):  
        self.pid = Person.objects.filter(surname=self.person.surname, name=self.person.name).values("person_number")
        super(Item, self).save(*args, **kwargs) 

views.py

def event(request, event_id=None):

    if event_id:
        instance = get_object_or_404(Event, pk=event_id)
    else:
        instance = Event()
   
    form = EventForm(request.POST or None, instance=instance)    

    if request.POST and form.is_valid():
        form.save()
        return HttpResponseRedirect(reverse('cal:calendar'))
    context = {
        'form': form,
    }    
    return render(request, 'cal/event.html', context)

forms.py

class EventForm(ModelForm):
  class Meta:
    model = Event

    fields = ('person', 'item', 'manymore',)

Solution

  • To customise the choices of your FK fields is fairly simple.

    I've done this just a few hours back and I do it using a cached method so that the database isn't hit to generate the list each time.

    So I have a Currency table and I wanted to allow an applicant choose a currency related to their application;

    class ApplicationForm(forms.ModelForm):
        """ Application form """
    
        currency = forms.ChoiceField(
            choices=[]
        )
    
        class Meta:
            """ Setup the form """
            fields = (
                ...
                'currency',
                ...
            )
            model = Application
    
        
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
            self.fields['currency'].choices = get_currency_choices()
    
        def clean(self):
            """
            Custom form cleaning
            """
            cleaned_data = super().clean()
    
            currency_id = cleaned_data.get('currency')
            cleaned_data['currency'] = Currency.objects.get(id=currency_id)
    
    

    So as you can see in this form, the currency field is getting it's choices from get_currency_choices. For a ForeignKey you also need to have an instance of the related model to save that relationship, so in the clean I get the instance related to the choice by the ID which is the value of the field in the form.

    The choices are generated from the related model and stored in the cache with the following method;

    
    def get_currency_choices():
        """ Get currency choices, from cache if possible """
        currency_choices = cache.get(CACHED_CURRENCY_KEY)
        if not currency_choices:
            currencies = Currency.objects.values('id', 'symbol', 'code')
            currency_choices = list()
            currency_choices.append((None, ''))  # I use this so that there isn't a default choice & people can't make the wrong choice by leaving the default value.
            for currency in currencies:
                currency_choices.append(
                    (currency['id'], f"{currency['symbol']} ({currency['code']})")
                )
            cache.set(
                CACHED_CURRENCY_KEY, currency_choices, CACHED_CURRENCY_LENGTH
            )
        return currency_choices