Include QR codes on the proposal page; added API route

This commit is contained in:
Sander Ferdinand 2018-10-20 02:11:54 +02:00
parent 75c7f49842
commit 531072d2aa
No known key found for this signature in database
GPG Key ID: 7BBC83D7A8810AAB
15 changed files with 191 additions and 21 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ settings.py
.env .env
htmlcov htmlcov
.coverage .coverage
funding/static/qr/*

View File

@ -35,12 +35,14 @@ Expose wallet via RPC.
Download application and configure. Download application and configure.
``` ```
sudo apt install python-virtualenv python3 redis-server postgresql-server postgresql-server-dev-* sudo apt install libjpeg-dev libpng-dev python-virtualenv python3 redis-server postgresql-server postgresql-server-dev-*
git clone https://github.com/skftn/wownero-wfs.git git clone https://github.com/skftn/wownero-wfs.git
cd funding cd wownero-wfs
virtualenv -p /usr/bin/python3 virtualenv -p /usr/bin/python3
source venv/bin/activate source venv/bin/activate
pip uninstall pillow
pip install -r requirements.txt pip install -r requirements.txt
CC="cc -mavx2" pip install -U --force-reinstall pillow-simd
cp settings.py_example settings.py cp settings.py_example settings.py
- change settings accordingly - change settings accordingly
``` ```
@ -55,6 +57,8 @@ python run_dev.py
Beware `run_dev.py` is meant as a development server. Beware `run_dev.py` is meant as a development server.
When running behind nginx/apache, inject `X-Forwarded-For`.
### Contributors ### Contributors
- [camthegeek](https://github.com/camthegeek) - [camthegeek](https://github.com/camthegeek)

View File

@ -1,11 +1,13 @@
from datetime import datetime from flask import jsonify, send_from_directory, Response, request
from flask import request, redirect, Response, abort, render_template, url_for, flash, make_response, send_from_directory, jsonify
from flask.ext.login import login_user , logout_user , current_user , login_required, current_user
from flask_yoloapi import endpoint, parameter from flask_yoloapi import endpoint, parameter
import settings import settings
from funding.bin.utils import get_ip
from funding.bin.qr import QrCodeGenerator
from funding.factory import app, db_session from funding.factory import app, db_session
from funding.orm.orm import Proposal, User from funding.orm.orm import Proposal, User
@app.route('/api/1/proposals') @app.route('/api/1/proposals')
@endpoint.api( @endpoint.api(
parameter('status', type=int, location='args', default=1), parameter('status', type=int, location='args', default=1),
@ -21,6 +23,7 @@ def api_proposals_get(status, cat, limit, offset):
return 'error', 500 return 'error', 500
return [p.json for p in proposals] return [p.json for p in proposals]
@app.route('/api/1/convert/wow-usd') @app.route('/api/1/convert/wow-usd')
@endpoint.api( @endpoint.api(
parameter('amount', type=int, location='args', required=True) parameter('amount', type=int, location='args', required=True)
@ -28,4 +31,33 @@ def api_proposals_get(status, cat, limit, offset):
def api_coin_usd(amount): def api_coin_usd(amount):
from funding.bin.utils import Summary, coin_to_usd from funding.bin.utils import Summary, coin_to_usd
prices = Summary.fetch_prices() prices = Summary.fetch_prices()
return jsonify(usd=coin_to_usd(amt=amount, btc_per_coin=prices['coin-btc'], usd_per_btc=prices['btc-usd'])) return jsonify(usd=coin_to_usd(amt=amount, btc_per_coin=prices['coin-btc'], usd_per_btc=prices['btc-usd']))
@app.route('/api/1/qr')
@endpoint.api(
parameter('address', type=str, location='args', required=True)
)
def api_qr_generate(address):
"""
Generate a QR image. Subject to IP throttling.
:param address: valid receiving address
:return:
"""
from funding.factory import cache
throttling_seconds = 3
ip = get_ip()
cache_key = 'qr_ip_%s' % ip
hit = cache.get(cache_key)
if hit and ip not in ['127.0.0.1', 'localhost']:
return Response('Wait a bit before generating a new QR', 403)
cache.set(cache_key, {}, throttling_seconds)
qr = QrCodeGenerator()
if not qr.exists(address):
created = qr.create(address)
if not created:
raise Exception('Could not create QR code')
return send_from_directory('static/qr', '%s.png' % address)

86
funding/bin/qr.py Normal file
View File

@ -0,0 +1,86 @@
import os
from io import BytesIO
import pyqrcode
from PIL import Image, ImageDraw
import settings
class QrCodeGenerator:
def __init__(self):
self.base = 'funding/static/qr'
self.image_size = (300, 300)
self.pil_save_options = {
'quality': 25,
'optimize': True
}
if not os.path.exists(self.base):
os.mkdir(self.base)
def exists(self, address):
if not os.path.exists(self.base):
os.mkdir(self.base)
if os.path.exists(os.path.join(self.base, '%s.png' % address)):
return True
def create(self, address, dest=None, color_from=(210, 83, 200), color_to=(255, 169, 62)):
"""
Create QR code image. Optionally a gradient.
:param address:
:param dest:
:param color_from: gradient from color
:param color_to: gradient to color
:return:
"""
if len(address) != settings.COIN_ADDRESS_LENGTH:
raise Exception('faulty address length')
if not dest:
dest = os.path.join(self.base, '%s.png' % address)
created = pyqrcode.create(address, error='L')
buffer = BytesIO()
created.png(buffer, scale=14, quiet_zone=2)
im = Image.open(buffer)
im = im.convert("RGBA")
im.thumbnail(self.image_size)
im_data = im.getdata()
# make black color transparent
im_transparent = []
for color_point in im_data:
if sum(color_point[:3]) == 255 * 3:
im_transparent.append(color_point)
else:
# get rid of the subtle grey borders
alpha = 0 if color_from and color_to else 1
im_transparent.append((0, 0, 0, alpha))
continue
if not color_from and not color_to:
im.save(dest, **self.pil_save_options)
return dest
# turn QR into a gradient
im.putdata(im_transparent)
gradient = Image.new('RGBA', im.size, color=0)
draw = ImageDraw.Draw(gradient)
for i, color in enumerate(QrCodeGenerator.gradient_interpolate(color_from, color_to, im.width * 2)):
draw.line([(i, 0), (0, i)], tuple(color), width=1)
im_gradient = Image.alpha_composite(gradient, im)
im_gradient.save(dest, **self.pil_save_options)
return dest
@staticmethod
def gradient_interpolate(color_from, color_to, interval):
det_co = [(t - f) / interval for f, t in zip(color_from, color_to)]
for i in range(interval):
yield [round(f + det * i) for f, det in zip(color_from, det_co)]

View File

@ -1,15 +1,21 @@
from datetime import datetime, date import os
import requests
from flask import g
from flask.json import JSONEncoder
import json import json
from datetime import datetime, date
import pyqrcode
import requests
from flask import g, request
from flask.json import JSONEncoder
import settings import settings
def json_encoder(obj): def json_encoder(obj):
if isinstance(obj, (datetime, date)): if isinstance(obj, (datetime, date)):
return obj.isoformat() return obj.isoformat()
raise TypeError ("Type %s not serializable" % type(obj)) raise TypeError ("Type %s not serializable" % type(obj))
class Summary: class Summary:
@staticmethod @staticmethod
def fetch_prices(): def fetch_prices():
@ -60,26 +66,35 @@ class Summary:
cache.set(cache_key, data=data, expiry=300) cache.set(cache_key, data=data, expiry=300)
return data return data
def price_cmc_btc_usd(): def price_cmc_btc_usd():
headers = {'User-Agent': 'Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0'} headers = {'User-Agent': 'Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0'}
try: try:
print('request coinmarketcap')
r = requests.get('https://api.coinmarketcap.com/v2/ticker/1/?convert=USD', headers=headers) r = requests.get('https://api.coinmarketcap.com/v2/ticker/1/?convert=USD', headers=headers)
r.raise_for_status() r.raise_for_status()
return r.json().get('data', {}).get('quotes', {}).get('USD', {}).get('price') return r.json().get('data', {}).get('quotes', {}).get('USD', {}).get('price')
except: except:
return return
def coin_btc_value(): def coin_btc_value():
headers = {'User-Agent': 'Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0'} headers = {'User-Agent': 'Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0'}
try: try:
print('request TO')
r = requests.get('https://tradeogre.com/api/v1/ticker/BTC-WOW', headers=headers) r = requests.get('https://tradeogre.com/api/v1/ticker/BTC-WOW', headers=headers)
r.raise_for_status() r.raise_for_status()
return float(r.json().get('high')) return float(r.json().get('high'))
except: except:
return return
def coin_to_usd(amt: float, usd_per_btc: float, btc_per_coin: float): def coin_to_usd(amt: float, usd_per_btc: float, btc_per_coin: float):
try: try:
return round(usd_per_btc / (1.0 / (amt * btc_per_coin)), 2) return round(usd_per_btc / (1.0 / (amt * btc_per_coin)), 2)
except: except:
pass pass
def get_ip():
return request.headers.get('X-Forwarded-For') or request.remote_addr

View File

@ -58,4 +58,4 @@ class WowCache:
return {} return {}
def set(self, key: str, data: dict, expiry = 300): def set(self, key: str, data: dict, expiry = 300):
self._cache.set(key, json.dumps(data, default=json_encoder), ex=expiry) self._cache.set(key, json.dumps(data, default=json_encoder), ex=expiry)

View File

@ -351,6 +351,7 @@ class Payout(base):
@staticmethod @staticmethod
def get_payouts(proposal_id): def get_payouts(proposal_id):
from funding.factory import db_session
return db_session.query(Payout).filter(Payout.proposal_id == proposal_id).all() return db_session.query(Payout).filter(Payout.proposal_id == proposal_id).all()

View File

@ -154,7 +154,7 @@ def proposal_api_add(title, content, pid, funds_target, addr_receiving, category
return make_response(jsonify('letters detected'),500) return make_response(jsonify('letters detected'),500)
if funds_target < 1: if funds_target < 1:
return make_response(jsonify('Proposal asking less than 1 error :)'), 500) return make_response(jsonify('Proposal asking less than 1 error :)'), 500)
if len(addr_receiving) != 97: if len(addr_receiving) != settings.COIN_ADDRESS_LENGTH:
return make_response(jsonify('Faulty address, should be of length 72'), 500) return make_response(jsonify('Faulty address, should be of length 72'), 500)
p = Proposal(headline=title, content=content, category='misc', user=current_user) p = Proposal(headline=title, content=content, category='misc', user=current_user)
@ -208,6 +208,7 @@ def user(name):
user = q.first() user = q.first()
return render_template('user.html', user=user) return render_template('user.html', user=user)
@app.route('/proposals') @app.route('/proposals')
@endpoint.api( @endpoint.api(
parameter('status', type=int, location='args', required=False), parameter('status', type=int, location='args', required=False),
@ -234,6 +235,7 @@ def proposals(status, page, cat):
return make_response(render_template('proposal/proposals.html', return make_response(render_template('proposal/proposals.html',
proposals=proposals, status=status, cat=cat)) proposals=proposals, status=status, cat=cat))
@app.route('/register', methods=['GET', 'POST']) @app.route('/register', methods=['GET', 'POST'])
def register(): def register():
if settings.USER_REG_DISABLED: if settings.USER_REG_DISABLED:

View File

@ -608,3 +608,8 @@ ul.b {
.wow_addy[data-active="true"]{ .wow_addy[data-active="true"]{
cursor: default; cursor: default;
} }
.proposal_qr{
margin-top:8px;
margin-bottom:8px;
}

View File

@ -66,12 +66,24 @@
] ]
}</pre> }</pre>
</div> </div>
<div class="api_container" id="api_qr_generation">
<h5><small>GET</small> <code>/api/1/qr</code></h5>
<hr>
<p>
WOW address to QR code generation. Returns 300x300 png.
</p>
<b>Example:</b>
<br>
<a target="_blank" href="https://funding.wownero.com/api/qr?address=WW44Zvkfv76ezeFn5M4ijAdjyCe7dK48bjaFixvyJoeyP1pmkn2MbLugchM8hV6czL4hBhjnKTP4W9HfHyNUqBch2y2R4DT3v">
link
</a>
</div>
</div> </div>
{% include 'sidebar.html' %} {% include 'sidebar.html' %}
</div> </div>
</div> </div>
<!-- /.container --> <!-- /.container -->
{% endblock %} {% endblock %}

View File

@ -95,20 +95,20 @@
<hr> <hr>
</div> </div>
<div class="col-lg-8"> <div class="col-lg-8">
{{proposal.balance['available']|round(3) or 0 }} WOW Raised {{proposal.balance['available']|round(3) or 0 }} WOW Raised
{% if (proposal.funds_target-proposal.balance['available']|float|round(3)) > 0 %} {% if (proposal.funds_target-proposal.balance['available']|float|round(3)) > 0 %}
({{ (proposal.funds_target-proposal.balance['available']|float|round(3)|int) }} WOW until goal) ({{ (proposal.funds_target-proposal.balance['available']|float|round(3)|int) }} WOW until goal)
{% else %} {% else %}
({{ (proposal.balance['available']-proposal.funds_target|float|round(3)|int) }} WOW past goal!) ({{ (proposal.balance['available']-proposal.funds_target|float|round(3)|int) }} WOW past goal!)
{% endif %} {% endif %}
<div class="progress"> <div class="progress">
<div class="progress-bar progress-warning progress-bar" style="width: {{proposal.balance['pct']}}%;"> <div class="progress-bar progress-warning progress-bar" style="width: {{proposal.balance['pct']}}%;">
</div> </div>
</div> </div>
<hr> <hr>
</div> </div>
<br/> <br/>
<br/> <br/>
<div class="col-lg-8"> <div class="col-lg-8">
{{proposal.spends['spent']|round(3) or 0}} WOW Paid out {{proposal.spends['spent']|round(3) or 0}} WOW Paid out
@ -134,6 +134,12 @@
Donation address: Donation address:
<pre class="proposal_address">{% if proposal.addr_donation %}{{ proposal.addr_donation }}{% else %}<small>None generated yet</small>{% endif %}</pre> <pre class="proposal_address">{% if proposal.addr_donation %}{{ proposal.addr_donation }}{% else %}<small>None generated yet</small>{% endif %}</pre>
</div> </div>
{% if proposal.addr_donation %}
<div class="col-lg-12">
<img class="proposal_qr" src="/api/1/qr?address={{ proposal.addr_donation }}">
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,6 +2,7 @@ def val_username(value):
if len(value) >= 20: if len(value) >= 20:
raise Exception("username too long") raise Exception("username too long")
def val_email(value): def val_email(value):
if len(value) >= 50: if len(value) >= 50:
raise Exception("email too long") raise Exception("email too long")

View File

@ -8,4 +8,7 @@ redis
gunicorn gunicorn
psycopg2 psycopg2
markdown2 markdown2
requests requests
pyqrcode
pypng
pillow-simd

View File

@ -1,6 +1,7 @@
from funding.factory import create_app from funding.factory import create_app
import settings import settings
if __name__ == '__main__': if __name__ == '__main__':
app = create_app() app = create_app()
app.run(host=settings.BIND_HOST, port=settings.BIND_PORT, app.run(host=settings.BIND_HOST, port=settings.BIND_PORT,

View File

@ -7,6 +7,7 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SECRET = '' SECRET = ''
DEBUG = True DEBUG = True
COIN_ADDRESS_LENGTH = 97
COINCODE = '' COINCODE = ''
PSQL_USER = '' PSQL_USER = ''
PSQL_PASS = '' PSQL_PASS = ''