Python Forum
ATM machine demo with Python and Django
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
ATM machine demo with Python and Django
#1
This is a long post. The most important information is at the top and gradually progresses to less and less relevant information. So you don’t have to read all of it. Just start reading at the top and then when you get bored, simply discard the rest.

I’m trying to build a rudimentary ATM machine demo with Python and Django.

Here is a screenshot of my site in its current form.

The intended basic functionality I am trying to create right now is to have the web visitor (banking client) enter the amount of cash in the input field and then click: “Deposit” or ”Withdraw”. Say for example the client deposits $20, the amount should appear in the HTML table under the “Credits” column and the “Running Balance” column should reflect that as well, like anyone would expect a typical ATM and basic accounting ledger to behave. The problem is, when a web visitor enters an amount and clicks “Deposit”, nothing happens.

I know the main problem is clearly with my views.py:

from django.shortcuts import render
from telagents.models import Account
 
def index(request):
   balances = 0
   withdrawals = 0
   deposits = 0
   amounts = 0
   data = Account.objects.all().order_by('-inception_date')
   context = {'data':data,'withdrawals':withdrawals,'deposits':deposits,'amount':amounts, 'balances': balances,} # initialize context var to be updated after math operations
   if 'deposits' in request.POST:  
       balances = balances + amounts
       context.update({'balances': balances,})
   elif 'withdrawals' in request.POST:
       balances  = balances - amounts
       context.update({'balance': balances,})   
   return render(request, "telagents/home.html", context)
As you can see above, I declare (initialize) the numerical variables each to integer 0. Next I establish the context variable as a dictionary. I am then trying to use conditional logic to catch cases when the template receives a POST request from the input form from the web visitor. But instead, it’s as if the deposits antecedent isn’t getting triggered so Django is effectively ignoring it and continues on to the next operation ultimately skipping down to the bottom and just passing in the original context dictionary with the variables assigned to 0. That’s not how I want my app to behave. Instead I want Django to receive a deposit from the user as a POST request and then process the basic mathematical operation (add the amount for a Deposit) and then update the balance according to client input and serve the updated balance back into the HTML table in the template. To achieve that desired outcome, I am all out of ideas on what to try next. Can anyone provide some guidance or further insight here?

Here is my template: home.html (which has its own set of issues that I already have some insight into discussed afterwards):
Output:
<body> {% block content %} <br><br> {% for data_obj in data %} <center> Client Name : {{data_obj.first_name}} {{data_obj.last_name}} <br> Bank Account Number : {{data_obj.account_number}} <br> Interest Rate : {{data_obj.interest}} % </center> {% endfor%} <br><br> <center> <form action="{% url 'index' %}" method="post"> {% csrf_token %} <!-- <label for="fname">Merchant Name:</label> <input type="text" id="fname" name="fname"><br><br> --> <label for="amount">Amount:</label> <input type="number" id="amount" name="amounts"><br><br> <input type="submit" value="Deposit" name="deposits"> <input type="submit" value="Withdraw"name="withdrawals"> </form> </center> <br> <!-- HTML Code: Place this code in the document's body (between the 'body' tags) where the table should appear --> <center> <table class="GeneratedTable"> <thead> <tr> <th>Type</th> <th>Timestamp</th> <th>Trans ID #</th> <th>Debits</th> <th>Credits</th> <th>Running Balance</th> </tr> </thead> <tbody> <tr> <td>Cell</td> {% for data_obj in data %} <td>{{ data_obj.transaction_time_stamp }}</td> {% endfor %} <td>Cell</td> <td>{{ withdrawals }}</td> <td>{{ deposits }}</td> <td>{{ balances }} </td> </tr> </tbody> </table> <!-- Codes by Quackit.com --> {% endblock %} </center>


The problem with the above template is the withdrawals, deposits, and balances. They are just empty variables. They are not dynamic. In an effort to make them dynamic, I tried wrapping them around with variations of Jinja for loops and conditionals but no matter which combination I tried, I couldn’t get the user input to reflect inside the right columns in the table.

I’ve got my models.py configured. For what it’s worth (and for general reference), here it is:

from django.db import models
from datetime import datetime
#from pytz import timezone
import decimal
from decimal import Decimal
from random import randint
 
 
class Account(models.Model):
   interest = models.DecimalField(max_digits=6, decimal_places=3) # Decimal('0.005') # Percent
   inception_date = models.DateTimeField('Client since (inception date)')
   first_name = models.CharField(max_length=30)
   last_name = models.CharField(max_length=30)
   account_number = models.BigIntegerField()
 
   def transaction_time_stamp(self):
       return self.inception_date.strftime("%A %d %B %Y @ %I:%M:%S %p")
  
   def __str__(self):
       return f'{self.first_name} {self.last_name}'
This project started out as an exercise for a hobby course on Udemy by Fred Baptiste that I am taking on Python OOP (working CLI script below) which I decided to port to Django:

from datetime import datetime
from pytz import timezone
import decimal
from decimal import Decimal
from random import randint
 
class TimeZone:
  
   def __init__(self):
       self.selection = int(input("Howdy! Make your time zone selection: \n 1: Los Angeles \n 2: London \n 3: Shanghai \n 4: Sydney \n 5: Rio de Janeiro \n"))
       self.locality = {
           1: "US/Pacific", # Los Angeles
           2: "Europe/London", # London
           3: "Asia/Shanghai", # Shanghai
           4: "Australia/Sydney", # Sydney
           5: "Brazil/East", # Rio de Janeiro
       }  
       self.tz = datetime.now(timezone(self.locality[self.selection]))
       self.readable_format = '%Y-%m-%d %H:%M:%S %Z%z'
       print(f'The date and time: {self.tz.strftime(self.readable_format)}')
       self.transaction_time_id_format = '%Y%m%d%H%M%S'
       print(f'The date and time condensed: {self.tz.strftime(self.transaction_time_id_format)}')
  
   def condensed(self):
      return f'{self.tz.strftime(self.transaction_time_id_format)}'
 
class Account:
  
   interest = Decimal('0.005') # Percen t
 
   def __init__(self, first_name, last_name, account_num=10000001, starting_balance=0.00):
       self.first_name = first_name
       self.last_name = last_name
       self.full_name = f'{first_name} {last_name}'
       self.account_num = randint(9999999,99999999)
       self.balance = round(Decimal(starting_balance),2)
       self.transaction_id = randint(101,999)
       self.tzone = TimeZone()
 
   def deposit(self, amount):
       self.balance += round(Decimal(amount),2)
       self.transaction_id += randint(101,999) - randint(101,999)
       return f'D-{self.account_num}-{self.tzone.condensed()}-{self.transaction_id}'
  
   def withdraw(self, amount):
       if amount > self.balance:        
           self.transaction_id += randint(101,999) - randint(101,999)
           print(f'X-{self.account_num}-{self.tzone.condensed()}-{self.transaction_id}')
           raise ValueError('Transaction declined. Insufficient funds. Please deposit some more $$$ first.')
          
       self.balance -= round(Decimal(amount),2)
       self.transaction_id += randint(101,999) - randint(101,999)
       return f'W-{self.account_num}-{self.tzone.condensed()}-{self.transaction_id}'
 
   def pay_interest(self):
       monthly_rate = self.interest/12
       monthly_sum = monthly_rate * self.balance
       self.transaction_id += randint(101,999) - randint(101,999)
       print(f'I-{self.account_num}-{self.tzone.condensed()}-{self.transaction_id}')
       return round(Decimal(monthly_sum, + self.balance), 2)
 
   def __repr__(self):
       """Return a string that represents the account."""
       return f"{self.__class__.__name__}({self.last_name}, {self.first_name}, balance={self.balance})"
Below are the original specs for the above completed exercise. My goal for my Django ATM machine is to reach feature parity.

SPECS
Here are the basic specs, functionality, and characteristics as outlined by course instructor Fred Baptiste:
  • accounts are uniquely identified by an account number (assume it will just be passed in the initializer)
  • account holders have a first and last name
  • accounts have an associated preferred time zone offset (e.g. -7 for MST)
  • balances need to be zero or higher, and should not be directly settable.
  • but, deposits and withdrawals can be made (given sufficient funds)
  • if a withdrawal is attempted that would result in negative funds, the transaction should be declined.
  • a monthly interest rate exists and is applicable to all accounts uniformly. There should be a method that can be called to calculate the interest on the current balance using the current interest rate, and add it to the balance.
  • each deposit and withdrawal must generate a confirmation number composed of:
  • the transaction type: D for deposit, and W for withdrawal, I for interest deposit, and X for declined (in which case the balance remains unaffected)
    - the account number
    - the time the transaction was made, using UTC
    - an incrementing number (that increments across all accounts and transactions)
    - for (extreme!) simplicity assume that the transaction id starts at zero (or - whatever number you choose) whenever the program starts
    - the confirmation number should be returned from any of the transaction methods (deposit, withdraw, etc)
  • create a method that, given a confirmation number, returns:
    - the account number, transaction code (D, W, etc), datetime (UTC format), date time (in whatever timezone is specified in te argument, but more human readable), the transaction ID
    - make it so it is a nicely structured object (so can use dotted notation to access these three attributes)
    - I purposefully made it so the desired timezone is passed as an argument. Can you figure out why? (hint: does this method require any information from any instance?)
Reply
#2
Hello, Friends!

I reviewed the Django docs on forms. In particular I leveraged the sections on views and on field data. Following along with these docs closely and based on what I learned, I rebuilt my codebase. I created forms.py and completely rewrote both my template and views.py from scratch. Now I am getting a key error. I figure the solution I need is far more trivial today than last week but at this point I am basing my head against the wall and stabbing in the dark.

I’ll share my code, along with the traceback and explain what I think is going on which hopefully will be enough for you people to recommend what I could try next.

forms.py:
from django import forms

class AmountForm(forms.Form):
    amount = forms.DecimalField(label='Amount', max_digits=10, decimal_places=2)
As you can see above, there is only one field I’ve declared: amount. This is the only information the web visitor can input when they visit the site. My form has two input buttons (‘Withdraw’ and ‘Deposit’) but when they are pressed by the web visitor, Django redirects the amount value back for processing by the views (below) which should then in turn store it in the database and then be presented back to the web visitor with an updated balance variable in the table when Django re-serves the template.

views.py:
from django.shortcuts import render
from django.http import HttpResponseRedirect,HttpResponse
from .models import Account
# Create your views here.
from .forms import AmountForm

def index(request):
    # Starting balance variable initialization:
    balance = 0 
    context = {'balance': balance}
    # Import `Account` model data:
    data = Account.objects.all().order_by('-inception_date')
    # 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 = AmountForm(request.POST)
        # Check whether it's valid:
        if form.is_valid():
            # Process the data in form.cleaned_data as required:
            deposit = form.cleaned_data['deposit']
            withdraw = form.cleaned_data['withdraw']
            amount = form.cleaned_data['amount']
            if deposit:
                balance = balance + amount
                context.update({'balance': balance,})
            elif withdraw:
                balance = balance - withdraw
                context.update({'balance': balance,})
            # Redirect to a new URL:
            return render(request, 'telagents/home.html', {'form': form, 'data':data, 'context': context,})

    # If a GET (or any other method) we'll create a blank form:
    else:
        form = AmountForm()

    return render(request, 'telagents/home.html', {'form': form, 'data':data, })
As you can see in the above views, I’ve retained and added annotations line-by-line to help document what is going on. When Django checks for the validity of the form, assuming it is valid, received (and cleaned) data (deposit, withdraw, amount) is assigned to variables. If deposit is triggered, then the basic mathematical operation takes place (increase the size of balance by the amount entered by the web visitor). And then I attempt to update the context dictionary variable with the new balance amount. Finally, the request is rendered for the home template while passing in the form, data, and context information. That’s ‘air tight’. That’s my very best effort up to this point at rewriting a proper views.py based on the documentation.

templates/telagents/home.html:
Quote: <body>
{% block content %}

<br><br>

{% for data_obj in data %}
<center>
Client Name : {{data_obj.first_name}} {{data_obj.last_name}}
<br>
Bank Account Number : {{data_obj.account_number}}
<br>
Client Since : {{ data_obj.inception_date }}
<br>
Interest Rate : {{data_obj.interest}} %
</center>
{% endfor%}

<br><br>

<center>
<form action="{% url 'index' %}" method="post">

{% csrf_token %}
{{ form }}
<input type="submit" value="Deposit" name="deposit" >
<input type="submit" value="Withdraw" name="withdraw">
</form>
</center>
<br>

<center>
<table class="GeneratedTable">
<thead>
<tr>
<th>Type</th>
<th>Timestamp</th>
<th>Trans ID #</th>
<th>Debits</th>
<th>Credits</th>
<th>Running Balance</th>
</tr>
</thead>
<tbody>
<tr>
{% for trans_objs in context %}
<td>Cell</td>

<td>Cell</td>

<td>Cell</td>

<td>{{ trans_objs.withdraw }}</td>

<td>{{ trans_objs.deposit }}</td>

<td>{{ trans_objs.balance }} </td>
{% endfor %}
</tr>
</tbody>
</table>

</center>

{% endblock %}

</body>

As you can see above, I’ve used Django’s very easy and convenient instance of {{ form }} for easy deployment of HTML forms. I’ve got the CSRF token in place. I’ve got the two input tags for the withdraw and deposit buttons. Finally in the table, I’ve completely reworked the Jinja for loop to output the withdraw, deposit, and balance data points.

Here is my traceback:

Error:
System check identified no issues (0 silenced). May 20, 2022 - 17:36:04 Django version 4.0.4, using settings 'Django_ATM_OOP_demo.settings' Starting development server at http://localhost:9000/ Quit the server with CONTROL-C. Internal Server Error: / Traceback (most recent call last): File "/home/<user>/dev/projects/python/2018-and-2020/Django_ATM_OOP_demo/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/Django_ATM_OOP_demo/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/Django_ATM_OOP_demo/telagents/views.py", line 20, in index deposit = form.cleaned_data['deposit'] KeyError: 'deposit'
This error is pointing to line 20 of my views.py where the deposit variable is declared referencing where Django is trying to extrapolate the deposit information as cleaned_data. That’s all I understand about that.

This is the point where I ask for insight from the community.

What else is this traceback trying to say?

I suppose a good question for me to ask is which Django doc do I need to review next? Although I may be just missing a specific passage from the same Django doc on forms I leveraged earlier that I can review again.

Is there any advice you people could provide in terms of what I might need to change in my template, views, or forms to resolve this KeyError and enable my web app to properly process the data entry made by my web visitors?

For what it is worth and for those who may be interested, here is the full source code on GitHub (working branch).
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Django: How to automatically substitute a variable in the admin page at Django 1.11? m0ntecr1st0 3 3,295 Jun-30-2019, 12:21 AM
Last Post: scidam
  python-forum.io on way back machine metulburr 1 2,981 Jan-13-2019, 03:52 PM
Last Post: metulburr

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020