summaryrefslogtreecommitdiffstats
path: root/bitvend
diff options
context:
space:
mode:
authorPiotr Dobrowolski <admin@tastycode.pl>2017-04-05 20:15:50 +0200
committerPiotr Dobrowolski <admin@tastycode.pl>2017-04-05 20:15:50 +0200
commit15428910fa8c4037f7d80bd122ed86dbbc5b451b (patch)
tree6b69f5f3d5c2557e198071ed084dd8f1832f932c /bitvend
parent32caeed871a88c7e6fbb63c38cf453e5dec3a0f8 (diff)
downloadbitvend-15428910fa8c4037f7d80bd122ed86dbbc5b451b.tar.gz
bitvend-15428910fa8c4037f7d80bd122ed86dbbc5b451b.tar.bz2
bitvend-15428910fa8c4037f7d80bd122ed86dbbc5b451b.tar.xz
bitvend-15428910fa8c4037f7d80bd122ed86dbbc5b451b.zip
Transfers implementation
Diffstat (limited to 'bitvend')
-rw-r--r--bitvend/__init__.py2
-rw-r--r--bitvend/forms.py39
-rw-r--r--bitvend/models.py6
-rw-r--r--bitvend/templates/_formathelpers.html13
-rw-r--r--bitvend/templates/_formhelpers.html58
-rw-r--r--bitvend/templates/base.html14
-rw-r--r--bitvend/templates/index.html73
-rw-r--r--bitvend/views.py38
8 files changed, 221 insertions, 22 deletions
diff --git a/bitvend/__init__.py b/bitvend/__init__.py
index 5b1f4f9..8e4fb77 100644
--- a/bitvend/__init__.py
+++ b/bitvend/__init__.py
@@ -34,6 +34,8 @@ def create_app():
'sat_to_btc': sat_to_btc,
'qrcode': lambda data: flask.url_for('bitvend.qrcode_gen', data=data),
'current_transaction': Transaction.query.filter(Transaction.finished == False).first(),
+ 'mdb_online': dev.online,
+ 'proc_online': proc.online,
}
return app
diff --git a/bitvend/forms.py b/bitvend/forms.py
new file mode 100644
index 0000000..1f19577
--- /dev/null
+++ b/bitvend/forms.py
@@ -0,0 +1,39 @@
+from flask_wtf import FlaskForm
+from wtforms import StringField
+from bitvend.models import User
+from wtforms.fields.core import DecimalField
+from wtforms.validators import DataRequired, NumberRange, ValidationError
+from decimal import Decimal, InvalidOperation
+
+
+class DecimalUnityField(DecimalField):
+ unity = 100
+
+ def __init__(self, label=None, validators=None, unity=100, **kwargs):
+ super(DecimalUnityField, self).__init__(label, validators, **kwargs)
+ self.unity = unity
+
+ def _value(self):
+ if self.data is not None:
+ format = '%%0.%df' % self.places
+ return (format % (Decimal(self.data) / self.unity,))
+ elif self.raw_data:
+ return self.raw_data[0]
+
+ def process_formdata(self, valuelist):
+ if valuelist:
+ try:
+ self.data = int(Decimal(valuelist[0]) * self.unity)
+ except InvalidOperation:
+ self.data = None
+
+def UserExists(form, field):
+ if not User.query.get(field.data):
+ raise ValidationError('User does not exist.')
+
+class TransferForm(FlaskForm):
+ target = StringField("Target user", validators=[
+ DataRequired(), UserExists])
+ amount = DecimalUnityField("Amount", default=0, validators=[
+ NumberRange(min=1),
+ ])
diff --git a/bitvend/models.py b/bitvend/models.py
index dbf6567..bab5836 100644
--- a/bitvend/models.py
+++ b/bitvend/models.py
@@ -32,14 +32,14 @@ class User(db.Model):
)
def transfer(self, target, amount):
- if self.balance - amount < -self.debt_limit:
+ if amount > self.amount_available:
raise NoFunds()
self.transactions.append(Transaction(
- amount=-amount, type='transfer'
+ amount=-amount, type='transfer', related=target.uid
))
target.transactions.append(Transaction(
- amount=amount, type='transfer'
+ amount=amount, type='transfer', related=self.uid
))
@property
diff --git a/bitvend/templates/_formathelpers.html b/bitvend/templates/_formathelpers.html
new file mode 100644
index 0000000..eafaf70
--- /dev/null
+++ b/bitvend/templates/_formathelpers.html
@@ -0,0 +1,13 @@
+{% macro format_currency(amount, color=True, precision=2) -%}
+{%- if amount == None -%}
+None
+{%- else -%}
+<span class="amount{% if amount < 0 and color %} amount-negative{% endif %}" data-original="{{ amount }}">
+ {{ format_currency_raw(amount, precision) }}
+</span>
+{%- endif %}
+{%- endmacro %}
+
+{% macro format_currency_raw(amount, precision=0) -%}
+{{ ("%%.%sf" | format(precision) | format(amount/100)) }}SOG
+{%- endmacro %}
diff --git a/bitvend/templates/_formhelpers.html b/bitvend/templates/_formhelpers.html
new file mode 100644
index 0000000..ac037cb
--- /dev/null
+++ b/bitvend/templates/_formhelpers.html
@@ -0,0 +1,58 @@
+{% macro render_field(field, prefix=None, suffix=None, layout=True, label=True) %}
+ {% if field.type == 'HiddenField' %}
+ {{ field(**kwargs) }}
+ {% else %}
+ {% if layout %}
+ <div class="form-group{% if field.errors %} has-error{% endif %}">
+ {% if field.type == 'BooleanField' %}
+ <div class="col-xs-3"></div>
+ {% elif label %}
+ {{ field.label(class_='col-xs-3 control-label') }}
+ {% endif %}
+ <div class="col-xs-9">
+ {% endif %}
+
+ {{ render_field_inner(field, prefix, suffix, label=label, **kwargs) }}
+
+ {% if layout %}
+ </div>
+ </div>
+ {% endif %}
+ {% endif %}
+{% endmacro %}
+
+{% macro render_field_inner(field, prefix=None, suffix=None, label=True, input_group_class='') %}
+ {% if field.type == 'BooleanField' %}<div class="checkbox"><label for="{{ field.id }}">{% endif %}
+ {% if prefix or suffix %}<div class="input-group {{ input_group_class }}">{% endif %}
+ {% if prefix %}<span class="input-group-addon">{{ prefix }}</span>{% endif %}
+ {% if field.type == 'BooleanField' %}
+ {{ field(**kwargs) }} {% if label %}{{ field.label.text }}{% endif %}
+ {% elif field.type == 'RadioField' %}
+ {{ field(**kwargs) }}
+ {% else %}
+ {{ field(class_='form-control '+kwargs.pop('class_', ''), **kwargs) }}
+ {% endif %}
+ {% if suffix %}<span class="input-group-addon">{{ suffix }}</span>{% endif %}
+ {% if prefix or suffix %}</div>{% endif %}
+ {% if field.description and label %}
+ <span class="help-block">{{ field.description }}</span>
+ {% endif %}
+ {% if field.errors %}
+ {% for error in field.errors %}
+ <span class="help-block">{{ error }}</span>
+ {% endfor %}
+ {% endif %}
+ {% if field.type == 'BooleanField' %}</label></div>{% endif %}
+{% endmacro %}
+
+{% macro render_submit(label='Submit', class_='btn btn-primary', layout=True) %}
+{% if layout %}
+<div class="form-group">
+ <div class="col-xs-9 col-xs-offset-3">
+{% endif %}
+ <button type="submit" class="{{ class_ }}">{{ label }}</button>
+{% if layout %}
+ </div>
+</div>
+{% endif %}
+{% endmacro %}
diff --git a/bitvend/templates/base.html b/bitvend/templates/base.html
index f24d165..ba5d6f5 100644
--- a/bitvend/templates/base.html
+++ b/bitvend/templates/base.html
@@ -13,9 +13,10 @@
<![endif]-->
<style>
-body {
- margin-top: 80px;
-}
+ body {
+ margin-top: 80px;
+ }
+
h3 {
margin: 0;
padding: 0;
@@ -27,6 +28,13 @@ body {
.well h3 { padding: 0; }
.well h3 small { padding-top: 5px; }
+ .amount-negative { color: #990000; }
+ h3.page-header {
+ margin-top: 10px;
+ }
+ .input-group-btn .btn {
+ padding-bottom: 8px;
+ }
</style>
</head>
<body>
diff --git a/bitvend/templates/index.html b/bitvend/templates/index.html
index dc01175..b7e9840 100644
--- a/bitvend/templates/index.html
+++ b/bitvend/templates/index.html
@@ -1,6 +1,10 @@
{% extends "base.html" %}
+
+{% from "_formathelpers.html" import format_currency %}
+{% from "_formhelpers.html" import render_field, render_submit %}
+
{% block content %}
- {% if false and not mdb_online or not proc_online %}
+ {% if not mdb_online or not proc_online %}
<div class="alert alert-warning">
<b>Some of the subsystems are misbehaving.</b> Please avoid making payments, unless in absolute need of Mate.
</div>
@@ -8,19 +12,15 @@
{% if current_user.is_authenticated %}
<div class="row">
- <div class="col-sm-4 col-sm-offset-2">
+ <div class="col-sm-4">
<div class="well text-right">
- <h3><small class="pull-left">Balance</small> <small>{{ current_user.amount_available }} / </small> {{ current_user.balance }}</h3>
+ <h3><small class="pull-left">Balance</small> <small>{{ format_currency(current_user.amount_available) }} / </small> {{ format_currency(current_user.balance) }}</h3>
</div>
- </div>
- <div class="col-sm-4">
+
<div class="well text-right">
- <h3><small class="pull-left">Purchases</small> {{ current_user.transactions.count() }}</h3>
+ <h3><small class="pull-left">Transactions</small> {{ current_user.transactions.count() }}</h3>
</div>
- </div>
- </div>
- <div class="row">
- <div class="col-sm-6 col-sm-offset-3">
+ <div class="well">
{% if current_transaction %}
<a href="{{ url_for('.cancel') }}" class="btn btn-danger btn-block btn-lg">Cancel transaction
{% if current_transaction.user != current_user %}
@@ -29,7 +29,55 @@
</a>
{% else %}
<a href="{{ url_for('.begin') }}" class="btn btn-primary btn-block btn-lg">Begin transaction</a>
- {% endif %}
+ {% endif %}
+ </div>
+ <form action="{{ url_for('.transfer') }}" method="POST" class="well">
+ {{ transfer_form.hidden_tag() }}
+ <div class="form-group">
+ <input type="input" class="form-control" name="target" placeholder="target username" />
+ </div>
+ <div class="form-group">
+ <div class="input-group">
+ <input type="number" class="form-control" min="0" name="amount" placeholder="0.00" />
+ <span class="input-group-btn">
+ <button class="btn btn-info">Transfer</button>
+ </span>
+ </div>
+ </div>
+ </form>
+ </div>
+ <div class="col-sm-8">
+ <h3 class="page-header">Latest transactions</h3>
+ <table class="table table-hover table-striped">
+ <thead>
+ <tr>
+ <th>Type</th>
+ <th>Amount</th>
+ <th>Date</th>
+ </tr>
+ </thead>
+
+ {% for tx in transactions %}
+ <tr{% if not tx.finished %} style="opacity: 0.5"{% endif %}>
+ <td>
+ {{ tx.type }} {% if not tx.finished %}<i>(processing)</i>{% endif %}
+ <small>
+ {% if tx.type == 'transfer' and tx.amount > 0 %}
+ from <b>{{ tx.related_user }}</b>
+ {% elif tx.type == 'transfer' and tx.amount < 0 %}
+ to <b>{{ tx.related }}</b>
+ {% elif tx.type == 'purchase' and tx.product_id %}
+ of product <b>{{ tx.product_id }}</b>
+ {% endif %}
+ </small>
+ </td>
+ <td>{{ format_currency(tx.amount) }}</td>
+ <td>{{ tx.created }}</td>
+ </tr>
+ {% else %}
+ <tr><td colspan=3 class="placeholder">Nothing to see here...</td></tr>
+ {% endfor %}
+ </table>
</div>
</div>
<hr>
@@ -39,6 +87,7 @@
This is just a test deployment of Warsaw Hackerspace Vending Machine Bitcoin Payments System™.<br />
<b>Please report any issues to <a href="mailto:informatic@hackerspace.pl" class="alert-link">informatic@hackerspace.pl</a>.</b>
</div>
+
<div class="row">
{% for item in items %}
<div class="col-sm-6">
@@ -46,7 +95,7 @@
<div class="row">
<div class="col-md-12">
<div class="pull-right">
- <span class="label label-info">{{ '%.2fzł'|format(item.value/100) }}</span>
+ <span class="label label-info">{{ format_currency(item.value) }}</span>
<span class="label label-primary">{{ format_btc(from_local_currency(item.value*1.03)) }}</span>
</div>
<h3>{{ item.name }}</h3>
diff --git a/bitvend/views.py b/bitvend/views.py
index 8cb1e2d..2ce73a5 100644
--- a/bitvend/views.py
+++ b/bitvend/views.py
@@ -6,8 +6,9 @@ import qrcode
import qrcode.image.svg
from bitvend import dev, proc
-from bitvend.models import db, Transaction
+from bitvend.models import db, User, Transaction, NoFunds
from bitvend.auth import try_login
+from bitvend.forms import TransferForm
from flask_login import login_required, current_user, logout_user
@@ -15,10 +16,35 @@ bp = Blueprint('bitvend', __name__, template_folder='templates')
@bp.route('/')
def index():
+ transactions = []
+
+ if current_user.is_authenticated:
+ transactions = current_user.transactions.order_by(Transaction.created.desc()).limit(10)
+
return render_template(
- 'index.html', items=app.config['ITEMS'],
- mdb_online=dev.online,
- proc_online=proc.online)
+ 'index.html',
+ items=app.config['ITEMS'],
+ transactions=transactions,
+ transfer_form=TransferForm(),
+ )
+
+@bp.route('/transfer', methods=['GET', 'POST'])
+def transfer():
+ transfer_form = TransferForm()
+
+ if transfer_form.validate_on_submit():
+ print(transfer_form.amount)
+ try:
+ current_user.transfer(User.query.get(transfer_form.target.data), transfer_form.amount.data)
+ db.session.commit()
+ flash('Transfer succeeded.', 'success')
+ except NoFunds:
+ flash('No funds.', 'danger')
+ return redirect(url_for('.index'))
+
+ flash('; '.join(sum(transfer_form.errors.values(), [])), 'danger')
+
+ return redirect(url_for('.index'))
@bp.route('/login')
def login():
@@ -52,6 +78,10 @@ def begin():
flash('Nope xD', 'danger')
return redirect(url_for('.index'))
+ if current_user.amount_available <= 0:
+ flash('Nope xD', 'danger')
+ return redirect(url_for('.index'))
+
tx = Transaction(type='purchase')
current_user.transactions.append(tx)
db.session.commit()