Update commodity and transaction details and list

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2020-03-26 15:18:41 -04:00
parent c320f81181
commit cf2b4ab06d
9 changed files with 211 additions and 31 deletions

View File

@@ -1,4 +1,4 @@
# Generated by Django 2.2 on 2020-03-26 16:27
# Generated by Django 2.2 on 2020-03-26 19:12
from django.conf import settings
from django.db import migrations, models
@@ -43,9 +43,9 @@ class Migration(migrations.Migration):
name='Commodity',
fields=[
('id', hashid_field.field.HashidAutoField(alphabet='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', min_length=7, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True, verbose_name='commodity name')),
('name', models.CharField(editable=False, max_length=100, unique=True, verbose_name='commodity name')),
('symbol', models.CharField(editable=False, max_length=6, unique=True, verbose_name='symbol')),
('in_circulation', models.PositiveIntegerField()),
('symbol', models.CharField(blank=True, max_length=6, verbose_name='symbol')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
@@ -58,6 +58,8 @@ class Migration(migrations.Migration):
fields=[
('id', hashid_field.field.HashidAutoField(alphabet='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', min_length=7, primary_key=True, serialize=False)),
('status', models.CharField(choices=[('OPEN', 'Open'), ('DECL', 'Declined'), ('ACC', 'Accepted')], default='OPEN', max_length=2)),
('create_time', models.DateTimeField(auto_now_add=True)),
('update_time', models.DateTimeField(auto_now=True)),
('source_amount', models.PositiveIntegerField(editable=False)),
('dest_amount', models.PositiveIntegerField(editable=False)),
('dest', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='tx_received', to=settings.AUTH_USER_MODEL)),

View File

@@ -1,11 +1,12 @@
from datetime import date
from datetime import datetime
from typing import Mapping, Optional, Sequence
from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models
from django.db.models import Q
from django.db.models import Q, Sum
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from frozendict import frozendict
from guardian.shortcuts import assign_perm
@@ -75,8 +76,6 @@ class User(PermissionsMixin, AbstractBaseUser):
return self.balances().get(commodity, 0)
def balances(self) -> Mapping["Commodity", int]:
from django.db.models import Sum
# We are the source, receiving from the destination
credits_sent = {
row["dest_sends"]: row["dest_amount__sum"]
@@ -172,24 +171,82 @@ class Commodity(models.Model):
id = HashidAutoField(primary_key=True)
name = models.CharField(
blank=False, unique=True, max_length=100, verbose_name=_("commodity name")
blank=False,
unique=True,
editable=False,
max_length=100,
verbose_name=_("commodity name"),
)
symbol = models.CharField(
blank=False, unique=True, editable=False, max_length=6, verbose_name=_("symbol")
)
in_circulation = models.PositiveIntegerField()
created_by = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
symbol = models.CharField(blank=True, max_length=6, verbose_name=_("symbol"))
class Meta:
verbose_name = _("commodity")
verbose_name_plural = _("commodities")
def get_absolute_url(self):
return "/c/detail/%s" % self.pk
def trading_volume(self, since: Optional[datetime] = None) -> int:
"""
Gets the amount of this commodity traded since the given cutoff, inclusive.
def html_symbol(self) -> str:
if self.symbol:
return f"<span>{self.symbol}</span>"
else:
return f"<span>units of {self.name}</span>"
If no cutoff is given, then it returns the volume of all transactions.
"""
source_filt = {"source_sends": self, "status": TxRequest.ACCEPTED}
dest_filt = {"dest_sends": self, "status": TxRequest.ACCEPTED}
if since is not None:
source_filt["update_time__gte"] = since
dest_filt["update_time__gte"] = since
source_query = (
TxRequest.objects.filter(**source_filt)
.annotate(Sum("source_amount"))
.values("source_amount__sum")
.first()
)
dest_query = (
TxRequest.objects.filter(**dest_filt)
.annotate(Sum("dest_amount"))
.values("dest_amount__sum")
.first()
)
source_amount = source_query["source_amount__sum"] if source_query else 0
dest_amount = dest_query["dest_amount__sum"] if dest_query else 0
return source_amount + dest_amount
@property
def market_spread(self) -> int:
count = 0
for user in User.objects.all():
if not user.is_active:
continue
if user.balance_of(self) > 0:
count += 1
return count
@property
def market_spread_ratio(self) -> float:
user_count = len(User.objects.all())
if user_count == 0:
return 0.0
return self.market_spread / user_count
@property
def amount_pending(self) -> int:
"""
Gets the amount of this commodity that is currently being held in pending transactions.
"""
pending = (
TxRequest.objects.filter(source_sends=self, status=TxRequest.OPEN)
.annotate(Sum("source_amount"))
.values("source_amount__sum")
.first()
)
return pending["source_amount__sum"] if pending else 0
@property
def url(self) -> str:
return f'<a href="{reverse("trading:commodity_detail", args=[self.id])}">{self.symbol}</a>'
class MaxCommodityError(Exception):
@@ -230,6 +287,9 @@ class TxRequest(models.Model):
# related_name="revised_request",
# )
create_time = models.DateTimeField(auto_now_add=True, editable=False)
update_time = models.DateTimeField(auto_now=True)
source = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="tx_sent", editable=False,
)

View File

@@ -1,5 +1,6 @@
{% extends "trading/base.html" %}
{% load bootstrap4 %}
{% load humanize %}
{% block title %}
{% with title="Commodity detail" %}
@@ -13,4 +14,46 @@
<h4>{{object.name}}</h4>
</div>
</div>
<div class="row">
<div class="col-sm">
<table class="table">
<tbody>
<tr>
<td scope="row">Created by</td>
<td>{{object.created_by.username}}</td>
</tr>
{% if object.symbol %}
<tr>
<td scope="row">Trading symbol</td>
<td>{{object.symbol}}</td>
</tr>
{% endif %}
<tr>
<td scope="row">In circulation</td>
<td>{{object.in_circulation|intcomma}}</td>
</tr>
<tr>
<td scope="row">Held in pending transactions</td>
<td>{{object.amount_pending|intcomma}}</td>
</tr>
<tr>
<td scope="row">Trading volume (all time)</td>
<td>{{trading_volume_all|intcomma}}</td>
</tr>
<tr>
<td scope="row">Trading volume (past 24 hours)</td>
<td>{{trading_volume_day|intcomma}}</td>
</tr>
<tr>
<td scope="row">Trading volume (past hour)</td>
<td>{{trading_volume_hour|intcomma}}</td>
</tr>
<tr>
<td scope="row">Market spread</td>
<td>{{object.market_spread|intcomma}} user{{object.market_spread|pluralize}} ({{market_spread_ratio|intcomma}}%)</td>
</tr>
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -14,10 +14,12 @@
<table class="table">
<thead>
<tr>
<th>Transaction ID</th>
<th>Commodity sent</th>
<th>Commodity received</th>
<th>Amount sent</th>
<th>Amount received</th>
<th>Status</th>
<th>ID</th>
</tr>
</thead>
<tbody>

View File

@@ -1,12 +1,18 @@
{% load humanize %}
<tr>
<td><a href="{% url "trading:tx_detail" object.id %}">{{object.id}}</a></td>
{% if object.source == request.user %}
<td>{{object.source_amount}}x {{object.source_sends.name}}</td>
<td>{{object.dest_amount}}x {{object.dest_sends.name}}</td>
<td>{{object.source_sends.url|safe}}</td>
<td>{{object.source_amount|intcomma}}</td>
<td>{{object.dest_sends.url|safe}}</td>
<td>{{object.dest_amount|intcomma}}</td>
{% else %}
<td>{{object.dest_amount}}x {{object.dest_sends.name}}</td>
<td>{{object.source_amount}}x {{object.source_sends.name}}</td>
<td>{{object.dest_sends.url|safe}}</td>
<td>{{object.dest_amount|intcomma}}</td>
<td>{{object.source_sends.url|safe}}</td>
<td>{{object.source_amount|intcomma}}</td>
{% endif %}
<td>{{object.status | title}}</td>
<td><a href="{% url "trading:tx_detail" object.id %}">{{object.id}}</a></td>
</tr>

View File

@@ -3,7 +3,7 @@
{% load humanize %}
{% block title %}
{% with title="Transaction detail" %}
{% with title="Transactions" %}
{{ block.super }}
{% endwith %}
{% endblock title %}
@@ -14,10 +14,12 @@
<table class="table">
<thead>
<tr>
<th>Transaction ID</th>
<th>Commodity sent</th>
<th>Commodity received</th>
<th>Amount sent</th>
<th>Amount received</th>
<th>Status</th>
<th>ID</th>
</tr>
</thead>
<tbody>

View File

@@ -9,11 +9,11 @@ class TestTransactions(TestCase):
user2 = User.objects.create_user(username="test2", email="test2@test.test")
# Create test commodity for user1
com1 = Commodity(created_by=user1, in_circulation=1000, name="test1")
com1 = Commodity(created_by=user1, in_circulation=1000, name="test1", symbol="TEST1")
com1.save()
# Create test commodity for user2
com2 = Commodity(created_by=user2, in_circulation=1000, name="test2")
com2 = Commodity(created_by=user2, in_circulation=1000, name="test2", symbol="TEST2")
com2.save()
# Create TX request
@@ -75,3 +75,47 @@ class TestTransactions(TestCase):
# And make sure you can still decline it after failing to accept it.
req3.decline()
class TestCommodities(TestCase):
def test_commodity_stats(self):
# Create users
user1 = User.objects.create_user(username="test1", email="test1@test.test")
user2 = User.objects.create_user(username="test2", email="test2@test.test")
# Create test commodity for user1
com1 = Commodity(created_by=user1, in_circulation=1000, name="test1", symbol="TEST1")
com1.save()
# Create test commodity for user2
com2 = Commodity(created_by=user2, in_circulation=1000, name="test2", symbol="TEST2")
com2.save()
# Ensure stats are correct
self.assertEqual(com1.amount_pending, 0)
self.assertEqual(com2.amount_pending, 0)
self.assertEqual(com1.trading_volume(), 0)
self.assertEqual(com2.trading_volume(), 0)
self.assertEqual(com1.market_spread, 1)
self.assertEqual(com2.market_spread, 1)
# Create TX request
req1 = TxRequest.open(user1, user2, com1, com2, 10, 100)
# Ensure trading volume is correct
self.assertEqual(com1.amount_pending, 10)
self.assertEqual(com2.amount_pending, 0)
self.assertEqual(com1.trading_volume(), 0)
self.assertEqual(com2.trading_volume(), 0)
self.assertEqual(com1.market_spread, 1)
self.assertEqual(com2.market_spread, 1)
# Accept trade
req1.accept()
# Ensure amount pending is correct
self.assertEqual(com1.amount_pending, 0)
self.assertEqual(com2.amount_pending, 0)
self.assertEqual(com1.trading_volume(), 10)
self.assertEqual(com2.trading_volume(), 100)
self.assertEqual(com1.market_spread, 2)
self.assertEqual(com2.market_spread, 2)

View File

@@ -69,12 +69,11 @@ urlpatterns = [
path("u/profile/<int:pk>/", UserProfileView.as_view(), name="user_profile"),
# t/ for tx
path("t/detail/<str:pk>/", TxRequestDetailView.as_view(), name="tx_detail"),
path("t/<str:pk>/", TxRequestDetailView.as_view(), name="tx_detail"),
path("t/", TxRequestListView.as_view(), name="tx_list"),
# c/ for commodities
path("c/create/", CommodityCreateView.as_view(), name="commodity_create"),
#path("c/list/", CommodityListView.as_view(), name="commodity_list"),
path("c/detail/<str:pk>/", CommodityDetailView.as_view(), name="commodity_detail"),
path("c/", CommodityListView.as_view(), name="commodity_list"),
path("c/<str:pk>/", CommodityDetailView.as_view(), name="commodity_detail"),
]

View File

@@ -12,6 +12,7 @@ from django.views.generic.edit import CreateView, UpdateView
from django.views.generic.list import ListView
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from guardian.mixins import PermissionRequiredMixin
from trading.models import User, Commodity, MaxCommodityError, TxRequest
@@ -27,6 +28,26 @@ class CommodityDetailView(DetailView):
template_name = "trading/c/detail.html"
model = Commodity
def get_context_data(self, **kwargs):
from django import utils
from datetime import timedelta
DAY = timedelta(days=1)
HOUR = timedelta(hours=1)
context = super().get_context_data(**kwargs)
now = utils.timezone.now()
obj = context["object"]
context["trading_volume_all"] = obj.trading_volume()
context["trading_volume_day"] = obj.trading_volume(now - DAY)
context["trading_volume_hour"] = obj.trading_volume(now - HOUR)
context["market_spread_ratio"] = f"{obj.market_spread_ratio*100:.2f}"
return context
class CommodityListView(ListView):
template_name = "trading/c/list.html"
model = Commodity
paginate_by = 100
class CommodityCreateView(LoginRequiredMixin, CreateView):
template_name = "trading/c/create.html"
@@ -53,9 +74,10 @@ class CommodityCreateView(LoginRequiredMixin, CreateView):
# Tx views
################################################################################
class TxRequestDetailView(DetailView):
class TxRequestDetailView(PermissionRequiredMixin, DetailView):
template_name = "trading/t/detail.html"
model = TxRequest
permission_required = "view_txrequest"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)