This is going to be the third and final part of this series. In the previous part of this series; our aim was just to get our application to work. Now that we have achieved that; in this part, we will be making our code more presentable.
Objectives:
I) Carry out seperation of concerns.
II) Add styling to our page to make the User Interface clean.
Objective I
All of our business logic was written in one single function in the views.py
file. We will be correcting that here by seperating our code into the service.py
and views.py
files.
We have to ensure that our service.py
file is not aware of any http communication, so we completely isolate the following logic to the views.py
file; the handling of input extraction from the HTTP request , input validation and handling of http parameters. Every other logic will go into the service.py
file. Explanation of the code will be included as comments in the various files where we will be making changes.
services
First we create a services.py
file in our price application directory.
Then we will add the code containing our functions below
import requests
from datetime import date, timedelta , datetime
from .forms import PriceSearchForm
#function to get the current and today-10days dates respectively
class getDateService():
def getCurrentDateView(self):
datetime_today = date.today() #get current date
date_today = str(datetime_today) #convert datetime class to string
date_10daysago = str(datetime_today - timedelta(days=10)) #get date of today -10 days
#assign 'date from' and 'date to' for chart template heading
date_from = date_10daysago
date_to = date_today
return date_from,date_to
#function to make the api get call and retrieve the default 10days api data.
class getDefaultData():
def makeDefaultApiView(self, date_from, date_to):
PriceFormSearch = initialData(date_from, date_to) #call the initial data function and append the values to the search form
search_form_default= PriceFormSearch
return search_form_default
class getUserInputDateRange():
def userFormInputView(self, date_from, date_to, date_today):
if date_to > date_today: #if the date to from input is greater than today's date; there wont be data for the extra days, so we change the 'date_to' input back to todays's date
date_to = date_today
PriceFormSearch = initialData(date_from, date_to)
search_form_current= PriceFormSearch #when the page reloads, set the default form date input values to the dates picked by the user
return search_form_current
class outOfRange():
def ooR(self, date_from, date_to, range_error):
from_date= datetime.strptime(date_from, '%Y-%m-%d').date()
to_date= datetime.strptime(date_to, '%Y-%m-%d').date()
if from_date < (to_date - timedelta(days=90)): #check if the date range is not greater than 3 months
range_error= 'No more than 3 months data can be displayed'
PriceFormSearch = initialData(date_from, date_to)
search_form_values= PriceFormSearch
return date_from, date_to, range_error, search_form_values
def initialData(date_from, date_to): #initial data function to render our date inputs out with chosen value
initial_data={'date_from':date_from,
'date_to':date_to,
}
PriceForm = PriceSearchForm(initial=initial_data) #append the date_from and date_to values to the search form
return PriceForm
views
You can go ahead and do a complete overhaul of your views file and replace the content with the code below.
import requests
from django.shortcuts import render
from .forms import PriceSearchForm
from .services import getDateService,getDefaultData,getUserInputDateRange,outOfRange #import business logic from services.py layer
# Create your views here.
def chart(request):
bitcoin_price= None
wrong_input = None
range_error = None
# assign the functions imported from services.py to variables to allow for easier use
initiateDateGet = getDateService()
initiateDefaultDataGet = getDefaultData()
initiateUserDateGet = getUserInputDateRange()
initiateRangeErrorGet = outOfRange()
date_from, date_to = initiateDateGet.getCurrentDateView() #get the dates for present day and present day - 10 days
search_form= initiateDefaultDataGet.makeDefaultApiView(date_from, date_to) #use the 10days period obtained from the function above to set the default form values
bitcoin_price = getBitcoinData(date_from, date_to)#use the 10days period obtained from the function above to get dafualt 10days data
from_date, to_date = getUserDateView(request) #if request method is 'post', validate the form and get date range supplied by user and use it for the api call
if from_date is not None and to_date is not None: #check if data was supplied by the user
date_today=date_to #assign todays date to date_today variable
date_from, date_to, date_out_of_range, search_form = initiateRangeErrorGet.ooR(from_date, to_date, range_error) #check if the supplied date range is not greater than 3 months
if date_out_of_range is not None:
range_error = date_out_of_range #if date range is more than 3 months, render this error in the html page
bitcoin_price = None
else:
bitcoin_price, date_from, date_to, wrong_input = getUserInputData(from_date, to_date, date_today, wrong_input) #if there is data supplied my the user via the form, proceed to make the api call and retrieve the required data
search_form = initiateUserDateGet.userFormInputView(from_date, to_date, date_today ) #make the date range submitted in the form supplied by the user via the form the default input of the form
context = {
'search_form': search_form,
'price': bitcoin_price,
'wrong_input':wrong_input,
'date_from':date_from,
'date_to':date_to,
'range_error': range_error
}
return render(request, "chart.html", context)
#function to confirm if valid date ranges have been supplied by the user.
def getUserDateView(request):
date_from = None
date_to = None
search_form= PriceSearchForm(request.POST or None) #get post request from the front end
if request.method == 'POST':
if search_form.is_valid(): #Confirm if valid data was received from the form
date_from = request.POST.get('date_from') #extract input 1 from submitted data
date_to = request.POST.get('date_to') #extract input 2 from submitted data
else:
raise Http400("Sorry, this did not work. Invalid input")
return date_from,date_to
def getUserInputData(date_from, date_to, date_today, wrong_input):
from_date= None
to_date= None
requested_btc_price_range= None
if date_to > date_from: #confirm that input2 is greater than input 1
if date_to > date_today: #if the date to from input is greater than today's date; there wont be data for the extra days, so we change the 'date_to' input back to todays's date
date_to = date_today
api= 'https://api.coindesk.com/v1/bpi/historical/close.json?start=' + date_from + '&end=' + date_to + '&index=[USD]' #use the 10days period obtained above to get dafualt 10days value
try:
response = requests.get(api, timeout=10) #get api response data from coindesk based on date range supplied by user with a timeout of 10seconds
response.raise_for_status() #raise error if HTTP request returned an unsuccessful status code.
prices = response.json() #convert response to json format
requested_btc_price_range=prices.get("bpi") #filter prices based on "bpi" values only
from_date= date_from
to_date= date_to
except requests.exceptions.ConnectionError as errc: #raise error if connection fails
raise ConnectionError(errc)
except requests.exceptions.Timeout as errt: #raise error if the request gets timed out after 10 seconds without receiving a single byte
raise TimeoutError(errt)
except requests.exceptions.HTTPError as err: #raise a general error if the above named errors are not triggered
raise SystemExit(err)
else:
wrong_input = 'Wrong date input selection: date from cant be greater than date to, please try again' #print out an error message if the user chooses a date that is greater than input1's date
return requested_btc_price_range, from_date, to_date , wrong_input,
def getBitcoinData(date_from, date_to):
api= 'https://api.coindesk.com/v1/bpi/historical/close.json?start=' + date_from + '&end=' + date_to + '&index=[USD]'
try:
response = requests.get(api, timeout=10) #get api response data from coindesk based on date range supplied by user
response.raise_for_status() #raise error if HTTP request returned an unsuccessful status code.
prices = response.json() #convert response to json format
default_btc_price_range=prices.get("bpi") #filter prices based on "bpi" values only
except requests.exceptions.ConnectionError as errc: #raise error if connection fails
raise ConnectionError(errc)
except requests.exceptions.Timeout as errt: #raise error if the request gets timed out after 10 seconds without receiving a single byte
raise TimeoutError(errt)
except requests.exceptions.HTTPError as err: #raise a general error if the above named errors are not triggered
raise SystemExit(err)
return default_btc_price_range
Objective II
We will be using crispy forms to make our forms look really crispy😉. All pun intended. Run the command below
pip install django-crispy-forms
Then after installation we'll need to adjust our settings file. Add this under your installed app section
#3rd Party
'crispy_forms'
and this directly below the installed app settings.
CRISPY_TEMPLATE_PACK = 'bootstrap4'
It should look exactly like this
chart
We are going to add this error alert code to our chart.html file to cap possible data that can be obtained to 3months so our chart does not become unreadable. We'll also be changing our form code and then replace our previous span elements with hidden input and add a h4 tag header to give our chart a dynamic title.
<!-- raise error when selected date range is more than 3 months -->
{% if range_error %}
<div class="alert alert-warning" role="alert">
{{range_error}}
</div>
{% endif %}
<div class="row">
<!-- form to filter dates on chart -->
<form id="myForm" action="" method='POST'>
{% csrf_token %}
<div class="d-flex flex-nowrap cover">
{{search_form| crispy}} <!-- render form with crispy forms -->
<div class="">
<button type="submit" class="order-2 btn btn-primary mt-3"> Render</button>
</div>
</div>
</form>
</div>
<h4 class="text-center">Bitcoin price change from {{date_from}} to {{date_to}}</h4>
{% for date,price in price.items %}
<input type="hidden" class="date-item" value="{{date}}">
<input type="hidden" class="price-item" value="{{price}}">
{% endfor %}
Please ensure you add all the code above within the {block element} template tags.
Next, add the bootstrap cdn and link to your css stylesheet within the {block style} template tags.
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<!-- Local css -->
<link rel="stylesheet" href="{% static 'css/style.css' %}">
chart.js
Since we are appending the data received from the api to our input element now, we can no longer use innerHtml. We need to go to our chart.js file and make little changes to lines 7 and 12. Change innerhtml to value.
date[i] = dates[i].value
price[j] = prices[j].value
style.css
Create a style.css file in your css folder and add the following lines of code to it.
.form-control {
width: unset;
margin: 0 !important ;
display: unset !important;
}
.form-group, .form-group div{
margin-right:0 !important ;
width: 33%;
display: unset !important;
}
.cover{
text-align: center;
justify-content: space-evenly;
}
form{
margin-bottom: 2rem;
}
input{
cursor: pointer;
}
.btn{
margin-top: 0 !important;
}
.chart-container{
width: 100% !important;
}
@media (max-width: 760px) {
.form-control{
width: 100%;
}
}
@media (max-width: 991px) {
.btn{
margin-top: 25px !important;
}
}
Congratulations. We have carried out separation of concern, so our code looks more readable and we have also added little styling to make it look much more presentable. Here is the new look of our application.
Thank you for coming this far. You can access the github repo here.
If you have any question, feel free to drop it as a comment or send me a message on Linkedin or Twitter and I'll ensure I respond as quick as I can. Ciao 👋