ATM 'Big Bank' web app - setting up dynamic `pk` for correct routing with views.py and in url.py

I am pretending to work for a bank. I am writing a Django web app which processes bank account applications from web visitors. It accepts the new client’s first name, last name, and year of their birth. I’m using Django’s Forms API, Form fields, and Model Form Functions. When they submit their application input, Django is supposed to redirect to an ‘Application is Approved’ page displaying the visitor’s personal details that they just entered, including a 4 digit debit card number along with an automatically generated and random routing number and account number, among other details.

It partially works.

The major issue I am tackling now is that views.py refers to the pk (primary key) of a specific entry. It’s hard coded. It’s not dynamic. For example, there are 6 people (entries) stored in my db. When the approved.html template is served, the entry rendered is always the same person (entry) designated in as primary key 3. I would rather have my app pull the entry dynamically, so that Django serves the ‘approved’ page based on the application the web vistor just submitted.

Here is my views.py:

def new_application(request):
    # POST REQUEST -- > CONTENTS --> APPROVED
    if request.method == "POST":
        form = BankAccountApplicationForm(request.POST)
        
        # VALIDATE ROUTINE::
        if form.is_valid():
            print(form.cleaned_data)
            form.save()
            return redirect(reverse('account_application_form:approved'))             
    
    # ELSE, Re-RENDER FORM    
    else:
        form = BankAccountApplicationForm
    return render(request, 'new_application/welcome.html', context={'form':form})

def approved(request):
    approved_user = models.User.objects.get(pk=3)
    return render(request, 'new_application/approved.html', context= {'approved_user':approved_user})

The second last line at the bottom of that views.py indicates pk=3 which is why Django keeps serving the 3rd entry. To resolve this issue, I tried adding pk as arguments to both view functions. No dice. For the validation routine for the redirect function reversal, I also tried combinations of these:

return redirect(reverse('account_application_form:approved/'+str(models.User.objects.filter(id=pk).primary_key)))

Above I tried swapping the order: from id=pk to pk=id. Didn’t work.

Next I tried:

return redirect(reverse('account_application_form:approved/'+str(object.pk)))

And:

return redirect(reverse('account_application_form:approved/'+str(models.User.objects.filter('application_approval_date')).pk))

This too:

return redirect(reverse('account_application_form:approved/'+str(models.User.objects.get(pk=pk))))

Every one of the above produced different tracebacks. I am stabbing in the dark at this point.

The original application form works. It accepts user input and stores it. But how do I have Django process a web visitor’s input and then redirect them to the next approved application page?

Here is my urls.py:

from django.http.response import HttpResponse
from django.urls import path, include
from . import views

app_name = 'account_application_form'
urlpatterns = [
   path('hello/', views.new_application, name='new_application'),
   path('approved/', views.approved, name='approved'),   

Above I tried adding <int:pk>/ after 'approved/'. I tried this in combination with the other various changes to views.py mentioned above.

To help me along with all those different possibilities, I’ve been using Google search terms such as:

  • ‘pk primary key django’
  • ‘pass in pk to path urls.py’
  • ‘specifying primary key in django pk’
  • ‘how to refer to primary key django’
  • ‘django redirect with primary key urls.py’
  • ‘get pk django’

In my searches I’ve encountered all kinds of official and unofficial Django documentation along with Stack Overflow questions and answers. But I can’t figure it out. The gnashing of the teeth!

For reference, here are some more elements to my code base. Here is forms.py:

from django import forms
from .models import User
from django.forms import ModelForm


class BankAccountApplicationForm(ModelForm):
    class  Meta:
        model = User
        fields = ['first_name', 'last_name', 'year_of_birth',]

models.py:

from django.db import models
import random
import datetime
from django.utils.timezone import now
 
class User(models.Model):
  
   # pk
  
   # Next : Dynamic input from prospective clients :
   first_name = models.CharField(max_length=30)
   last_name = models.CharField(max_length=30)
   year_of_birth = models.CharField(max_length=4,default='1900')
  
   # Unique data generated for each new client after approval :
   application_approval_date = models.DateField(auto_now_add=False, default=now)
   unique_card_num = models.CharField(max_length=4, default='6054')
   routing_num = models.CharField(max_length=5, default=str(random.randint(10000, 99999)))
   account_num = models.CharField(max_length=7, default=str(random.randint(1000000, 9999999)))
 
   def __str__(self):
       return f'Client {self.first_name} {self.last_name}'

Template: welcome.html:

<body>
   <div class="container">
   <form method="POST">
       {% csrf_token %}
       {{ form.as_p }}
       <input type="submit" value="Submit Application">
   </form>
   </div>
</body>

Template: approved.html:

<h1>Your application was approved. Welcome Aboard!</h1>
<h3>Here are your banking details:</h3>
<ul>
   <li>pk: {{approved_user.pk}}</li>
   <li>First Name: {{approved_user.first_name}}</li>
   <li>Last Name: {{approved_user.last_name}}</li>
   <li>YOB: {{approved_user.year_of_birth}}</li>
   <li>Client Since (Application Approved On): {{approved_user.application_approval_date}}</li>
   <li>Assigned Access Card Number (4 Digits): {{approved_user.unique_card_num}}</li>
   <li>Assigned Routing Number: {{approved_user.routing_num}}</li>
   <li>Assigned Bank Account Number {{approved_user.account_num}}</li>
</ul>

You want to add a url parameter to your account_application_form:approved url, then pass that as a parameter in your reverse call from the data you just saved.

That’s the right start. Now review the docs for reverse to see your options for passing the parameter to the url.

Thank you for sharing the django.urls utility functions | Django documentation | Django
It’s short and to the point. I read it over more than once. But since the official Django docs (in general) are written by programmers, for programmers, in some places it can be hard for beginners like me to follow along and understand completely. Parts of the official Django docs (not all but some parts) are easier to read than others. I find this specific passage describing how to use reverse() in particular to be not descriptive enough. When this happens I turn to Google to find guides on third party sites. I discovered a terrific answer to a Stack Overflow question asking about what the purpose of is reverse() and how to use it in the context of the polling web app: django - What is reverse()? - Stack Overflow

The original question at that SO link is >6 years old, and so are most of the top answers. But there is a relatively new answer by a Stack Overflow user named onlyphantom which explains how to properly reference .id in views.py and in urls.py. I did my best to integrate that explanation into my project.

Here is my latest views.py:

def new_application(request):
   # POST REQUEST -- > CONTENTS --> APPROVED
   if request.method == "POST":
       form = BankAccountApplicationForm(request.POST)
      
       # VALIDATE ROUTINE::
       if form.is_valid():
           print(form.cleaned_data)
           form.save()
           return redirect(reverse('account_application_form:approved', args=[form.id]))
         
   # ELSE, Re-RENDER FORM   
   else:
       form = BankAccountApplicationForm
   return render(request, 'new_application/welcome.html', context={'form':form})

def approved(request, pk):
   approved_user = models.User.objects.get(pk=pk)
   return render(request, 'new_application/approved.html', context= {'approved_user':approved_user})

As you can see above, for the first return operation, where redirect(reverse()) is invoked, I’ve passed in form.id as an argument. Interpolated into form.id is the BankAccountApplicationForm class instantiated a few lines above which was subsequently validated. For the args parameter, form.id is wrapped around square brackets just as explained in the official Django doc linked to originally by @KenWhitesell. Now when I navigate to a url such as http://localhost:8000/approved/2, Django serves the data for the entry designated by primary key identifier 2. When I cycle the primary key id to 3, it serves the next one in the sequence. So that is working. Success! I am making progress.

Here is my current working urls.py:

app_name = 'account_application_form'
urlpatterns = [
   path('hello/', views.new_application, name='new_application'),
   path('approved/<int:pk>', views.approved, name='approved'),   
]

Above you can see I included <int:pk> as part of the web addres string for the approved view function just as Ken previously advised was the right thing to do.

When the web visitor lands on http://localhost:8000/hello/, Django serves the template asking the visitor to enter their first name, last name, year of birth. So that works too.

The problem now is that when the web visitor clicks the “Submit Application” button, Django throws an “AttributeError” referring to the “BankAccountApplicationForm object having no id”.

Here is the full traceback in my Django dev server log:

[05/Sep/2022 12:23:40] "POST /hello/ HTTP/1.1" 500 69861
{'first_name': '2', 'last_name': '22', 'year_of_birth': '1900'}
Internal Server Error: /hello/
Traceback (most recent call last):
  File "/home/<user>/dev/projects/python/2018-and-2020/ATM_v2_Django/venv/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File "/home/<user>/dev/projects/python/2018-and-2020/ATM_v2_Django/venv/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/<user>/dev/projects/python/2018-and-2020/ATM_v2_Django/new_application/views.py", line 17, in new_application
    return redirect(reverse('account_application_form:approved', args=[form.id]))
AttributeError: 'BankAccountApplicationForm' object has no attribute 'id'
[05/Sep/2022 12:25:30] "POST /hello/ HTTP/1.1" 500 69861

What is going on with my class model BankAccountApplicationForm id attribute getting thrown around incorrectly by my views.py? As far as I can tell, everything should be working. Can someone kindly elaborate what this error is referring to? What will it take to get the web visitor’s submitted data to redirect to their “Application is Approved” page properly?

The issue here is that the form doesn’t have an id. The form is used to save an object. It’s that object being saved that has the id.

The form.save() method returns the instance of the model saved. Your statement calling save could then be application = form.save(), allowing you to use application.id in that second statement.

[Edit: You also have two other options which we can go over later if you are interested.]

1 Like

Hi Ken!

Thanks for the reply. Below the second conditional in my new_application function view, as per your advice, it now looks like this:

application_form = form.save()            
return redirect(reverse('account_application_form:approved', args=[application_form.id]))

My Django form fields are now processing web visitor input smoothly and successfully.

You mentioned there are two alternatives. Yes, I am curious. I am open to feedback on how to more optimally handle user input with Django.

I think I can already anticipate one of your suggestions. I finished watching the section in Jose Portilla’s Django 4 Udemy course covering Class Based Views last week. CBV’s are one possible alternative to the function based view approach that I currently have working. Although I didn’t code-along-side the instructor which is what I will need to do next to take my this Django web app to the next level.

If you have anything else to add, I am open, receptive, and would be grateful for any further guidance.

I would absolutely change this.

The function call form.save() does not return a form. It returns the instance of the object created or updated as a result of data being entered in the form. As a result, the name application_form is misleading. It really should be called application, because that’s the data type of the object returned from the form.save().

You are correct, but that wasn’t the direction I was going.

The other option I was going to mention is that the form internally keeps a reference to the object being saved as the attribute named instance. So instead of saving the return value from form.save(), you could reference the instance attribute directly as form.instance - or in the case of your reverse call, it would be form.instance.id.

(Yes, I know I said there were two other options, but upon further thought, my original third idea doesn’t really apply here, so I’m going to back off that satement to say there was this other option.)