Finishing Up
It’s pretty amazing what we were able to build with less than 150 lines of custom code.
I’m sure there are plenty of things you would change about our account dashboard if it was really going live, but our basic functionality is working and in place. Here’s the completed extension for reference:
import arrow
from q2_sdk.core import q2_requests
from q2_sdk.core.cache import cache
from q2_sdk.core.http_handlers.tecton_server_handler import Q2TectonServerRequestHandler
from q2_sdk.core.web import ajax
from q2_sdk.hq.hq_api.q2_api import GetAccountHistoryById
from q2_sdk.hq.models.db_config.db_config import DbConfig
from q2_sdk.hq.models.db_config.db_config_list import DbConfigList
from q2_sdk.models.tecton import Success
class AccountDashboardHandler(Q2TectonServerRequestHandler):
REQUIRED_CONFIGURATIONS = {
'fi_name': 'Imaginary FI of Texas'
}
WEDGE_ADDRESS_CONFIGS = DbConfigList(
[
DbConfig('apikey', 'S6TczFkiWjDZy7FElvqKYMogzdcEcoQy')
]
)
CONFIG_FILE_NAME = 'AccountDashboard' # configuration/AccountDashboard.py file must exist if REQUIRED_CONFIGURATIONS exist
TECTON_URL = 'https://cdn1.onlineaccess1.com/cdn/base/tecton/v1.38.0/q2-tecton-sdk.js'
def __init__(self, application, request, **kwargs):
super().__init__(application, request, **kwargs)
@property
def router(self):
router = super().router
router.update({
'default': self.default,
'submit': self.submit,
'get_currency_data': self.get_currency_data
})
return router
async def default(self):
account_models = []
for account in self.account_list:
account_models.append({
'account_num': account.host_acct_id,
'account_name': account.product_name,
'account_balance': account.balance_to_display
})
template = self.get_template('index.html.jinja2', {
'accounts': account_models
})
html = self.get_tecton_form(
self.config.fi_name,
custom_template=template,
routing_key="submit",
hide_submit_button=True
)
return html
@cache(timeout=3600, key="quotes_dictionary")
async def get_currency_rates(self):
self.logger.info('Getting current exchange rates from API')
response = await q2_requests.get(
self.logger,
'https://local-dev-api.q2developer.com/projects/currencyRates',
headers={
'apikey': 'S6TczFkiWjDZy7FElvqKYMogzdcEcoQy'
}
)
response_as_dict = response.json()
quotes_dictionary = response_as_dict.get('quotes', {})
if not quotes_dictionary:
self.logger.error('CurrencyRates API request failed')
return quotes_dictionary
async def submit(self):
rates_dictionary = await self.get_currency_rates()
sorted_rates = sorted(rates_dictionary.keys())
current_account_id = self.form_fields['account_id']
selected_currency = self.form_fields.get('currency', 'USD')
params_obj = GetAccountHistoryById.ParamsObj(
self.logger,
current_account_id,
'',
hq_credentials=self.hq_credentials
)
hq_response = await GetAccountHistoryById.execute(params_obj)
transaction_models = []
for transaction in hq_response.result_node.Data.AllHostTransactions.Transactions:
date_string = str(transaction.PostDate)
arrow_date_object = arrow.get(date_string)
display_date = arrow_date_object.format('MMMM DD, YYYY')
is_credit = (transaction.SignedTxnAmount > 0)
if selected_currency and selected_currency != 'USD':
# multiply our USD amount by the retrieved exchange rate
# return a string representation of the absolute value of the amount
# in our new category rounded to 2 decimal places
modified_amount = transaction.TxnAmount * rates_dictionary[selected_currency]
display_amount = format(modified_amount, '.2f')
else:
# if we are displaying USD, just return a string representation of
# the absolute value of the amounts as before
display_amount = str(abs(transaction.TxnAmount))
display_description = str(transaction.Description)
transaction_models.append({
'display_date': display_date,
'display_amount': display_amount,
'is_credit': is_credit,
'display_description': display_description
})
template = self.get_template(
'transaction_history.html.jinja2',
{
'header': 'Transaction History',
'transactions': transaction_models,
'currency_options': sorted_rates,
'current_account_id': self.form_fields['account_id'],
'current_selected_currency': selected_currency
}
)
html = self.get_tecton_form(
"Transaction History",
custom_template=template,
hide_submit_button=True
)
return html
@ajax
async def get_currency_data(self):
rates_dictionary = await self.get_currency_rates()
selected_currency = self.form_fields.get('currency', 'USD')
conversion_rate = rates_dictionary[selected_currency]
return Success({
'selectedCurrency': selected_currency,
'conversionRate': conversion_rate
})
<style>
table {
table-layout: fixed;
text-align: left;
}
td {
vertical-align: top;
}
td,
th {
padding: 15px 10px;
}
th {
box-shadow: inset 0 -2px 0 0 #cccccc;
}
td {
box-shadow: inset 0 -1px 0 0 #eeeeee;
}
.text-right {
text-align: right;
}
</style>
<img src="{{ this.base_assets_url }}/logo.png" alt="Logo" height="84" width="84">
<div class="table-wrapper">
<table>
<thead>
<tr>
<th width="150">
Host Account ID
</th>
<th>
Account Name
</th>
<th class="text-right">
Account Balance
</th>
</tr>
</thead>
<tbody>
{% for account in accounts %}
<tr>
<td>{{ account.account_num }}</td>
<td>{{ account.account_name }}</td>
<td class="text-right">{{ account.account_balance }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<q2-input
name='account_id'
id='account_id'
label="Account ID">
</q2-input>
<q2-btn intent="workflow-primary" onclick="validateForm(event)">Get Transaction History</q2-btn>
<script>
window.tecton.connected.then(function(tecton) {
const accounts = {{ accounts|tojson }}
const accountNumbers = accounts.map(account => account.account_num)
let inputField = document.getElementById('account_id')
inputField.onblur = checkForErrors
function checkForErrors(event) {
if (!accountNumbers.find(accountNumber => accountNumber == parseInt(event.target.value))){
inputField.errors = ["That's not a real account number. Please try again."]
} else {
inputField.errors = undefined
}
}
window.validateForm = function validateForm(event) {
let inputs = Array.from(document.querySelectorAll('q2-input'))
const form = document.querySelector('form')
if (inputs.find(input => input.errors)){
tecton.actions.showModal({
title: 'Form has Errors',
message: 'Please correct the fields with errors, indicated by a red triangle.',
modalType: 'error'
})
} else {
submitForm()
}
}
});
</script>
<div>
<a href="#default">Back to Accounts</a>
</div>
<q2-select label="Currency" name="currency">
{% for currency in currency_options %}
{% if currency == current_selected_currency %}
<q2-option value="{{currency}}" display="{{currency}}" selected>{{currency}}</q2-option>
{% else %}
<q2-option value="{{currency}}" display="{{currency}}">{{currency}}</q2-option>
{% endif %}
{% endfor %}
</q2-select>
<q2-btn id="getCurrencies" intent="workflow-secondary">Get Currency Data</q2-btn>
<input id="hidden_account_id_storage" name="account_id" value="{{current_account_id}}" type="hidden">
<div>
<a href="#submit">Convert Currency</a>
</div>
<div id="placeholder-converted-values">
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th width="100">
Date
</th>
<th>
Description
</th>
<th class="text-right">
Amount
</th>
</tr>
</thead>
<tbody>
{% for transaction in transactions %}
<tr>
<td>{{transaction.display_date}}</td>
<td>{{transaction.display_description}}</td>
{% if transaction.is_credit %}
<td class="text-right clr(const-stoplight-success)">+{{transaction.display_amount}} {{current_selected_currency}}</td>
{% else %}
<td class="text-right">-{{transaction.display_amount}} {{current_selected_currency}}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
<script>
window.tecton.connected.then(function(tecton) {
document.querySelector('q2-select').value = '{{current_selected_currency}}';
const currencyButton = document.getElementById('getCurrencies')
currencyButton.onclick = fetchCurrencies
function fetchCurrencies(event) {
tecton.sources.requestExtensionData({
route: 'get_currency_data',
body: {
'currency': document.querySelector('q2-select').value
}
})
.then(response => {
const data = response.data
const currencyDiv = document.createElement('div')
const currencyText = document.createTextNode(`USD to ${document.querySelector('q2-select').value}: ${data.conversionRate}`)
currencyDiv.appendChild(currencyText)
document.querySelector('#placeholder-converted-values').appendChild(currencyDiv);
})
.catch(error => {
tecton.actions.showModal({
title: 'Error Fetching Currencies',
message: `We ran into this error while trying to fetch currencies: ${error.message}`,
modalType: 'error'
})
})
}
})
</script>
</div>
If we were going to take it further, what would you add? Better styling? More features? More third-party functionality? If you can do it in Python, it can be done in the Caliper SDK. That completes our tutorial, but you can check out the reference materials for more ideas and information. Thanks for following along!
Note
The currency preferences would be saved per user but not per extension. For example: If the username retail0 is used to login, the currency preference saved would be same for all those logged in using retail0
Note
If this was your own extension, at this point you might be thinking about how to get it in the hands of your users. Our guide to review and deployment is here: Review and Deployment.