- 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
remotes/1691605234125273493/master
dsc 4 years ago committed by dsc
parent c0b972edde
commit 401c26e85b

@ -5,7 +5,7 @@ from flask_yoloapi import endpoint, parameter
import settings import settings
from funding.bin.utils import get_ip from funding.bin.utils import get_ip
from funding.bin.qr import QrCodeGenerator 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 from funding.orm.orm import Proposal, User

@ -34,8 +34,8 @@ class QrCodeGenerator:
:param color_to: gradient to color :param color_to: gradient to color
:return: :return:
""" """
if len(address) != settings.COIN_ADDRESS_LENGTH: if len(address) not in settings.COIN_ADDRESS_LENGTH:
raise Exception('faulty address length') raise Exception(f'faulty address length, should be: {" or ".join(map(str, settings.COIN_ADDRESS_LENGTH))}')
if not dest: if not dest:
dest = os.path.join(self.base, '%s.png' % address) dest = os.path.join(self.base, '%s.png' % address)

@ -8,6 +8,7 @@ from flask import g, request
from flask.json import JSONEncoder from flask.json import JSONEncoder
import settings import settings
from funding.factory import cache
def json_encoder(obj): def json_encoder(obj):
@ -18,62 +19,50 @@ def json_encoder(obj):
class Summary: class Summary:
@staticmethod @staticmethod
@cache.cached(timeout=300, key_prefix="fetch_prices")
def fetch_prices(): def fetch_prices():
if hasattr(g, 'funding_prices') and g.coin_prices: return {
return g.coin_prices
from funding.factory import cache
cache_key = 'funding_prices'
data = cache.get(cache_key)
if data:
return data
data = {
'coin-btc': coin_btc_value(), 'coin-btc': coin_btc_value(),
'btc-usd': price_cmc_btc_usd() 'btc-usd': price_cmc_btc_usd()
} }
cache.set(cache_key, data=data, expiry=1200)
g.coin_prices = data
return data
@staticmethod @staticmethod
def fetch_stats(purge=False): @cache.cached(timeout=300, key_prefix="funding_stats")
from funding.factory import db_session def fetch_stats():
from funding.factory import db
from funding.orm.orm import Proposal, User, Comment 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 categories = settings.FUNDING_CATEGORIES
statuses = settings.FUNDING_STATUSES.keys() statuses = settings.FUNDING_STATUSES.keys()
for cat in categories: for cat in categories:
q = db_session.query(Proposal) q = db.session.query(Proposal)
q = q.filter(Proposal.category == cat) q = q.filter(Proposal.category == cat)
res = q.count() res = q.count()
data.setdefault('cats', {}) data.setdefault('cats', {})
data['cats'][cat] = res data['cats'][cat] = res
for status in statuses: for status in statuses:
q = db_session.query(Proposal) q = db.session.query(Proposal)
q = q.filter(Proposal.status == status) q = q.filter(Proposal.status == status)
res = q.count() res = q.count()
data.setdefault('statuses', {}) data.setdefault('statuses', {})
data['statuses'][status] = res data['statuses'][status] = res
data.setdefault('users', {}) data.setdefault('users', {})
data['users']['count'] = db_session.query(User.id).count() data['users']['count'] = db.session.query(User.id).count()
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.coingecko.com/api/v3/coins/markets?vs_currency=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') data = r.json()
btc = next(c for c in data if c['symbol'] == 'btc')
return btc['current_price']
except: except:
return return
@ -81,7 +70,6 @@ def price_cmc_btc_usd():
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'))

@ -2,16 +2,16 @@ from datetime import datetime
from flask import session, g, request from flask import session, g, request
import settings import settings
from funding.bin.utils import Summary 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 from funding.orm.orm import Proposal, User, Comment
@app.context_processor @app.context_processor
def templating(): def templating():
from flask.ext.login import current_user 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() recent_comments = db.session.query(Comment).filter(Comment.automated == False).order_by(Comment.date_added.desc()).limit(8).all()
summary_data = Summary.fetch_stats() 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, return dict(logged_in=current_user.is_authenticated,
current_user=current_user, current_user=current_user,
funding_categories=settings.FUNDING_CATEGORIES, funding_categories=settings.FUNDING_CATEGORIES,
@ -28,8 +28,6 @@ def before_request():
@app.after_request @app.after_request
def after_request(res): def after_request(res):
if hasattr(g, 'funding_prices'):
delattr(g, 'funding_prices')
res.headers.add('Accept-Ranges', 'bytes') res.headers.add('Accept-Ranges', 'bytes')
if request.full_path.startswith('/api/'): if request.full_path.startswith('/api/'):
@ -45,7 +43,7 @@ def after_request(res):
@app.teardown_appcontext @app.teardown_appcontext
def shutdown_session(**kwargs): def shutdown_session(**kwargs):
db_session.remove() db.session.remove()
@app.errorhandler(404) @app.errorhandler(404)

@ -45,17 +45,3 @@ class JsonRedis(RedisSessionInterface):
redis=redis.Redis(**redis_args()), redis=redis.Redis(**redis_args()),
key_prefix=key_prefix, key_prefix=key_prefix,
use_signer=use_signer) 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)

@ -1,40 +1,79 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import settings import settings
from werkzeug.contrib.fixers import ProxyFix
from flask import Flask from flask import Flask
from flask_caching import Cache
from flask_session import Session
from flask_sqlalchemy import SQLAlchemy
app = None app = None
sentry = None
cache = None cache = None
db_session = None db = None
bcrypt = 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(): def create_app():
global app global app
global db_session global db
global sentry
global cache global cache
global bcrypt global bcrypt
from funding.orm.connect import create_session app = Flask(import_name=__name__,
db_session = create_session() static_folder='static',
template_folder='templates')
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app)
app.config.from_object(settings) app.config.from_object(settings)
app.config['PERMANENT_SESSION_LIFETIME'] = 2678400 app.config['PERMANENT_SESSION_LIFETIME'] = 2678400
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 30 app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 30
app.secret_key = settings.SECRET app.secret_key = settings.SECRET
_setup_cache(app)
_setup_session(app)
_setup_db(app)
# flask-login # flask-login
from flask.ext.login import LoginManager from flask_login import LoginManager
login_manager = LoginManager() login_manager = LoginManager()
login_manager.init_app(app) login_manager.init_app(app)
login_manager.login_view = 'login' login_manager.login_view = 'login'
from flask.ext.bcrypt import Bcrypt from flask_bcrypt import Bcrypt
bcrypt = Bcrypt(app) bcrypt = Bcrypt(app)
@login_manager.user_loader @login_manager.user_loader
@ -42,15 +81,10 @@ def create_app():
from funding.orm.orm import User from funding.orm.orm import User
return User.query.get(int(_id)) 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 # import routes
from funding import routes from funding import routes
from funding import api from funding import api
from funding.bin import utils_request from funding.bin import utils_request
app.app_context().push() app.app_context().push()
return app return app

@ -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

@ -1,19 +1,29 @@
from datetime import datetime from datetime import datetime
import string
import random
import requests
from sqlalchemy.orm import relationship, backref
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.orm import scoped_session, sessionmaker, relationship from sqlalchemy.orm import scoped_session, sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.types import Float
from sqlalchemy_json import MutableJson
import settings import settings
from funding.factory import db
base = declarative_base(name="Model") base = declarative_base(name="Model")
class User(base):
class User(db.Model):
__tablename__ = "users" __tablename__ = "users"
id = sa.Column('user_id', sa.Integer, primary_key=True) id = db.Column('user_id', db.Integer, primary_key=True)
username = sa.Column(sa.String(20), unique=True, index=True) username = db.Column(db.String(20), unique=True, index=True)
password = sa.Column(sa.String(60)) password = db.Column(db.String(60))
email = sa.Column(sa.String(50), unique=True, index=True) email = db.Column(db.String(50), unique=True, index=True)
registered_on = sa.Column(sa.DateTime) registered_on = db.Column(db.DateTime)
admin = sa.Column(sa.Boolean, default=False) admin = db.Column(db.Boolean, default=False)
proposals = relationship('Proposal', back_populates="user") proposals = relationship('Proposal', back_populates="user")
comments = relationship("Comment", back_populates="user") comments = relationship("Comment", back_populates="user")
@ -48,7 +58,7 @@ class User(base):
@classmethod @classmethod
def add(cls, username, password, email): 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 from funding.validation import val_username, val_email
try: try:
@ -57,37 +67,39 @@ class User(base):
val_email(email) val_email(email)
user = User(username, password, email) user = User(username, password, email)
db_session.add(user) db.session.add(user)
db_session.commit() db.session.commit()
db_session.flush() db.session.flush()
return user return user
except Exception as ex: except Exception as ex:
db_session.rollback() db.session.rollback()
raise raise
class Proposal(base): class Proposal(db.Model):
__tablename__ = "proposals" __tablename__ = "proposals"
id = sa.Column(sa.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
headline = sa.Column(sa.VARCHAR, nullable=False) archived = db.Column(db.Boolean, default=False)
content = sa.Column(sa.VARCHAR, nullable=False) headline = db.Column(db.VARCHAR, nullable=False)
category = sa.Column(sa.VARCHAR, nullable=False) content = db.Column(db.VARCHAR, nullable=False)
date_added = sa.Column(sa.TIMESTAMP, default=datetime.now) category = db.Column(db.VARCHAR, nullable=False)
html = sa.Column(sa.VARCHAR) date_added = db.Column(db.TIMESTAMP, default=datetime.now)
last_edited = sa.Column(sa.TIMESTAMP) html = db.Column(db.VARCHAR)
last_edited = db.Column(db.TIMESTAMP)
# the FFS target # the FFS target
funds_target = sa.Column(sa.Float, nullable=False) funds_target = db.Column(db.Float, nullable=False)
# the FFS progress (cached) # 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) # 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 # the FFS receiving and withdrawal addresses
addr_donation = sa.Column(sa.VARCHAR) addr_donation = db.Column(db.VARCHAR)
addr_receiving = sa.Column(sa.VARCHAR) addr_receiving = db.Column(db.VARCHAR)
payment_id = db.Column(db.VARCHAR)
# proposal status: # proposal status:
# 0: disabled # 0: disabled
@ -95,9 +107,9 @@ class Proposal(base):
# 2: funding required # 2: funding required
# 3: wip # 3: wip
# 4: completed # 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") user = relationship("User", back_populates="proposals")
payouts = relationship("Payout", back_populates="proposal") payouts = relationship("Payout", back_populates="proposal")
@ -131,17 +143,12 @@ class Proposal(base):
@classmethod @classmethod
def find_by_id(cls, pid: int): def find_by_id(cls, pid: int):
from funding.factory import db_session from funding.factory import db
q = cls.query q = cls.query
q = q.filter(Proposal.id == pid) q = q.filter(Proposal.id == pid)
result = q.first() result = q.first()
if not result: if not result:
return 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 return result
@property @property
@ -154,21 +161,21 @@ class Proposal(base):
@property @property
def comment_count(self): def comment_count(self):
from funding.factory import db_session from funding.factory import db
q = db_session.query(sa.func.count(Comment.id)) q = db.session.query(db.func.count(Comment.id))
q = q.filter(Comment.proposal_id == self.id) q = q.filter(Comment.proposal_id == self.id)
return q.scalar() return q.scalar()
def get_comments(self): def get_comments(self):
from funding.factory import db_session from funding.factory import db
q = db_session.query(Comment) q = db.session.query(Comment)
q = q.filter(Comment.proposal_id == self.id) 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()) q = q.order_by(Comment.date_added.desc())
comments = q.all() comments = q.all()
for c in comments: 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.proposal_id == self.id)
q = q.filter(Comment.replied_to == c.id) q = q.filter(Comment.replied_to == c.id)
_c = q.all() _c = q.all()
@ -177,6 +184,12 @@ class Proposal(base):
setattr(self, '_comments', comments) setattr(self, '_comments', comments)
return self 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 @property
def balance(self): def balance(self):
"""This property retrieves the current funding status """This property retrieves the current funding status
@ -184,28 +197,49 @@ class Proposal(base):
daemon too much. Returns a nice dictionary containing daemon too much. Returns a nice dictionary containing
all relevant proposal funding info""" all relevant proposal funding info"""
from funding.bin.utils import Summary, coin_to_usd from funding.bin.utils import Summary, coin_to_usd
from funding.factory import cache, db_session from funding.factory import cache, db
rtn = {'sum': 0.0, 'txs': [], 'pct': 0.0} rtn = {'sum': 0.0, 'txs': [], 'pct': 0.0, 'available': 0}
cache_key = 'coin_balance_pid_%d' % self.id if self.archived:
data = cache.get(cache_key) return rtn
if not data:
from funding.bin.daemon import Daemon try:
try: r = requests.get(f'http://{settings.RPC_HOST}:{settings.RPC_PORT}/json_rpc', json={
data = Daemon().get_transfers_in(proposal=self) "jsonrpc": "2.0",
if not isinstance(data, dict): "id": "0",
print('error; get_transfers_in; %d' % self.id) "method": "get_payments",
return rtn "params": {
cache.set(cache_key, data=data, expiry=60) "payment_id": self.payment_id
except Exception as ex: }
print('error; get_transfers_in; %d' % self.id) })
return rtn 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() prices = Summary.fetch_prices()
for tx in data['txs']: for tx in data['txs']:
if prices: 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['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): if data.get('sum', 0.0):
data['pct'] = 100 / float(self.funds_target / 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: if data['pct'] != self.funds_progress:
self.funds_progress = data['pct'] self.funds_progress = data['pct']
db_session.commit() db.session.commit()
db_session.flush() db.session.flush()
if data['available']: if data['available']:
data['remaining_pct'] = 100 / float(data['sum'] / data['available']) data['remaining_pct'] = 100 / float(data['sum'] / data['available'])
@ -226,74 +260,9 @@ class Proposal(base):
return data 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 @classmethod
def find_by_args(cls, status: int = None, cat: str = None, limit: int = 20, offset=0): 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(): if isinstance(status, int) and status not in settings.FUNDING_STATUSES.keys():
raise NotImplementedError('invalid status') raise NotImplementedError('invalid status')
if isinstance(cat, str) and cat not in settings.FUNDING_CATEGORIES: if isinstance(cat, str) and cat not in settings.FUNDING_CATEGORIES:
@ -313,71 +282,82 @@ class Proposal(base):
@classmethod @classmethod
def search(cls, key: str): def search(cls, key: str):
key_ilike = '%' + key.replace('%', '') + '%' key_ilike = f"%{key.replace('%', '')}%"
q = Proposal.query q = Proposal.query
q = q.filter(sa.or_( q = q.filter(db.or_(
Proposal.headline.ilike(key_ilike), Proposal.headline.ilike(key_ilike),
Proposal.content.ilike(key_ilike))) Proposal.content.ilike(key_ilike)))
return q.all() return q.all()
class Payout(base): class Payout(db.Model):
__tablename__ = "payouts" __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") proposal = relationship("Proposal", back_populates="payouts")
amount = sa.Column(sa.Integer, nullable=False) amount = db.Column(db.Integer, nullable=False)
to_address = sa.Column(sa.VARCHAR, 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 @classmethod
def add(cls, proposal_id, amount, to_address): def add(cls, proposal_id, amount, to_address):
# @TODO: validate that we can make this payout; check previous payouts # @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: if not current_user.admin:
raise Exception("user must be admin to add a payout") raise Exception("user must be admin to add a payout")
from funding.factory import db_session from funding.factory import db
try: try:
payout = Payout(propsal_id=proposal_id, amount=amount, to_address=to_address) payout = Payout(propsal_id=proposal_id, amount=amount, to_address=to_address)
db_session.add(payout) db.session.add(payout)
db_session.commit() db.session.commit()
db_session.flush() db.session.flush()
return payout return payout
except Exception as ex: except Exception as ex:
db_session.rollback() db.session.rollback()
raise raise
@staticmethod @staticmethod
def get_payouts(proposal_id): def get_payouts(proposal_id):
from funding.factory import db_session from funding.factory import db
return db_session.query(Payout).filter(Payout.proposal_id == proposal_id).all() 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" __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") 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") 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) message = db.Column(db.VARCHAR, nullable=False)
replied_to = sa.Column(sa.ForeignKey("comments.id")) 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_replied_to = db.Index("ix_comment_replied_to", replied_to)
ix_comment_proposal_id = sa.Index("ix_comment_proposal_id", proposal_id) ix_comment_proposal_id = db.Index("ix_comment_proposal_id", proposal_id)
@property @property
def message_html(self): def message_html(self):
@ -390,28 +370,30 @@ class Comment(base):
@staticmethod @staticmethod
def find_by_id(cid: int): def find_by_id(cid: int):
from funding.factory import db_session from funding.factory import db
return db_session.query(Comment).filter(Comment.id == cid).first() return db.session.query(Comment).filter(Comment.id == cid).first()
@staticmethod @staticmethod
def remove(cid: int): def remove(cid: int):
from funding.factory import db_session from funding.factory import db
from flask.ext.login import current_user from flask_login import current_user
if current_user.id != user_id and not current_user.admin:
raise Exception("no rights to remove this comment")
comment = Comment.get(cid=cid) 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: try:
comment.delete() comment.delete()
db_session.commit() db.session.commit()
db_session.flush() db.session.flush()
except: except:
db_session.rollback() db.session.rollback()
raise raise
@staticmethod @staticmethod
def lock(cid: int): def lock(cid: int):
from funding.factory import db_session from funding.factory import db
from flask.ext.login import current_user from flask_login import current_user
if not current_user.admin: if not current_user.admin:
raise Exception("admin required") raise Exception("admin required")
comment = Comment.find_by_id(cid=cid) comment = Comment.find_by_id(cid=cid)
@ -419,17 +401,17 @@ class Comment(base):
raise Exception("comment by that id not found") raise Exception("comment by that id not found")
comment.locked = True comment.locked = True
try: try:
db_session.commit() db.session.commit()
db_session.flush() db.session.flush()
return comment return comment
except: except:
db_session.rollback() db.session.rollback()
raise raise
@classmethod @classmethod
def add_comment(cls, pid: int, user_id: int, message: str, cid: int = None, message_id: int = None, automated=False): 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 flask_login import current_user
from funding.factory import db_session from funding.factory import db
if not message: if not message:
raise Exception("empty message") raise Exception("empty message")
@ -448,7 +430,7 @@ class Comment(base):
comment.replied_to = parent.id comment.replied_to = parent.id
else: else:
try: 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: if not user:
raise Exception("no user by that id") raise Exception("no user by that id")
comment = next(c for c in user.comments if c.id == message_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") raise Exception("unknown error")
try: try:
comment.message = message comment.message = message
db_session.add(comment) db.session.add(comment)
db_session.commit() db.session.commit()
db_session.flush() db.session.flush()
except Exception as ex: except Exception as ex:
db_session.rollback() db.session.rollback()
raise Exception(str(ex)) raise Exception(str(ex))
return comment return comment

@ -1,12 +1,14 @@
from datetime import datetime 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 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 dateutil.parser import parse as dateutil_parse
from flask_yoloapi import endpoint, parameter from flask_yoloapi import endpoint, parameter
import settings 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 from funding.orm.orm import Proposal, User, Comment
@ -27,7 +29,7 @@ def api():
@app.route('/proposal/add/disclaimer') @app.route('/proposal/add/disclaimer')
def 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') @app.route('/proposal/add')
@ -79,9 +81,9 @@ def propsal_comment_reply(cid, pid):
@app.route('/proposal/<int:pid>') @app.route('/proposal/<int:pid>')
def proposal(pid): def proposal(pid):
p = Proposal.find_by_id(pid=pid) p = Proposal.find_by_id(pid=pid)
p.get_comments()
if not p: if not p:
return make_response(redirect(url_for('proposals'))) return make_response(redirect(url_for('proposals')))
p.get_comments()
return make_response(render_template(('proposal/proposal.html'), proposal=p)) 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: except Exception as ex:
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) != settings.COIN_ADDRESS_LENGTH: if len(addr_receiving) not in settings.COIN_ADDRESS_LENGTH:
return make_response(jsonify('Faulty address, should be of length 72'), 500) 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 = Proposal(headline=title, content=content, category='misc', user=current_user)
p.html = html p.html = html
@ -167,18 +169,32 @@ def proposal_api_add(title, content, pid, funds_target, addr_receiving, category
p.category = category p.category = category
p.status = status p.status = status
db_session.add(p) # generate integrated address
db_session.commit() try:
db_session.flush() 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.add(p)
db_session.flush()
# reset cached statistics db.session.commit()
from funding.bin.utils import Summary db.session.flush()
Summary.fetch_stats(purge=True)
# reset cached stuffz
cache.delete('funding_stats')
return make_response(jsonify({'url': url_for('proposal', pid=p.id)})) return make_response(jsonify({'url': url_for('proposal', pid=p.id)}))
@ -189,7 +205,7 @@ def proposal_edit(pid):
if not p: if not p:
return make_response(redirect(url_for('proposals'))) 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') @app.route('/search')
@ -205,7 +221,7 @@ def search(key=None):
@app.route('/user/<path:name>') @app.route('/user/<path:name>')
def user(name): def user(name):
q = db_session.query(User) q = db.session.query(User)
q = q.filter(User.username == name) q = q.filter(User.username == name)
user = q.first() user = q.first()
return render_template('user.html', user=user) return render_template('user.html', user=user)
@ -241,7 +257,9 @@ def proposals(status, page, cat):
@app.route('/donate') @app.route('/donate')
def donate(): def donate():
from funding.bin.daemon import Daemon 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': []} data_default = {'sum': 0, 'txs': []}
cache_key = 'devfund_txs_in' cache_key = 'devfund_txs_in'
@ -280,6 +298,7 @@ def register():
try: try:
user = User.add(username, password, email) user = User.add(username, password, email)
flash('Successfully registered. No confirmation email required. You can login!') flash('Successfully registered. No confirmation email required. You can login!')
cache.delete('funding_stats') # reset cached stuffz
return redirect(url_for('login')) return redirect(url_for('login'))
except Exception as ex: except Exception as ex:
flash('Could not register user. Probably a duplicate username or email that already exists.', 'error') flash('Could not register user. Probably a duplicate username or email that already exists.', 'error')
@ -318,4 +337,4 @@ def logout():
@app.route('/static/<path:path>') @app.route('/static/<path:path>')
def static_route(path): def static_route(path):
return send_from_directory('static', path) return send_from_directory('static', path)

@ -1,5 +1,5 @@
from flask.ext.login import login_required from flask_login import login_required
from wowfunding.factory import app, db_session from funding.factory import app, db
@app.route('/admin/index') @app.route('/admin/index')

@ -650,10 +650,6 @@ ul.b {
color: #999; color: #999;
} }
.tx_item .height {
float:right
}
.tx_item .height b { .tx_item .height b {
font-size:14px; font-size:14px;
} }

@ -26,7 +26,7 @@
When you encounter problems; please visit #wownero on chat.freenode.org When you encounter problems; please visit #wownero on chat.freenode.org
</p> </p>
<p> <p>
AEON (<a target="_blank" href="https://github.com/camthegeek">camthegeek</a>) has contributed commits to <a href="http://github.com/skftn/wownero-wfs">upstream</a>; WOW is grateful. AEON (<a target="_blank" href="https://github.com/camthegeek">camthegeek</a>) has contributed commits <a href="http://github.com/skftn/wownero-wfs">upstream</a>; WOW is grateful.
</p> </p>
</div> </div>

@ -64,7 +64,7 @@
<!-- Footer --> <!-- Footer -->
<footer class="bg-dark footer"> <footer class="bg-dark footer">
<div class="container"> <div class="container">
<p class="m-0 text-center text-white">WOW 2018</p> <p class="m-0 text-center text-white">WOW 2018-2020</p>
</div> </div>
</footer> </footer>

@ -27,7 +27,7 @@
</div> </div>
<hr> <hr>
<div class="row"> <div class="row">
<div class="col-lg-3"> <div class="col-lg-4">
<form class="form-horizontal" action="" method=post> <form class="form-horizontal" action="" method=post>
<div class="form-group"> <div class="form-group">
<label class="sr-only" for="inlineFormInput">Name</label> <label class="sr-only" for="inlineFormInput">Name</label>
@ -46,10 +46,13 @@
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary btn-sm">Login</button> <button type="submit" class="btn btn-primary btn-sm">Login</button>
</div> </div>
<div class="form-group">
<small>Forgot your password? Out of luck! Create a new account lol</small>
</div>
</form> </form>
</div> </div>
<div class="col-lg-5"> <div class="col-lg-5">
<a href="/register">Or register here</a> <a href="/register">Register here</a>
</div> </div>
</div> </div>
</div> </div>

@ -1,20 +1,28 @@
{% macro tx_item(tx) %} {% macro tx_item(tx) %}
<li class="list-group-item tx_item"> <li class="list-group-item tx_item">
<span class="datetime">
{{tx['datetime'].strftime('%Y-%m-%d %H:%M')}}
</span>
<span class="height"> <span class="height">
<b>Blockheight</b>: <b>Blockheight</b>:
{% if tx['type'] == 'pool' %} {% if tx['type'] == 'pool' %}
soon^tm soon^tm
{% else %} {% elif tx['type'] == 'out' %}
{{tx['height']}} <small>hidden</small>
{% endif %} {% else %}
{{tx['block_height']}}
{% endif %}
</span> </span>
<br> <br>
{% if tx['type'] in ['in', 'pool'] %}
<a target="_blank" href="https://explore.wownero.com/tx/{{tx['txid']}}">{{tx['txid'][:32]}}...</a> <a target="_blank" href="https://explore.wownero.com/tx/{{tx['txid']}}">{{tx['txid'][:32]}}...</a>
{% 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']
%}
<a style="font-size:11px;" href="#">Sent to author. Enjoy the {{ lulz|random }}!</a>
{% endif %}
<span class="amount{% if tx['type'] in ['pool', 'in'] %} in{% endif %}"> <span class="amount{% if tx['type'] in ['pool', 'in'] %} in{% endif %}">
{% if tx['type'] in ['in', 'pool'] %} {% if tx['type'] in ['in', 'pool'] %}
+ +

@ -97,11 +97,13 @@
{{proposal.balance['available']|round(3) or 0 }} WOW Raised {{proposal.balance['available']|round(3) or 0 }} WOW Raised
{% set remaining = proposal.funds_target - proposal.balance['available']|float|round(3) %} {% set remaining = proposal.funds_target - proposal.balance['available']|float|round(3) %}
<small>
{% if remaining > 0 %} {% if remaining > 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)
{% elif remaining < 0 %} {% elif remaining < 0 %}
({{ (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 %}
</small>
<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']}}%;">
@ -113,15 +115,16 @@
<br/> <br/>
<div class="col-lg-8"> <div class="col-lg-8">
{{proposal.spends['spent']|round(3) or 0}} WOW Paid out {{ proposal.spends['amount'] }} WOW Paid out <small>({{ proposal.spends['pct']|round(1) }}%)</small>
<div class="progress"> <div class="progress">
<div class="progress-bar progress-warning progress-bar" style="width: {{proposal.spends['remaining_pct']}}%;"> <!-- @todo: spends remaining pct -->
<div class="progress-bar progress-warning progress-bar" style="width: {{ proposal.spends['pct'] }}%;">
</div> </div>
</div> </div>
<hr> <hr>
</div> </div>
<div class="col-lg-8"> <div class="col-lg-8">
{{(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 :-)
<div class="progress"> <div class="progress">
<div class="progress-bar progress-warning progress-bar" style="width: {{proposal.balance['remaining_pct']}}%;"> <div class="progress-bar progress-warning progress-bar" style="width: {{proposal.balance['remaining_pct']}}%;">
</div> </div>
@ -185,15 +188,15 @@
<!-- /.row --> <!-- /.row -->
{% endif %} {% endif %}
{% if proposal.spends['txs'] %} {% if proposal.payouts %}
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="card my-6" id="incoming_txs"> <div class="card my-6" id="incoming_txs">
<h5 class="card-header">Outgoing transactions <small>({{proposal.spends['txs']|length}})</small></h5> <h5 class="card-header">Outgoing transactions</h5>
<div class="card-body"> <div class="card-body">
<ul class="list-group"> <ul class="list-group">
{% for tx in proposal.spends['txs'] %} {% for payout in proposal.payouts %}
{{ tx_item(tx) }} {{ tx_item(payout.as_tx) }}
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

@ -1,5 +1,5 @@
sqlalchemy==1.3.4 sqlalchemy==1.3.4
flask==0.12.3 flask
flask-yoloapi==0.1.5 flask-yoloapi==0.1.5
flask_session flask_session
flask-login flask-login
@ -12,3 +12,6 @@ requests
pyqrcode pyqrcode
pypng pypng
pillow-simd pillow-simd
Flask-Caching
flask-sqlalchemy
sqlalchemy_json

@ -7,11 +7,12 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SECRET = '' SECRET = ''
DEBUG = True DEBUG = True
COIN_ADDRESS_LENGTH = 97 COIN_ADDRESS_LENGTH = [97, 108]
COINCODE = '' COINCODE = ''
PSQL_USER = '' PSQL_HOST = "127.0.0.1:5432"
PSQL_PASS = ''
PSQL_DB = '' 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) SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', 'postgresql://{user}:{pw}@localhost/{db}').format(user=PSQL_USER, pw=PSQL_PASS, db=PSQL_DB)

Loading…
Cancel
Save