From 401c26e85b1033d54d8826e688037c4dab28b8cf Mon Sep 17 00:00:00 2001
From: dsc
Date: Tue, 9 Jun 2020 04:32:51 +0200
Subject: [PATCH] - Switch to integrated addresses (address+payment_id) ->
`make_integrated_address @ RPC) - Changed homegrown caching solution to use
flask extension `flask_cache` instead - Remove detection of out-going (sent)
funds, needs to be manually registered in table "payouts" now - Updated Flask
version
---
funding/api.py | 2 +-
funding/bin/qr.py | 4 +-
funding/bin/utils.py | 40 +--
funding/bin/utils_request.py | 12 +-
funding/cache.py | 14 -
funding/factory.py | 70 +++-
funding/orm/connect.py | 18 -
funding/orm/orm.py | 326 +++++++++---------
funding/routes.py | 59 ++--
funding/routes_admin.py | 4 +-
funding/static/css/wow2.css | 4 -
funding/templates/about.html | 2 +-
funding/templates/base.html | 2 +-
funding/templates/login.html | 7 +-
.../proposal/macros/transaction.html | 26 +-
funding/templates/proposal/proposal.html | 17 +-
requirements.txt | 5 +-
settings.py_example | 7 +-
18 files changed, 311 insertions(+), 308 deletions(-)
delete mode 100644 funding/orm/connect.py
diff --git a/funding/api.py b/funding/api.py
index 95ee6b7..d898199 100644
--- a/funding/api.py
+++ b/funding/api.py
@@ -5,7 +5,7 @@ from flask_yoloapi import endpoint, parameter
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
from funding.orm.orm import Proposal, User
diff --git a/funding/bin/qr.py b/funding/bin/qr.py
index 4d7a1f4..e369ce2 100644
--- a/funding/bin/qr.py
+++ b/funding/bin/qr.py
@@ -34,8 +34,8 @@ class QrCodeGenerator:
:param color_to: gradient to color
:return:
"""
- if len(address) != settings.COIN_ADDRESS_LENGTH:
- raise Exception('faulty address length')
+ if len(address) not in settings.COIN_ADDRESS_LENGTH:
+ raise Exception(f'faulty address length, should be: {" or ".join(map(str, settings.COIN_ADDRESS_LENGTH))}')
if not dest:
dest = os.path.join(self.base, '%s.png' % address)
diff --git a/funding/bin/utils.py b/funding/bin/utils.py
index 0602b91..4caf0b4 100644
--- a/funding/bin/utils.py
+++ b/funding/bin/utils.py
@@ -8,6 +8,7 @@ from flask import g, request
from flask.json import JSONEncoder
import settings
+from funding.factory import cache
def json_encoder(obj):
@@ -18,62 +19,50 @@ def json_encoder(obj):
class Summary:
@staticmethod
+ @cache.cached(timeout=300, key_prefix="fetch_prices")
def fetch_prices():
- if hasattr(g, 'funding_prices') and g.coin_prices:
- return g.coin_prices
- from funding.factory import cache
- cache_key = 'funding_prices'
- data = cache.get(cache_key)
- if data:
- return data
- data = {
+ return {
'coin-btc': coin_btc_value(),
'btc-usd': price_cmc_btc_usd()
}
- cache.set(cache_key, data=data, expiry=1200)
- g.coin_prices = data
- return data
@staticmethod
- def fetch_stats(purge=False):
- from funding.factory import db_session
+ @cache.cached(timeout=300, key_prefix="funding_stats")
+ def fetch_stats():
+ from funding.factory import db
from funding.orm.orm import Proposal, User, Comment
- from funding.factory import cache
- cache_key = 'funding_stats'
- data = cache.get(cache_key)
- if data and not purge:
- return data
+ data = {}
categories = settings.FUNDING_CATEGORIES
statuses = settings.FUNDING_STATUSES.keys()
for cat in categories:
- q = db_session.query(Proposal)
+ q = db.session.query(Proposal)
q = q.filter(Proposal.category == cat)
res = q.count()
data.setdefault('cats', {})
data['cats'][cat] = res
for status in statuses:
- q = db_session.query(Proposal)
+ q = db.session.query(Proposal)
q = q.filter(Proposal.status == status)
res = q.count()
data.setdefault('statuses', {})
data['statuses'][status] = res
data.setdefault('users', {})
- data['users']['count'] = db_session.query(User.id).count()
- cache.set(cache_key, data=data, expiry=300)
+ data['users']['count'] = db.session.query(User.id).count()
return data
def price_cmc_btc_usd():
headers = {'User-Agent': 'Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0'}
try:
- print('request coinmarketcap')
- r = requests.get('https://api.coinmarketcap.com/v2/ticker/1/?convert=USD', headers=headers)
+ r = requests.get('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd', headers=headers)
r.raise_for_status()
- return r.json().get('data', {}).get('quotes', {}).get('USD', {}).get('price')
+ data = r.json()
+ btc = next(c for c in data if c['symbol'] == 'btc')
+ return btc['current_price']
except:
return
@@ -81,7 +70,6 @@ def price_cmc_btc_usd():
def coin_btc_value():
headers = {'User-Agent': 'Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0'}
try:
- print('request TO')
r = requests.get('https://tradeogre.com/api/v1/ticker/BTC-WOW', headers=headers)
r.raise_for_status()
return float(r.json().get('high'))
diff --git a/funding/bin/utils_request.py b/funding/bin/utils_request.py
index 26f1dab..f285e8c 100644
--- a/funding/bin/utils_request.py
+++ b/funding/bin/utils_request.py
@@ -2,16 +2,16 @@ from datetime import datetime
from flask import session, g, request
import settings
from funding.bin.utils import Summary
-from funding.factory import app, db_session
+from funding.factory import app, db
from funding.orm.orm import Proposal, User, Comment
@app.context_processor
def templating():
- from flask.ext.login import current_user
- recent_comments = db_session.query(Comment).filter(Comment.automated == False).order_by(Comment.date_added.desc()).limit(8).all()
+ from flask_login import current_user
+ recent_comments = db.session.query(Comment).filter(Comment.automated == False).order_by(Comment.date_added.desc()).limit(8).all()
summary_data = Summary.fetch_stats()
- newest_users = db_session.query(User).filter(User.admin == False).order_by(User.registered_on.desc()).limit(5).all()
+ newest_users = db.session.query(User).filter(User.admin == False).order_by(User.registered_on.desc()).limit(5).all()
return dict(logged_in=current_user.is_authenticated,
current_user=current_user,
funding_categories=settings.FUNDING_CATEGORIES,
@@ -28,8 +28,6 @@ def before_request():
@app.after_request
def after_request(res):
- if hasattr(g, 'funding_prices'):
- delattr(g, 'funding_prices')
res.headers.add('Accept-Ranges', 'bytes')
if request.full_path.startswith('/api/'):
@@ -45,7 +43,7 @@ def after_request(res):
@app.teardown_appcontext
def shutdown_session(**kwargs):
- db_session.remove()
+ db.session.remove()
@app.errorhandler(404)
diff --git a/funding/cache.py b/funding/cache.py
index b5c308f..e303c2e 100644
--- a/funding/cache.py
+++ b/funding/cache.py
@@ -45,17 +45,3 @@ class JsonRedis(RedisSessionInterface):
redis=redis.Redis(**redis_args()),
key_prefix=key_prefix,
use_signer=use_signer)
-
-
-class WowCache:
- def __init__(self):
- self._cache = redis.StrictRedis(**redis_args())
-
- def get(self, key):
- try:
- return json.loads(self._cache.get(key))
- except:
- return {}
-
- def set(self, key: str, data: dict, expiry=300):
- self._cache.set(key, json.dumps(data, default=json_encoder), ex=expiry)
diff --git a/funding/factory.py b/funding/factory.py
index 9fa6fa9..fffce45 100644
--- a/funding/factory.py
+++ b/funding/factory.py
@@ -1,40 +1,79 @@
# -*- coding: utf-8 -*-
import settings
-from werkzeug.contrib.fixers import ProxyFix
from flask import Flask
+from flask_caching import Cache
+from flask_session import Session
+from flask_sqlalchemy import SQLAlchemy
app = None
-sentry = None
cache = None
-db_session = None
+db = None
bcrypt = None
+def _setup_cache(app: Flask):
+ global cache
+
+ cache_config = {
+ "CACHE_TYPE": "redis",
+ "CACHE_DEFAULT_TIMEOUT": 60,
+ "CACHE_KEY_PREFIX": "wow_cache_"
+ }
+
+ app.config.from_mapping(cache_config)
+ cache = Cache(app)
+
+
+def _setup_session(app: Flask):
+ app.config['SESSION_TYPE'] = 'redis'
+ app.config['SESSION_COOKIE_NAME'] = 'bar'
+ Session(app) # defaults to timedelta(days=31)
+
+
+def _setup_db(app: Flask):
+ global db
+
+ DB_URL = 'postgresql+psycopg2://{user}:{pw}@{url}/{db}'.format(
+ user=settings.PSQL_USER,
+ pw=settings.PSQL_PASS,
+ url=settings.PSQL_HOST,
+ db=settings.PSQL_DB)
+ app.config['SQLALCHEMY_DATABASE_URI'] = DB_URL
+ db = SQLAlchemy(app)
+
+ import funding.orm
+
+ with app.app_context():
+ db.create_all()
+ db.session.commit()
+
+
def create_app():
global app
- global db_session
- global sentry
+ global db
global cache
global bcrypt
- from funding.orm.connect import create_session
- db_session = create_session()
-
- app = Flask(__name__)
- app.wsgi_app = ProxyFix(app.wsgi_app)
+ app = Flask(import_name=__name__,
+ static_folder='static',
+ template_folder='templates')
app.config.from_object(settings)
app.config['PERMANENT_SESSION_LIFETIME'] = 2678400
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 30
app.secret_key = settings.SECRET
+ _setup_cache(app)
+ _setup_session(app)
+ _setup_db(app)
+
# flask-login
- from flask.ext.login import LoginManager
+ from flask_login import LoginManager
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
- from flask.ext.bcrypt import Bcrypt
+ from flask_bcrypt import Bcrypt
bcrypt = Bcrypt(app)
@login_manager.user_loader
@@ -42,15 +81,10 @@ def create_app():
from funding.orm.orm import User
return User.query.get(int(_id))
- # session init
- from funding.cache import JsonRedis, WowCache
- app.session_interface = JsonRedis(key_prefix=app.config['SESSION_PREFIX'], use_signer=False)
- cache = WowCache()
-
# import routes
from funding import routes
from funding import api
from funding.bin import utils_request
app.app_context().push()
- return app
\ No newline at end of file
+ return app
diff --git a/funding/orm/connect.py b/funding/orm/connect.py
deleted file mode 100644
index 842eaca..0000000
--- a/funding/orm/connect.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from datetime import datetime
-
-import sqlalchemy as sa
-from sqlalchemy.orm import scoped_session, sessionmaker, relationship
-from sqlalchemy.ext.declarative import declarative_base
-
-import settings
-
-
-def create_session():
- from funding.orm.orm import base
- engine = sa.create_engine(settings.SQLALCHEMY_DATABASE_URI, echo=False, encoding="latin")
- session = scoped_session(sessionmaker(autocommit=False,
- autoflush=False,
- bind=engine))
- base.query = session.query_property()
- base.metadata.create_all(bind=engine)
- return session
\ No newline at end of file
diff --git a/funding/orm/orm.py b/funding/orm/orm.py
index 7d7b5e1..86ec16e 100644
--- a/funding/orm/orm.py
+++ b/funding/orm/orm.py
@@ -1,19 +1,29 @@
from datetime import datetime
+import string
+import random
+
+import requests
+from sqlalchemy.orm import relationship, backref
import sqlalchemy as sa
from sqlalchemy.orm import scoped_session, sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.types import Float
+from sqlalchemy_json import MutableJson
+
import settings
+from funding.factory import db
base = declarative_base(name="Model")
-class User(base):
+
+class User(db.Model):
__tablename__ = "users"
- id = sa.Column('user_id', sa.Integer, primary_key=True)
- username = sa.Column(sa.String(20), unique=True, index=True)
- password = sa.Column(sa.String(60))
- email = sa.Column(sa.String(50), unique=True, index=True)
- registered_on = sa.Column(sa.DateTime)
- admin = sa.Column(sa.Boolean, default=False)
+ id = db.Column('user_id', db.Integer, primary_key=True)
+ username = db.Column(db.String(20), unique=True, index=True)
+ password = db.Column(db.String(60))
+ email = db.Column(db.String(50), unique=True, index=True)
+ registered_on = db.Column(db.DateTime)
+ admin = db.Column(db.Boolean, default=False)
proposals = relationship('Proposal', back_populates="user")
comments = relationship("Comment", back_populates="user")
@@ -48,7 +58,7 @@ class User(base):
@classmethod
def add(cls, username, password, email):
- from funding.factory import db_session
+ from funding.factory import db
from funding.validation import val_username, val_email
try:
@@ -57,37 +67,39 @@ class User(base):
val_email(email)
user = User(username, password, email)
- db_session.add(user)
- db_session.commit()
- db_session.flush()
+ db.session.add(user)
+ db.session.commit()
+ db.session.flush()
return user
except Exception as ex:
- db_session.rollback()
+ db.session.rollback()
raise
-class Proposal(base):
+class Proposal(db.Model):
__tablename__ = "proposals"
- id = sa.Column(sa.Integer, primary_key=True)
- headline = sa.Column(sa.VARCHAR, nullable=False)
- content = sa.Column(sa.VARCHAR, nullable=False)
- category = sa.Column(sa.VARCHAR, nullable=False)
- date_added = sa.Column(sa.TIMESTAMP, default=datetime.now)
- html = sa.Column(sa.VARCHAR)
- last_edited = sa.Column(sa.TIMESTAMP)
+ id = db.Column(db.Integer, primary_key=True)
+ archived = db.Column(db.Boolean, default=False)
+ headline = db.Column(db.VARCHAR, nullable=False)
+ content = db.Column(db.VARCHAR, nullable=False)
+ category = db.Column(db.VARCHAR, nullable=False)
+ date_added = db.Column(db.TIMESTAMP, default=datetime.now)
+ html = db.Column(db.VARCHAR)
+ last_edited = db.Column(db.TIMESTAMP)
# the FFS target
- funds_target = sa.Column(sa.Float, nullable=False)
+ funds_target = db.Column(db.Float, nullable=False)
# the FFS progress (cached)
- funds_progress = sa.Column(sa.Float, nullable=False, default=0)
+ funds_progress = db.Column(db.Float, nullable=False, default=0)
# the FFS withdrawal amount (paid to the author)
- funds_withdrew = sa.Column(sa.Float, nullable=False, default=0)
+ funds_withdrew = db.Column(db.Float, nullable=False, default=0)
# the FFS receiving and withdrawal addresses
- addr_donation = sa.Column(sa.VARCHAR)
- addr_receiving = sa.Column(sa.VARCHAR)
+ addr_donation = db.Column(db.VARCHAR)
+ addr_receiving = db.Column(db.VARCHAR)
+ payment_id = db.Column(db.VARCHAR)
# proposal status:
# 0: disabled
@@ -95,9 +107,9 @@ class Proposal(base):
# 2: funding required
# 3: wip
# 4: completed
- status = sa.Column(sa.INTEGER, default=1)
+ status = db.Column(db.INTEGER, default=1)
- user_id = sa.Column(sa.Integer, sa.ForeignKey('users.user_id'))
+ user_id = db.Column(db.Integer, db.ForeignKey('users.user_id'))
user = relationship("User", back_populates="proposals")
payouts = relationship("Payout", back_populates="proposal")
@@ -131,17 +143,12 @@ class Proposal(base):
@classmethod
def find_by_id(cls, pid: int):
- from funding.factory import db_session
+ from funding.factory import db
q = cls.query
q = q.filter(Proposal.id == pid)
result = q.first()
if not result:
return
-
- # check if we have a valid addr_donation generated. if not, make one.
- if not result.addr_donation and result.status == 2:
- from funding.bin.daemon import Daemon
- Proposal.generate_donation_addr(result)
return result
@property
@@ -154,21 +161,21 @@ class Proposal(base):
@property
def comment_count(self):
- from funding.factory import db_session
- q = db_session.query(sa.func.count(Comment.id))
+ from funding.factory import db
+ q = db.session.query(db.func.count(Comment.id))
q = q.filter(Comment.proposal_id == self.id)
return q.scalar()
def get_comments(self):
- from funding.factory import db_session
- q = db_session.query(Comment)
+ from funding.factory import db
+ q = db.session.query(Comment)
q = q.filter(Comment.proposal_id == self.id)
- q = q.filter(Comment.replied_to == None)
+ q = q.filter(Comment.replied_to.is_(None))
q = q.order_by(Comment.date_added.desc())
comments = q.all()
for c in comments:
- q = db_session.query(Comment)
+ q = db.session.query(Comment)
q = q.filter(Comment.proposal_id == self.id)
q = q.filter(Comment.replied_to == c.id)
_c = q.all()
@@ -177,6 +184,12 @@ class Proposal(base):
setattr(self, '_comments', comments)
return self
+ @property
+ def spends(self):
+ amount = sum([p.amount for p in self.payouts])
+ pct = amount / 100 * self.balance['sum']
+ return {"amount": amount, "pct": pct}
+
@property
def balance(self):
"""This property retrieves the current funding status
@@ -184,28 +197,49 @@ class Proposal(base):
daemon too much. Returns a nice dictionary containing
all relevant proposal funding info"""
from funding.bin.utils import Summary, coin_to_usd
- from funding.factory import cache, db_session
- rtn = {'sum': 0.0, 'txs': [], 'pct': 0.0}
+ from funding.factory import cache, db
+ rtn = {'sum': 0.0, 'txs': [], 'pct': 0.0, 'available': 0}
- cache_key = 'coin_balance_pid_%d' % self.id
- data = cache.get(cache_key)
- if not data:
- from funding.bin.daemon import Daemon
- try:
- data = Daemon().get_transfers_in(proposal=self)
- if not isinstance(data, dict):
- print('error; get_transfers_in; %d' % self.id)
- return rtn
- cache.set(cache_key, data=data, expiry=60)
- except Exception as ex:
- print('error; get_transfers_in; %d' % self.id)
- return rtn
+ if self.archived:
+ return rtn
+
+ try:
+ r = requests.get(f'http://{settings.RPC_HOST}:{settings.RPC_PORT}/json_rpc', json={
+ "jsonrpc": "2.0",
+ "id": "0",
+ "method": "get_payments",
+ "params": {
+ "payment_id": self.payment_id
+ }
+ })
+ r.raise_for_status()
+ blob = r.json()
+
+ assert 'result' in blob
+ assert 'payments' in blob['result']
+ assert isinstance(blob['result']['payments'], list)
+ except Exception as ex:
+ return rtn
+
+ txs = blob['result']['payments']
+ for tx in txs:
+ tx['amount_human'] = float(tx['amount'])/1e11
+ tx['txid'] = tx['tx_hash']
+ tx['type'] = 'in'
+
+ data = {
+ 'sum': sum([float(z['amount']) / 1e11 for z in txs]),
+ 'txs': txs
+ }
+
+ if not isinstance(data, dict):
+ print('error; get_transfers_in; %d' % self.id)
+ return rtn
prices = Summary.fetch_prices()
for tx in data['txs']:
if prices:
tx['amount_usd'] = coin_to_usd(amt=tx['amount_human'], btc_per_coin=prices['coin-btc'], usd_per_btc=prices['btc-usd'])
- tx['datetime'] = datetime.fromtimestamp(tx['timestamp'])
if data.get('sum', 0.0):
data['pct'] = 100 / float(self.funds_target / data.get('sum', 0.0))
@@ -216,8 +250,8 @@ class Proposal(base):
if data['pct'] != self.funds_progress:
self.funds_progress = data['pct']
- db_session.commit()
- db_session.flush()
+ db.session.commit()
+ db.session.flush()
if data['available']:
data['remaining_pct'] = 100 / float(data['sum'] / data['available'])
@@ -226,74 +260,9 @@ class Proposal(base):
return data
- @property
- def spends(self):
- from funding.bin.utils import Summary, coin_to_usd
- from funding.factory import cache, db_session
- rtn = {'sum': 0.0, 'txs': [], 'pct': 0.0}
-
- cache_key = 'coin_spends_pid_%d' % self.id
- data = cache.get(cache_key)
- if not data:
- from funding.bin.daemon import Daemon
- try:
- data = Daemon().get_transfers_out(proposal=self)
- if not isinstance(data, dict):
- print('error; get_transfers_out; %d' % self.id)
- return rtn
- cache.set(cache_key, data=data, expiry=60)
- except:
- print('error; get_transfers_out; %d' % self.id)
- return rtn
-
- data['remaining_pct'] = 0.0
- prices = Summary.fetch_prices()
-
- for tx in data['txs']:
- if prices:
- tx['amount_usd'] = coin_to_usd(amt=tx['amount_human'], btc_per_coin=prices['coin-btc'], usd_per_btc=prices['btc-usd'])
- tx['datetime'] = datetime.fromtimestamp(tx['timestamp'])
-
- if data.get('sum', 0.0):
- data['pct'] = 100 / float(self.funds_target / data.get('sum', 0.0))
- data['spent'] = data['sum']
- else:
- data['pct'] = 0.0
- data['spent'] = 0.0
-
- cache_key_in = 'coin_balance_pid_%d' % self.id
- data_in = cache.get(cache_key_in)
- if data_in and data['spent']:
- data['remaining_pct'] = 100 / float(data_in['sum'] / data['spent'])
-
- return data
-
- @staticmethod
- def generate_donation_addr(cls):
- from funding.factory import db_session
- from funding.bin.daemon import Daemon
- if cls.addr_donation:
- return cls.addr_donation
-
- # check if the current user has an account in the wallet
- account = Daemon().get_accounts(cls.id)
- if not account:
- account = Daemon().create_account(cls.id)
- index = account['account_index']
-
- address = account.get('address') or account.get('base_address')
- if not address:
- raise Exception('Cannot generate account/address for pid %d' % cls.id)
-
- # assign donation address, commit to db
- cls.addr_donation = address
- db_session.commit()
- db_session.flush()
- return address
-
@classmethod
def find_by_args(cls, status: int = None, cat: str = None, limit: int = 20, offset=0):
- from funding.factory import db_session
+ from funding.factory import db
if isinstance(status, int) and status not in settings.FUNDING_STATUSES.keys():
raise NotImplementedError('invalid status')
if isinstance(cat, str) and cat not in settings.FUNDING_CATEGORIES:
@@ -313,71 +282,82 @@ class Proposal(base):
@classmethod
def search(cls, key: str):
- key_ilike = '%' + key.replace('%', '') + '%'
+ key_ilike = f"%{key.replace('%', '')}%"
q = Proposal.query
- q = q.filter(sa.or_(
+ q = q.filter(db.or_(
Proposal.headline.ilike(key_ilike),
Proposal.content.ilike(key_ilike)))
return q.all()
-class Payout(base):
+class Payout(db.Model):
__tablename__ = "payouts"
- id = sa.Column(sa.Integer, primary_key=True)
+ id = db.Column(db.Integer, primary_key=True)
- proposal_id = sa.Column(sa.Integer, sa.ForeignKey('proposals.id'))
+ proposal_id = db.Column(db.Integer, db.ForeignKey('proposals.id'))
proposal = relationship("Proposal", back_populates="payouts")
- amount = sa.Column(sa.Integer, nullable=False)
- to_address = sa.Column(sa.VARCHAR, nullable=False)
+ amount = db.Column(db.Integer, nullable=False)
+ to_address = db.Column(db.VARCHAR, nullable=False)
+
+ date_sent = db.Column(db.TIMESTAMP, default=datetime.now)
- ix_proposal_id = sa.Index("ix_proposal_id", proposal_id)
+ ix_proposal_id = db.Index("ix_proposal_id", proposal_id)
@classmethod
def add(cls, proposal_id, amount, to_address):
# @TODO: validate that we can make this payout; check previous payouts
- from flask.ext.login import current_user
+ from flask_login import current_user
if not current_user.admin:
raise Exception("user must be admin to add a payout")
- from funding.factory import db_session
+ from funding.factory import db
try:
payout = Payout(propsal_id=proposal_id, amount=amount, to_address=to_address)
- db_session.add(payout)
- db_session.commit()
- db_session.flush()
+ db.session.add(payout)
+ db.session.commit()
+ db.session.flush()
return payout
except Exception as ex:
- db_session.rollback()
+ db.session.rollback()
raise
@staticmethod
def get_payouts(proposal_id):
- from funding.factory import db_session
- return db_session.query(Payout).filter(Payout.proposal_id == proposal_id).all()
+ from funding.factory import db
+ return db.session.query(Payout).filter(Payout.proposal_id == proposal_id).all()
+
+ @property
+ def as_tx(self):
+ return {
+ "block_height": "-",
+ "type": "out",
+ "amount_human": self.amount,
+ "amount": self.amount
+ }
-class Comment(base):
+class Comment(db.Model):
__tablename__ = "comments"
- id = sa.Column(sa.Integer, primary_key=True)
+ id = db.Column(db.Integer, primary_key=True)
- proposal_id = sa.Column(sa.Integer, sa.ForeignKey('proposals.id'))
+ proposal_id = db.Column(db.Integer, db.ForeignKey('proposals.id'))
proposal = relationship("Proposal", back_populates="comments")
- user_id = sa.Column(sa.Integer, sa.ForeignKey('users.user_id'), nullable=False)
+ user_id = db.Column(db.Integer, db.ForeignKey('users.user_id'), nullable=False)
user = relationship("User", back_populates="comments")
- date_added = sa.Column(sa.TIMESTAMP, default=datetime.now)
+ date_added = db.Column(db.TIMESTAMP, default=datetime.now)
- message = sa.Column(sa.VARCHAR, nullable=False)
- replied_to = sa.Column(sa.ForeignKey("comments.id"))
+ message = db.Column(db.VARCHAR, nullable=False)
+ replied_to = db.Column(db.ForeignKey("comments.id"))
- locked = sa.Column(sa.Boolean, default=False)
+ locked = db.Column(db.Boolean, default=False)
- automated = sa.Column(sa.Boolean, default=False)
+ automated = db.Column(db.Boolean, default=False)
- ix_comment_replied_to = sa.Index("ix_comment_replied_to", replied_to)
- ix_comment_proposal_id = sa.Index("ix_comment_proposal_id", proposal_id)
+ ix_comment_replied_to = db.Index("ix_comment_replied_to", replied_to)
+ ix_comment_proposal_id = db.Index("ix_comment_proposal_id", proposal_id)
@property
def message_html(self):
@@ -390,28 +370,30 @@ class Comment(base):
@staticmethod
def find_by_id(cid: int):
- from funding.factory import db_session
- return db_session.query(Comment).filter(Comment.id == cid).first()
+ from funding.factory import db
+ return db.session.query(Comment).filter(Comment.id == cid).first()
@staticmethod
def remove(cid: int):
- from funding.factory import db_session
- from flask.ext.login import current_user
- if current_user.id != user_id and not current_user.admin:
- raise Exception("no rights to remove this comment")
+ from funding.factory import db
+ from flask_login import current_user
comment = Comment.get(cid=cid)
+
+ if current_user.id != comment.user_id and not current_user.admin:
+ raise Exception("no rights to remove this comment")
+
try:
comment.delete()
- db_session.commit()
- db_session.flush()
+ db.session.commit()
+ db.session.flush()
except:
- db_session.rollback()
+ db.session.rollback()
raise
@staticmethod
def lock(cid: int):
- from funding.factory import db_session
- from flask.ext.login import current_user
+ from funding.factory import db
+ from flask_login import current_user
if not current_user.admin:
raise Exception("admin required")
comment = Comment.find_by_id(cid=cid)
@@ -419,17 +401,17 @@ class Comment(base):
raise Exception("comment by that id not found")
comment.locked = True
try:
- db_session.commit()
- db_session.flush()
+ db.session.commit()
+ db.session.flush()
return comment
except:
- db_session.rollback()
+ db.session.rollback()
raise
@classmethod
def add_comment(cls, pid: int, user_id: int, message: str, cid: int = None, message_id: int = None, automated=False):
- from flask.ext.login import current_user
- from funding.factory import db_session
+ from flask_login import current_user
+ from funding.factory import db
if not message:
raise Exception("empty message")
@@ -448,7 +430,7 @@ class Comment(base):
comment.replied_to = parent.id
else:
try:
- user = db_session.query(User).filter(User.id == user_id).first()
+ user = db.session.query(User).filter(User.id == user_id).first()
if not user:
raise Exception("no user by that id")
comment = next(c for c in user.comments if c.id == message_id)
@@ -460,10 +442,10 @@ class Comment(base):
raise Exception("unknown error")
try:
comment.message = message
- db_session.add(comment)
- db_session.commit()
- db_session.flush()
+ db.session.add(comment)
+ db.session.commit()
+ db.session.flush()
except Exception as ex:
- db_session.rollback()
+ db.session.rollback()
raise Exception(str(ex))
- return comment
\ No newline at end of file
+ return comment
diff --git a/funding/routes.py b/funding/routes.py
index 26e7a87..93580b3 100644
--- a/funding/routes.py
+++ b/funding/routes.py
@@ -1,12 +1,14 @@
from datetime import datetime
+import requests
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_login import login_user , logout_user , current_user, login_required, current_user
from dateutil.parser import parse as dateutil_parse
from flask_yoloapi import endpoint, parameter
import settings
-from funding.factory import app, db_session
+from funding.bin.utils import Summary
+from funding.factory import app, db, cache
from funding.orm.orm import Proposal, User, Comment
@@ -27,7 +29,7 @@ def api():
@app.route('/proposal/add/disclaimer')
def proposal_add_disclaimer():
- return make_response(render_template(('proposal/disclaimer.html')))
+ return make_response(render_template('proposal/disclaimer.html'))
@app.route('/proposal/add')
@@ -79,9 +81,9 @@ def propsal_comment_reply(cid, pid):
@app.route('/proposal/')
def proposal(pid):
p = Proposal.find_by_id(pid=pid)
- p.get_comments()
if not p:
return make_response(redirect(url_for('proposals')))
+ p.get_comments()
return make_response(render_template(('proposal/proposal.html'), proposal=p))
@@ -155,9 +157,9 @@ def proposal_api_add(title, content, pid, funds_target, addr_receiving, category
except Exception as ex:
return make_response(jsonify('letters detected'),500)
if funds_target < 1:
- return make_response(jsonify('Proposal asking less than 1 error :)'), 500)
- if len(addr_receiving) != settings.COIN_ADDRESS_LENGTH:
- return make_response(jsonify('Faulty address, should be of length 72'), 500)
+ return make_response(jsonify('Proposal asking less than 1 error :)'), 500)
+ if len(addr_receiving) not in settings.COIN_ADDRESS_LENGTH:
+ return make_response(jsonify(f'Faulty address, should be of length: {" or ".join(map(str, settings.COIN_ADDRESS_LENGTH))}'), 500)
p = Proposal(headline=title, content=content, category='misc', user=current_user)
p.html = html
@@ -167,18 +169,32 @@ def proposal_api_add(title, content, pid, funds_target, addr_receiving, category
p.category = category
p.status = status
- db_session.add(p)
- db_session.commit()
- db_session.flush()
+ # generate integrated address
+ try:
+ r = requests.get(f'http://{settings.RPC_HOST}:{settings.RPC_PORT}/json_rpc', json={
+ "jsonrpc": "2.0",
+ "id": "0",
+ "method": "make_integrated_address"
+ })
+ r.raise_for_status()
+ blob = r.json()
+
+ assert 'result' in blob
+ assert 'integrated_address' in blob['result']
+ assert 'payment_id' in blob['result']
+ except Exception as ex:
+ raise
- p.addr_donation = Proposal.generate_donation_addr(p)
+ p.addr_donation = blob['result']['integrated_address']
+ p.payment_id = blob['result']['payment_id']
- db_session.commit()
- db_session.flush()
+ db.session.add(p)
- # reset cached statistics
- from funding.bin.utils import Summary
- Summary.fetch_stats(purge=True)
+ db.session.commit()
+ db.session.flush()
+
+ # reset cached stuffz
+ cache.delete('funding_stats')
return make_response(jsonify({'url': url_for('proposal', pid=p.id)}))
@@ -189,7 +205,7 @@ def proposal_edit(pid):
if not p:
return make_response(redirect(url_for('proposals')))
- return make_response(render_template(('proposal/edit.html'), proposal=p))
+ return make_response(render_template('proposal/edit.html', proposal=p))
@app.route('/search')
@@ -205,7 +221,7 @@ def search(key=None):
@app.route('/user/')
def user(name):
- q = db_session.query(User)
+ q = db.session.query(User)
q = q.filter(User.username == name)
user = q.first()
return render_template('user.html', user=user)
@@ -241,7 +257,9 @@ def proposals(status, page, cat):
@app.route('/donate')
def donate():
from funding.bin.daemon import Daemon
- from funding.factory import cache, db_session
+ from funding.factory import cache, db
+
+ return "devfund page currently not working :D"
data_default = {'sum': 0, 'txs': []}
cache_key = 'devfund_txs_in'
@@ -280,6 +298,7 @@ def register():
try:
user = User.add(username, password, email)
flash('Successfully registered. No confirmation email required. You can login!')
+ cache.delete('funding_stats') # reset cached stuffz
return redirect(url_for('login'))
except Exception as ex:
flash('Could not register user. Probably a duplicate username or email that already exists.', 'error')
@@ -318,4 +337,4 @@ def logout():
@app.route('/static/')
def static_route(path):
- return send_from_directory('static', path)
\ No newline at end of file
+ return send_from_directory('static', path)
diff --git a/funding/routes_admin.py b/funding/routes_admin.py
index fa60c95..da9ed19 100644
--- a/funding/routes_admin.py
+++ b/funding/routes_admin.py
@@ -1,5 +1,5 @@
-from flask.ext.login import login_required
-from wowfunding.factory import app, db_session
+from flask_login import login_required
+from funding.factory import app, db
@app.route('/admin/index')
diff --git a/funding/static/css/wow2.css b/funding/static/css/wow2.css
index d130ee2..dd0772b 100644
--- a/funding/static/css/wow2.css
+++ b/funding/static/css/wow2.css
@@ -650,10 +650,6 @@ ul.b {
color: #999;
}
-.tx_item .height {
- float:right
-}
-
.tx_item .height b {
font-size:14px;
}
diff --git a/funding/templates/about.html b/funding/templates/about.html
index 9bf4eb6..95f7b98 100644
--- a/funding/templates/about.html
+++ b/funding/templates/about.html
@@ -26,7 +26,7 @@
When you encounter problems; please visit #wownero on chat.freenode.org
- AEON (camthegeek) has contributed commits to upstream; WOW is grateful.
+ AEON (camthegeek) has contributed commits upstream; WOW is grateful.
diff --git a/funding/templates/base.html b/funding/templates/base.html
index 6d57fe7..91885c7 100644
--- a/funding/templates/base.html
+++ b/funding/templates/base.html
@@ -64,7 +64,7 @@
diff --git a/funding/templates/login.html b/funding/templates/login.html
index 8f23a0f..9f4866c 100644
--- a/funding/templates/login.html
+++ b/funding/templates/login.html
@@ -27,7 +27,7 @@
-
diff --git a/funding/templates/proposal/macros/transaction.html b/funding/templates/proposal/macros/transaction.html
index 227e264..ee7fd05 100644
--- a/funding/templates/proposal/macros/transaction.html
+++ b/funding/templates/proposal/macros/transaction.html
@@ -1,20 +1,28 @@
{% macro tx_item(tx) %}
-
- {{tx['datetime'].strftime('%Y-%m-%d %H:%M')}}
-
-
Blockheight:
- {% if tx['type'] == 'pool' %}
- soon^tm
- {% else %}
- {{tx['height']}}
- {% endif %}
+ {% if tx['type'] == 'pool' %}
+ soon^tm
+ {% elif tx['type'] == 'out' %}
+ hidden
+ {% else %}
+ {{tx['block_height']}}
+ {% endif %}
+ {% if tx['type'] in ['in', 'pool'] %}
{{tx['txid'][:32]}}...
+ {% else %}
+ {% set lulz = [
+ 'vodka', 'hookers', 'booze', 'strippers', 'new lambo',
+ 'new ferrari', 'new villa', 'new vacation home', 'new tesla',
+ 'new watch', 'new home cinema set', 'throphy wife', 'drugs']
+ %}
+ Sent to author. Enjoy the {{ lulz|random }}!
+ {% endif %}
+
{% if tx['type'] in ['in', 'pool'] %}
+
diff --git a/funding/templates/proposal/proposal.html b/funding/templates/proposal/proposal.html
index 6569e64..19fcdb1 100644
--- a/funding/templates/proposal/proposal.html
+++ b/funding/templates/proposal/proposal.html
@@ -97,11 +97,13 @@
{{proposal.balance['available']|round(3) or 0 }} WOW Raised
{% set remaining = proposal.funds_target - proposal.balance['available']|float|round(3) %}
+
{% if remaining > 0 %}
({{ (proposal.funds_target - proposal.balance['available']|float|round(3)|int) }} WOW until goal)
{% elif remaining < 0 %}
({{ (proposal.balance['available']-proposal.funds_target|float|round(3)|int) }} WOW past goal!)
{% endif %}
+
@@ -113,15 +115,16 @@
- {{proposal.spends['spent']|round(3) or 0}} WOW Paid out
+ {{ proposal.spends['amount'] }} WOW Paid out
({{ proposal.spends['pct']|round(1) }}%)
- {{(proposal.balance['available']-proposal.spends['spent']) |round(3) or 0}} WOW Available to Payout
+ {{ (proposal.balance['available']-proposal.spends['amount']) |round(3) or 0}} WOW Available for payout :-)
@@ -185,15 +188,15 @@
{% endif %}
- {% if proposal.spends['txs'] %}
+ {% if proposal.payouts %}
-
+
- {% for tx in proposal.spends['txs'] %}
- {{ tx_item(tx) }}
+ {% for payout in proposal.payouts %}
+ {{ tx_item(payout.as_tx) }}
{% endfor %}
diff --git a/requirements.txt b/requirements.txt
index 12aad6b..ec3b4eb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
sqlalchemy==1.3.4
-flask==0.12.3
+flask
flask-yoloapi==0.1.5
flask_session
flask-login
@@ -12,3 +12,6 @@ requests
pyqrcode
pypng
pillow-simd
+Flask-Caching
+flask-sqlalchemy
+sqlalchemy_json
\ No newline at end of file
diff --git a/settings.py_example b/settings.py_example
index 0132121..7e5efaf 100644
--- a/settings.py_example
+++ b/settings.py_example
@@ -7,11 +7,12 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SECRET = ''
DEBUG = True
-COIN_ADDRESS_LENGTH = 97
+COIN_ADDRESS_LENGTH = [97, 108]
COINCODE = ''
-PSQL_USER = ''
-PSQL_PASS = ''
+PSQL_HOST = "127.0.0.1:5432"
PSQL_DB = ''
+PSQL_USER = 'postgres'
+PSQL_PASS = ''
SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', 'postgresql://{user}:{pw}@localhost/{db}').format(user=PSQL_USER, pw=PSQL_PASS, db=PSQL_DB)