Skip to content

Commit

Permalink
Version 1.0.1 - View status of Coin's via the admin panel, plus vario…
Browse files Browse the repository at this point in the history
…us small changes

 - Add `health` function to coin_handlers.base.BaseManager - if children don't implement it,
   it defaults to a list of symbols, with status "Health data not supported".
   Child classes are able to customise the table columns to fit their specific needs.

 - Implemented `health` method for both SteemEngineManager and BitcoinManager

 - Created a "Coin Handler Health/Status" admin panel view, which lists health/status
   information about each coin in the database, using the `health` method of it's
   Manager class.

 - Created a "clear caches" view, allowing admin's to clear the Django cache backend. Currently
   only used by the Coin Handler status view.

 - Override Django admin's base_site.html and index.html
   - Changed admin title to CryptoToken Converter Admin
   - Added a custom pages table, with a link to "Coin Handler Health/Status"

 - Extended Django admin site class to add custom URLs

 - Better comments for settings.py, with some general clean-up of the file.
  • Loading branch information
Someguy123 committed Mar 24, 2019
1 parent 6fc66c6 commit bf22ab0
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 51 deletions.
118 changes: 109 additions & 9 deletions payments/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
from django import forms
from django.conf import settings
from django.contrib import admin
import logging

from django.contrib import admin, messages

# Register your models here.
from django.contrib.admin import AdminSite
from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib.auth.models import User, Group
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseNotAllowed
from django.shortcuts import render, redirect
from django.urls import path
from django.views.decorators.cache import cache_page
from django.views.generic import TemplateView

from payments.models import Coin, Deposit, AddressAccountMap, CoinPair, Conversion

"""
Expand All @@ -19,10 +30,27 @@
| |
+===================================================+
"""
from payments.coin_handlers import reload_handlers
from payments.coin_handlers import reload_handlers, has_manager, get_manager

log = logging.getLogger(__name__)


class CustomAdmin(AdminSite):
"""
To allow for custom admin views, we override AdminSite, so we can add custom URLs, among other things.
"""
def get_urls(self):
_urls = super(CustomAdmin, self).get_urls()
urls = [
path('coin_health/', CoinHealthView.as_view(), name='coin_health'),
path('_clear_cache/', clear_cache, name='clear_cache'),
]
return _urls + urls


ctadmin = CustomAdmin()


@admin.register(Coin)
class CoinAdmin(admin.ModelAdmin):
list_display = ('__str__', 'symbol', 'coin_type', 'enabled', 'our_account', 'can_issue')
list_filter = ('coin_type',)
Expand All @@ -34,20 +62,17 @@ def get_fieldsets(self, request, obj=None):
return super(CoinAdmin, self).get_fieldsets(request, obj)


@admin.register(AddressAccountMap)
class AddressAccountMapAdmin(admin.ModelAdmin):
list_display = ('deposit_coin', 'deposit_address', 'destination_coin', 'destination_address')
list_filter = ('deposit_coin', 'destination_coin',)
search_fields = ('deposit_address', 'destination_address',)
pass


@admin.register(CoinPair)
class CoinPairAdmin(admin.ModelAdmin):
pass


@admin.register(Conversion)
class ConversionAdmin(admin.ModelAdmin):
list_display = ('from_coin', 'from_address', 'from_amount', 'to_coin', 'to_address', 'to_amount',
'tx_fee', 'ex_fee', 'created_at')
Expand All @@ -56,10 +81,85 @@ class ConversionAdmin(admin.ModelAdmin):
ordering = ('created_at',)


@admin.register(Deposit)
class DepositAdmin(admin.ModelAdmin):
list_display = ('txid', 'status', 'coin', 'amount', 'address', 'from_account', 'to_account', 'tx_timestamp')
list_filter = ('status', 'coin',)
search_fields = ('id', 'txid', 'address', 'from_account', 'to_account', 'memo', 'refund_address')
ordering = ('created_at',)
pass


# Because we've overridden the admin site, the default user/group admin doesn't register properly.
# So we manually register them to their admin views.
ctadmin.register(User, UserAdmin)
ctadmin.register(Group, GroupAdmin)

ctadmin.register(Coin, CoinAdmin)
ctadmin.register(CoinPair, CoinPairAdmin)
ctadmin.register(Conversion, ConversionAdmin)
ctadmin.register(Deposit, DepositAdmin)
ctadmin.register(AddressAccountMap, AddressAccountMapAdmin)


class CoinHealthView(TemplateView):
"""
Admin view for viewing health/status information of all coins in the system.
Loads the coin handler manager for each coin, and uses the health() function to grab status info for the coin.
Uses caching API to avoid constant RPC queries, and displays results as a standard admin view.
"""
template_name = 'admin/coin_health.html'

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.coin_fails = []

def get_fails(self):
"""View function to be called from template, for getting list of coin handler errors"""
return self.coin_fails

def handler_dic(self):
"""View function to be called from template. Loads and queries coin handlers for health, with caching."""
hdic = {} # A dictionary of {handler_name: {headings:list, results:list[tuple/list]}
reload_handlers()
for coin in Coin.objects.all():
try:
if not has_manager(coin.symbol):
self.coin_fails.append('Cannot check {} (no manager registered in coin handlers)'.format(coin))
continue
# Try to load coin health data from cache.
# If it's not found, query the manager and cache it for up to 30 seconds to avoid constant RPC hits.
c_health = coin.symbol + '_health'
mname, mhead, mres = cache.get_or_set(c_health, get_manager(coin.symbol).health(), 30)
# Create the dict keys for the manager name if needed, then add the health results
d = hdic[mname] = dict(headings=list(mhead), results=[]) if mname not in hdic else hdic[mname]
d['results'].append(list(mres))
except:
log.exception('Something went wrong loading health data for coin %s', coin)
self.coin_fails.append(
'Failed checking {} (something went wrong loading health data, check server logs)'.format(coin)
)
return hdic

def get(self, request, *args, **kwargs):
r = self.request
u = r.user
if not u.is_authenticated or not u.is_superuser:
raise PermissionDenied
return super(CoinHealthView, self).get(request, *args, **kwargs)


def clear_cache(request):
"""Allow admins to clear the Django cache system"""
if request.method.upper() != 'POST':
raise HttpResponseNotAllowed(['POST'])

u = request.user
if not u.is_authenticated or not u.is_superuser:
raise PermissionDenied
cache.clear()
# Redirect back to the previous page. If not set, send them to /
referer = request.META.get('HTTP_REFERER', '/')
messages.add_message(request, messages.SUCCESS, 'Successfully cleared Django cache')
return redirect(referer)
50 changes: 49 additions & 1 deletion payments/coin_handlers/Bitcoin/BitcoinManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"""
import logging
from typing import List, Dict
from typing import List, Dict, Tuple

from decimal import Decimal, getcontext, ROUND_DOWN
from privex.jsonrpc import BitcoinRPC
Expand Down Expand Up @@ -65,13 +65,61 @@ class BitcoinManager(BaseManager, BitcoinMixin):
of BitcoinRPC - stored as a static property, ensuring we don't have to constantly re-create them.
"""

def health(self) -> Tuple[str, tuple, tuple]:
"""
Return health data for the passed symbol.
Health data will include: Symbol, Status, Current Block, Node Version, Wallet Balance,
and number of p2p connections (all as strings)
:return tuple health_data: (manager_name:str, headings:list/tuple, health_data:list/tuple,)
"""
headers = ('Symbol', 'Status', 'Current Block', 'Version',
'Wallet Balance', 'P2P Connections',)
class_name = type(self).__name__
status = 'Unknown Error'
current_block = version = balance = connections = ''
# If the coin is based on a newer protocol, e.g. bitcoin core + litecoin, use the new methods
try:
b = self.rpc.getblockchaininfo()
current_block = '{:,}'.format(b['blocks'])
if 'headers' in b:
current_block += ' (Headers: {:,})'.format(b['headers'])
n = self.rpc.getnetworkinfo()
version = '{} ({})'.format(n['version'], n['subversion'])
connections = str(n['connections'])
balance = '{0:,.8f}'.format(self.rpc.getbalance())
status = 'Online'
except:
# If we get an error, try using the old method
try:
b = self.rpc.getinfo()
current_block = '{:,}'.format(int(b['blocks']))
version = b['version']
balance = '{0:,.8f}'.format(b['balance'])
connections = str(b['connections'])
status = 'Online'
except:
log.exception('Exception during %s health check for symbol %s', class_name, self.symbol)
# If this also errors, it's definitely offline
status = 'Offline'

if status == 'Online':
status = '<b style="color: green">{}</b>'.format(status)
else:
status = '<b style="color: red">{}</b>'.format(status)

data = (self.symbol, status, current_block, version, balance, connections,)
return class_name, headers, data

def __init__(self, symbol: str):
super().__init__(symbol.upper())
# Get all RPCs
self.rpcs = self._get_rpcs()
# Manager's only deal with one coin, so unwrap the generated dicts
self.rpc = self.rpcs[symbol] # type: BitcoinRPC


@property
def settings(self) -> Dict[str, dict]:
"""To ensure we always get fresh settings from the DB after a reload, self.settings gets _prep_settings()"""
Expand Down
47 changes: 44 additions & 3 deletions payments/coin_handlers/SteemEngine/SteemEngineManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"""
import logging
import privex.steemengine.exceptions as SENG
from typing import List
from typing import List, Tuple
from beem.exceptions import MissingKeyError
from decimal import Decimal, getcontext, ROUND_DOWN
from payments.coin_handlers.base import exceptions, BaseManager
Expand Down Expand Up @@ -68,6 +68,46 @@ def __init__(self, symbol: str):
super().__init__(symbol.upper())
self.eng_rpc = SteemEngineToken()

def health(self) -> Tuple[str, tuple, tuple]:
"""
Return health data for the passed symbol.
Health data will include: Symbol, Status, Current Block, Node Version, Wallet Balance,
and number of p2p connections (all as strings)
:return tuple health_data: (manager_name:str, headings:list/tuple, health_data:list/tuple,)
"""
headers = ('Symbol', 'Status', 'API Node', 'Token Name', 'Issuer',
'Precision', 'Our Account', 'Our Balance')

class_name = type(self).__name__
api_node = token_name = issuer = precision = our_account = balance = ''

status = 'Okay'
try:
rpc = self.eng_rpc
api_node = rpc.rpc.url
our_account = self.coin.our_account
if not rpc.account_exists(our_account):
status = 'Account {} not found'.format(our_account)
tk = rpc.get_token(self.symbol)
issuer = tk.get('issuer', 'ERROR GETTING ISSUER')
token_name = tk.get('name', 'ERROR GETTING NAME')
precision = str(tk.get('precision', 'ERROR GETTING PRECISION'))
balance = self.balance(our_account)
balance = ('{0:,.' + str(tk['precision']) + 'f}').format(balance)
except:
status = 'ERROR'
log.exception('Exception during %s.health for symbol %s', class_name, self.symbol)

if status == 'Okay':
status = '<b style="color: green">{}</b>'.format(status)
else:
status = '<b style="color: red">{}</b>'.format(status)

data = (self.symbol, status, api_node, token_name, issuer, precision, our_account, balance)
return class_name, headers, data

def balance(self, address: str = None, memo: str = None, memo_case: bool = False) -> Decimal:
"""
Get token balance for a given Steem account, if memo is given - get total symbol amt received with this memo.
Expand All @@ -79,8 +119,9 @@ def balance(self, address: str = None, memo: str = None, memo_case: bool = False
"""

address = address.lower()
memo = memo.strip()
if memo is None:
if memo is not None:
memo = str(memo).strip()
if empty(memo):
return self.eng_rpc.get_token_balance(user=address, symbol=self.symbol)
txs = self.eng_rpc.list_transactions(user=address, symbol=self.symbol, limit=1000)
bal = Decimal(0)
Expand Down
13 changes: 13 additions & 0 deletions payments/coin_handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@ def get_loaders(symbol: str = None) -> list:
return [(s, data['loaders'],) for s, data in handlers] if symbol is None else handlers[symbol]['loaders']


def has_manager(symbol: str) -> bool:
"""Helper function - does this symbol have a manager class?"""

if not handlers_loaded: reload_handlers()
return symbol.upper() in handlers and len(handlers[symbol].get('managers', [])) > 0


def has_loader(symbol: str) -> bool:
"""Helper function - does this symbol have a loader class?"""
if not handlers_loaded: reload_handlers()
return symbol.upper() in handlers and len(handlers.get('loaders', [])) > 0


def get_managers(symbol: str = None) -> list:
"""
Get all manager's, or all manager's for a certain coin
Expand Down
18 changes: 17 additions & 1 deletion payments/coin_handlers/base/BaseManager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
from abc import ABC, abstractmethod
from decimal import Decimal
from typing import Tuple

from django.conf import settings

Expand All @@ -21,7 +22,6 @@ class BaseManager(ABC):
- The coin symbol ``self.symbol`` passed to the constructor
- The setting_xxx fields on ``self.coin`` :class:`payments.models.Coin`
- The Django settings `from django.conf import settings`
- They should also use the logging instance ``settings.LOGGER_NAME``
If your class requires anything to be added to the Coin object settings, or the Django ``settings`` file,
you should write a comment listing which settings are required, which are optional, and their format/type.
Expand Down Expand Up @@ -50,6 +50,22 @@ def __init__(self, symbol: str):
# The Coin object matching the `symbol`
self.coin = Coin.objects.get(symbol=symbol, enabled=True)

def health(self) -> Tuple[str, tuple, tuple]:
"""
Return health data for the passed symbol, e.g. current block height, block time, wallet balance
whether the daemon / API is accessible, etc.
It should return a tuple containing the manager name, the headings for a health table,
and the health data for the passed symbol (Should include a ``symbol`` or coin name column)
You may use basic HTML tags in the health data result list, such as
``<b>`` ``<em>`` ``<u>`` and ``<span style=""></span>``
:return tuple health_data: (manager_name:str, headings:list/tuple, health_data:list/tuple,)
"""

return type(self).__name__, ('Symbol', 'Status',), (self.symbol, 'Health data not supported')

@abstractmethod
def address_valid(self, address) -> bool:
"""
Expand Down
9 changes: 9 additions & 0 deletions payments/templates/admin/base_site.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends "admin/base.html" %}

{% block title %}CryptoToken Converter Admin{% endblock %}

{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">CryptoToken Converter Admin</a></h1>
{% endblock %}

{% block nav-global %}{% endblock %}
Loading

0 comments on commit bf22ab0

Please sign in to comment.