aeonfunding update for wownero

a lot more generic; a lot more configurable; UI goodness with accounting; wallet account support for ezmode
This commit is contained in:
CamTheGeek 2018-09-05 18:27:26 -04:00
parent f473a4234e
commit 955de2544e
19 changed files with 361 additions and 173 deletions

View file

@ -2,11 +2,9 @@ from datetime import datetime
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.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 wowfunding.factory import app, db_session from funding.factory import app, db_session
from wowfunding.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(
@ -21,15 +19,13 @@ def api_proposals_get(status, cat, limit, offset):
except Exception as ex: except Exception as ex:
print(ex) print(ex)
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('wow', type=int, location='args', required=True) parameter('amount', type=int, location='args', required=True)
) )
def api_wow_usd(wow): def api_coin_usd(amount):
from wowfunding.bin.utils import Summary, wow_to_usd from funding.bin.utils import Summary, coin_to_usd
prices = Summary.fetch_prices() prices = Summary.fetch_prices()
return jsonify(usd=wow_to_usd(wows=wow, btc_per_wow=prices['wow-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']))

View file

@ -1,10 +1,12 @@
import settings import settings
import requests import requests
from requests.auth import HTTPDigestAuth
class Daemon:
class WowneroDaemon:
def __init__(self): def __init__(self):
self.url = settings.RPC_LOCATION self.url = settings.RPC_LOCATION
self.username = settings.RPC_USERNAME
self.password = settings.RPC_PASSWORD
self.headers = {"User-Agent": "Mozilla"} self.headers = {"User-Agent": "Mozilla"}
def create_address(self, label_name): def create_address(self, label_name):
@ -16,37 +18,65 @@ class WowneroDaemon:
} }
return self._make_request(data) return self._make_request(data)
def get_address(self, index): def create_account(self, pid):
data = { data = {
'method': 'get_address', 'method': 'create_account',
'params': {'address_index': [index], 'account_index': 0}, 'params': {'label': '%s' % pid},
'jsonrpc': '2.0',
'id': '0'
}
return self._make_request(data)
def get_address(self, index, proposal_id):
data = {
'method': 'getaddress',
'params': {'account_index': proposal_id, 'address_index': '[0]'},
'jsonrpc': '2.0', 'jsonrpc': '2.0',
'id': '0' 'id': '0'
} }
try: try:
result = self._make_request(data) result = self._make_request(data)
return next(z for z in result['result']['addresses'] if z['address_index'] == index) return result['result']
except: except:
return return
def get_transfers_in(self, index): def get_transfers_in(self, index, proposal_id):
data = { data = {
"method":"get_transfers", "method":"get_transfers",
"params": {"pool": True, "in": True, "account_index": 0, "subaddr_indices": [index]}, "params": {"pool": True, "in": True, "account_index": proposal_id},
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": "0", "id": "0",
} }
data = self._make_request(data) data = self._make_request(data)
data = data['result'].get('in', []) data = data['result'].get('in', [])
for d in data: for d in data:
d['amount_human'] = float(d['amount'])/1e11 d['amount_human'] = float(d['amount'])/1e12
return { return {
'sum': sum([float(z['amount'])/1e11 for z in data]), 'sum': sum([float(z['amount'])/1e12 for z in data]),
'txs': data
}
def get_transfers_out(self, index, proposal_id):
data = {
"method":"get_transfers",
"params": {"pool": True, "out": True, "account_index": proposal_id},
"jsonrpc": "2.0",
"id": "0",
}
data = self._make_request(data)
data = data['result'].get('out', [])
for d in data:
d['amount_human'] = float(d['amount'])/1e12
return {
'sum': sum([float(z['amount'])/1e12 for z in data]),
'txs': data 'txs': data
} }
def _make_request(self, data): def _make_request(self, data):
r = requests.post(self.url, json=data, headers=self.headers) if self.username:
if self.password:
r = requests.post(self.url, auth=HTTPDigestAuth(settings.RPC_USERNAME, settings.RPC_PASSWORD), json=data, headers=self.headers)
else:
r = requests.post(self.url, json=data, headers=self.headers)
r.raise_for_status() r.raise_for_status()
return r.json() return r.json()

View file

@ -1,44 +1,39 @@
from datetime import datetime, date from datetime import datetime, date
import requests import requests
from flask import g from flask import g
from flask.json import JSONEncoder from flask.json import JSONEncoder
import json
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():
if hasattr(g, 'wowfunding_prices') and g.wow_prices: if hasattr(g, 'funding_prices') and g.coin_prices:
return g.wow_prices return g.coin_prices
from funding.factory import cache
from wowfunding.factory import cache cache_key = 'funding_prices'
cache_key = 'wowfunding_prices'
data = cache.get(cache_key) data = cache.get(cache_key)
if data: if data:
return data return data
data = { data = {
'wow-btc': price_tradeogre_wow_btc(), 'coin-btc': coin_btc_value(),
'btc-usd': price_cmc_btc_usd() 'btc-usd': price_cmc_btc_usd()
} }
cache.set(cache_key, data=data, expiry=7200) cache.set(cache_key, data=data, expiry=7200)
g.wow_prices = data g.coin_prices = data
return data return data
@staticmethod @staticmethod
def fetch_stats(purge=False): def fetch_stats(purge=False):
from wowfunding.factory import db_session from funding.factory import db_session
from wowfunding.orm.orm import Proposal, User, Comment from funding.orm.orm import Proposal, User, Comment
from wowfunding.factory import cache from funding.factory import cache
cache_key = 'wowfunding_stats' cache_key = 'funding_stats'
data = cache.get(cache_key) data = cache.get(cache_key)
if data and not purge: if data and not purge:
return data return data
@ -65,7 +60,6 @@ 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:
@ -75,8 +69,7 @@ def price_cmc_btc_usd():
except: except:
return return
def coin_btc_value():
def price_tradeogre_wow_btc():
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:
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)
@ -85,9 +78,8 @@ def price_tradeogre_wow_btc():
except: except:
return return
def coin_to_usd(amt: float, usd_per_btc: float, btc_per_coin: float):
def wow_to_usd(wows: float, usd_per_btc: float, btc_per_wow: float):
try: try:
return round(usd_per_btc / (1.0 / (wows * btc_per_wow)), 2) return round(usd_per_btc / (1.0 / (amt * btc_per_coin)), 2)
except: except:
pass pass

View file

@ -1,34 +1,32 @@
from datetime import datetime from datetime import datetime
from flask import session, g from flask import session, g
import settings import settings
from wowfunding.bin.utils import Summary from funding.bin.utils import Summary
from wowfunding.factory import app, db_session from funding.factory import app, db_session
from wowfunding.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.ext.login import current_user
recent_comments = db_session.query(Comment).filter(Comment.automated == False).order_by(Comment.date_added.desc()).limit(10).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()
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,
funding_statuses=settings.FUNDING_STATUSES, funding_statuses=settings.FUNDING_STATUSES,
summary_data=summary_data, summary_data=summary_data,
recent_comments=recent_comments) recent_comments=recent_comments,
newest_users=newest_users)
@app.before_request @app.before_request
def before_request(): def before_request():
pass pass
@app.after_request @app.after_request
def after_request(res): def after_request(res):
if hasattr(g, 'wowfunding_prices'): if hasattr(g, 'funding_prices'):
delattr(g, 'wowfunding_prices') delattr(g, 'funding_prices')
res.headers.add('Accept-Ranges', 'bytes') res.headers.add('Accept-Ranges', 'bytes')
if settings.DEBUG: if settings.DEBUG:
res.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' res.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
@ -37,12 +35,10 @@ def after_request(res):
res.headers['Cache-Control'] = 'public, max-age=0' res.headers['Cache-Control'] = 'public, max-age=0'
return res return 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)
def error(err): def error(err):
return 'Error', 404 return 'Error', 404

View file

@ -4,7 +4,7 @@ import redis
from flask_session import RedisSessionInterface from flask_session import RedisSessionInterface
import settings import settings
from wowfunding.bin.utils import json_encoder from funding.bin.utils import json_encoder
def redis_args(): def redis_args():

View file

@ -17,7 +17,7 @@ def create_app():
global cache global cache
global bcrypt global bcrypt
from wowfunding.orm.connect import create_session from funding.orm.connect import create_session
db_session = create_session() db_session = create_session()
app = Flask(__name__) app = Flask(__name__)
@ -39,18 +39,18 @@ def create_app():
@login_manager.user_loader @login_manager.user_loader
def load_user(_id): def load_user(_id):
from wowfunding.orm.orm import User from funding.orm.orm import User
return User.query.get(int(_id)) return User.query.get(int(_id))
# session init # session init
from wowfunding.cache import JsonRedis, WowCache from funding.cache import JsonRedis, WowCache
app.session_interface = JsonRedis(key_prefix=app.config['SESSION_PREFIX'], use_signer=False) app.session_interface = JsonRedis(key_prefix=app.config['SESSION_PREFIX'], use_signer=False)
cache = WowCache() cache = WowCache()
# import routes # import routes
from wowfunding import routes from funding import routes
from wowfunding import api from funding import api
from wowfunding.bin import utils_request from funding.bin import utils_request
app.app_context().push() app.app_context().push()
return app return app

View file

@ -8,7 +8,7 @@ import settings
def create_session(): def create_session():
from wowfunding.orm.orm import base from funding.orm.orm import base
engine = sa.create_engine(settings.SQLALCHEMY_DATABASE_URI, echo=False, encoding="latin") engine = sa.create_engine(settings.SQLALCHEMY_DATABASE_URI, echo=False, encoding="latin")
session = scoped_session(sessionmaker(autocommit=False, session = scoped_session(sessionmaker(autocommit=False,
autoflush=False, autoflush=False,

View file

@ -1,5 +1,4 @@
from datetime import datetime from datetime import datetime
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
@ -7,7 +6,6 @@ import settings
base = declarative_base(name="Model") base = declarative_base(name="Model")
class User(base): class User(base):
__tablename__ = "users" __tablename__ = "users"
id = sa.Column('user_id', sa.Integer, primary_key=True) id = sa.Column('user_id', sa.Integer, primary_key=True)
@ -17,11 +15,10 @@ class User(base):
registered_on = sa.Column(sa.DateTime) registered_on = sa.Column(sa.DateTime)
admin = sa.Column(sa.Boolean, default=False) admin = sa.Column(sa.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")
def __init__(self, username, password, email): def __init__(self, username, password, email):
from wowfunding.factory import bcrypt from funding.factory import bcrypt
self.username = username self.username = username
self.password = bcrypt.generate_password_hash(password).decode('utf8') self.password = bcrypt.generate_password_hash(password).decode('utf8')
self.email = email self.email = email
@ -47,12 +44,12 @@ class User(base):
return self.id return self.id
def __repr__(self): def __repr__(self):
return '<User %r>' % self.username return self.username
@classmethod @classmethod
def add(cls, username, password, email): def add(cls, username, password, email):
from wowfunding.factory import db_session from funding.factory import db_session
from wowfunding.validation import val_username, val_email from funding.validation import val_username, val_email
try: try:
# validate incoming username/email # validate incoming username/email
@ -134,7 +131,7 @@ class Proposal(base):
@classmethod @classmethod
def find_by_id(cls, pid: int): def find_by_id(cls, pid: int):
from wowfunding.factory import db_session from funding.factory import db_session
q = cls.query q = cls.query
q = q.filter(Proposal.id == pid) q = q.filter(Proposal.id == pid)
result = q.first() result = q.first()
@ -149,21 +146,21 @@ class Proposal(base):
@property @property
def funds_target_usd(self): def funds_target_usd(self):
from wowfunding.bin.utils import Summary, wow_to_usd from funding.bin.utils import Summary, coin_to_usd
prices = Summary.fetch_prices() prices = Summary.fetch_prices()
if not prices: if not prices:
return return
return wow_to_usd(wows=self.funds_target, btc_per_wow=prices['wow-btc'], usd_per_btc=prices['btc-usd']) return coin_to_usd(amt=self.funds_target, btc_per_coin=prices['coin-btc'], usd_per_btc=prices['btc-usd'])
@property @property
def comment_count(self): def comment_count(self):
from wowfunding.factory import db_session from funding.factory import db_session
q = db_session.query(sa.func.count(Comment.id)) q = db_session.query(sa.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 wowfunding.factory import db_session from funding.factory import db_session
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 == None)
@ -184,46 +181,86 @@ class Proposal(base):
def balance(self): def balance(self):
"""This property retrieves the current funding status """This property retrieves the current funding status
of this proposal. It uses Redis cache to not spam the of this proposal. It uses Redis cache to not spam the
wownerod 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 wowfunding.bin.utils import Summary, wow_to_usd from funding.bin.utils import Summary, coin_to_usd
from wowfunding.factory import cache, db_session from funding.factory import cache, db_session
rtn = {'sum': 0.0, 'txs': [], 'pct': 0.0} rtn = {'sum': 0.0, 'txs': [], 'pct': 0.0}
cache_key = 'wow_balance_pid_%d' % self.id cache_key = 'coin_balance_pid_%d' % self.id
data = cache.get(cache_key) data = cache.get(cache_key)
if not data: if not data:
from wowfunding.bin.daemon import WowneroDaemon from funding.bin.daemon import Daemon
try: try:
data = WowneroDaemon().get_transfers_in(index=self.id) data = Daemon().get_transfers_in(index=self.id, proposal_id=self.id)
if not isinstance(data, dict): if not isinstance(data, dict):
print('error; get_transfers; %d' % self.id) print('error; get_transfers_in; %d' % self.id)
return rtn return rtn
cache.set(cache_key, data=data, expiry=300) cache.set(cache_key, data=data, expiry=300)
except: except:
print('error; get_transfers; %d' % self.id) print('error; get_transfers_in; %d' % self.id)
return rtn 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'] = wow_to_usd(wows=tx['amount_human'], btc_per_wow=prices['wow-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']) 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))
data['remaining'] = data['sum'] - self.funds_withdrew data['available'] = data['sum']
else: else:
data['pct'] = 0.0 data['pct'] = 0.0
data['remaining'] = 0.0 data['available'] = 0.0
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['remaining']: if data['available']:
data['remaining_pct'] = 100 / float(data['sum'] / data['remaining']) data['remaining_pct'] = 100 / float(data['sum'] / data['available'])
else:
data['remaining_pct'] = 0.0
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(index=self.id, proposal_id=self.id)
if not isinstance(data, dict):
print('error; get_transfers_out; %d' % self.id)
return rtn
cache.set(cache_key, data=data, expiry=300)
except:
print('error; get_transfers_out; %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))
data['spent'] = data['sum']
else:
data['pct'] = 0.0
data['spent'] = 0.0
if data['spent']:
data['remaining_pct'] = 100 / float(data['sum'] / data['spent'])
else: else:
data['remaining_pct'] = 0.0 data['remaining_pct'] = 0.0
@ -231,13 +268,13 @@ class Proposal(base):
@staticmethod @staticmethod
def generate_donation_addr(cls): def generate_donation_addr(cls):
from wowfunding.factory import db_session from funding.factory import db_session
from wowfunding.bin.daemon import WowneroDaemon from funding.bin.daemon import Daemon
if cls.addr_donation: if cls.addr_donation:
return cls.addr_donation return cls.addr_donation
try: try:
addr_donation = WowneroDaemon().get_address(index=cls.id) addr_donation = Daemon().get_address(index=cls.id, proposal_id=cls.id)
if not isinstance(addr_donation, dict): if not isinstance(addr_donation, dict):
raise Exception('get_address, needs dict; %d' % cls.id) raise Exception('get_address, needs dict; %d' % cls.id)
except Exception as ex: except Exception as ex:
@ -250,9 +287,19 @@ class Proposal(base):
db_session.flush() db_session.flush()
return addr_donation['address'] return addr_donation['address']
@staticmethod
def generate_proposal_subaccount(pid):
from funding.bin.daemon import Daemon
try:
Daemon().create_account(pid)
except Exception as ex:
print('error: %s' % str(ex))
return
@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 wowfunding.factory import db_session from funding.factory import db_session
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:
@ -298,7 +345,7 @@ class Payout(base):
from flask.ext.login import current_user from flask.ext.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 wowfunding.factory import db_session from funding.factory import db_session
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)
@ -339,17 +386,17 @@ class Comment(base):
@property @property
def ago(self): def ago(self):
from wowfunding.bin.utils_time import TimeMagic from funding.bin.utils_time import TimeMagic
return TimeMagic().ago(self.date_added) return TimeMagic().ago(self.date_added)
@staticmethod @staticmethod
def find_by_id(cid: int): def find_by_id(cid: int):
from wowfunding.factory import db_session from funding.factory import db_session
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 wowfunding.factory import db_session from funding.factory import db_session
from flask.ext.login import current_user from flask.ext.login import current_user
if current_user.id != user_id and not current_user.admin: if current_user.id != user_id and not current_user.admin:
raise Exception("no rights to remove this comment") raise Exception("no rights to remove this comment")
@ -364,7 +411,7 @@ class Comment(base):
@staticmethod @staticmethod
def lock(cid: int): def lock(cid: int):
from wowfunding.factory import db_session from funding.factory import db_session
from flask.ext.login import current_user from flask.ext.login import current_user
if not current_user.admin: if not current_user.admin:
raise Exception("admin required") raise Exception("admin required")
@ -383,7 +430,7 @@ class Comment(base):
@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.ext.login import current_user
from wowfunding.factory import db_session from funding.factory import db_session
if not message: if not message:
raise Exception("empty message") raise Exception("empty message")

View file

@ -4,8 +4,8 @@ from flask.ext.login import login_user , logout_user , current_user, login_requi
from flask_yoloapi import endpoint, parameter from flask_yoloapi import endpoint, parameter
import settings import settings
from wowfunding.factory import app, db_session from funding.factory import app, db_session
from wowfunding.orm.orm import Proposal, User, Comment from funding.orm.orm import Proposal, User, Comment
@app.route('/') @app.route('/')
@ -61,7 +61,7 @@ def proposal_comment(pid, text, cid):
@app.route('/proposal/<int:pid>/comment/<int:cid>') @app.route('/proposal/<int:pid>/comment/<int:cid>')
def propsal_comment_reply(cid, pid): def propsal_comment_reply(cid, pid):
from wowfunding.orm.orm import Comment from funding.orm.orm import Comment
c = Comment.find_by_id(cid) c = Comment.find_by_id(cid)
if not c or c.replied_to: if not c or c.replied_to:
return redirect(url_for('proposal', pid=pid)) return redirect(url_for('proposal', pid=pid))
@ -88,7 +88,7 @@ def proposal(pid):
parameter('title', type=str, required=True, location='json'), parameter('title', type=str, required=True, location='json'),
parameter('content', type=str, required=True, location='json'), parameter('content', type=str, required=True, location='json'),
parameter('pid', type=int, required=False, location='json'), parameter('pid', type=int, required=False, location='json'),
parameter('funds_target', type=float, required=True, location='json'), parameter('funds_target', type=str, required=True, location='json'),
parameter('addr_receiving', type=str, required=True, location='json'), parameter('addr_receiving', type=str, required=True, location='json'),
parameter('category', type=str, required=True, location='json'), parameter('category', type=str, required=True, location='json'),
parameter('status', type=int, required=True, location='json', default=1) parameter('status', type=int, required=True, location='json', default=1)
@ -99,8 +99,9 @@ def proposal_api_add(title, content, pid, funds_target, addr_receiving, category
if current_user.is_anonymous: if current_user.is_anonymous:
return make_response(jsonify('err'), 500) return make_response(jsonify('err'), 500)
if len(title) <= 10: if len(title) <= 8:
return make_response(jsonify('title too short'), 500) return make_response(jsonify('title too short'), 500)
if len(content) <= 20: if len(content) <= 20:
return make_response(jsonify('content too short'), 500) return make_response(jsonify('content too short'), 500)
@ -114,12 +115,14 @@ def proposal_api_add(title, content, pid, funds_target, addr_receiving, category
return make_response(jsonify('no rights to change status'), 500) return make_response(jsonify('no rights to change status'), 500)
try: try:
from wowfunding.bin.anti_xss import such_xss from funding.bin.anti_xss import such_xss
content_escaped = such_xss(content) content_escaped = such_xss(content)
html = markdown2.markdown(content_escaped, safe_mode=True) html = markdown2.markdown(content_escaped, safe_mode=True)
except Exception as ex: except Exception as ex:
return make_response(jsonify('markdown error'), 500) return make_response(jsonify('markdown error'), 500)
if pid: if pid:
p = Proposal.find_by_id(pid=pid) p = Proposal.find_by_id(pid=pid)
if not p: if not p:
@ -146,13 +149,22 @@ def proposal_api_add(title, content, pid, funds_target, addr_receiving, category
p.status = status p.status = status
p.last_edited = datetime.now() p.last_edited = datetime.now()
else: else:
if funds_target <= 1: try:
return make_response(jsonify('proposal asking less than 1 error :)'), 500) funds_target = float(funds_target)
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) != 97: if len(addr_receiving) != 97:
return make_response(jsonify('faulty addr_receiving 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)
proposalID = current_user
addr_donation = Proposal.generate_proposal_subaccount(proposalID)
p.addr_donation = addr_donation
p.html = html p.html = html
p.last_edited = datetime.now() p.last_edited = datetime.now()
p.funds_target = funds_target p.funds_target = funds_target
@ -161,11 +173,13 @@ def proposal_api_add(title, content, pid, funds_target, addr_receiving, category
p.status = status p.status = status
db_session.add(p) db_session.add(p)
db_session.commit() db_session.commit()
db_session.flush() db_session.flush()
# reset cached statistics # reset cached statistics
from wowfunding.bin.utils import Summary from funding.bin.utils import Summary
Summary.fetch_stats(purge=True) Summary.fetch_stats(purge=True)
return make_response(jsonify({'url': url_for('proposal', pid=p.id)})) return make_response(jsonify({'url': url_for('proposal', pid=p.id)}))
@ -254,7 +268,7 @@ def login(username, password):
if request.method == 'GET': if request.method == 'GET':
return make_response(render_template('login.html')) return make_response(render_template('login.html'))
from wowfunding.factory import bcrypt from funding.factory import bcrypt
user = User.query.filter_by(username=username).first() user = User.query.filter_by(username=username).first()
if user is None or not bcrypt.check_password_hash(user.password, password): if user is None or not bcrypt.check_password_hash(user.password, password):
flash('Username or Password is invalid', 'error') flash('Username or Password is invalid', 'error')

View file

@ -7,23 +7,27 @@ body {
padding-top: 56px; padding-top: 56px;
} }
} }
.center {
text-align: center;
margin: 0 auto 0 auto;
}
.navbar { .navbar {
padding: .1rem 1rem; padding: .1rem 1rem;
} }
.navbar .navbar-brand img#logo{ .navbar .navbar-brand img#logo{
width:28px; width: 32px;
height:28px; height: 32px;
float:left; display: inline-block;
vertical-align: middle;
} }
.navbar .navbar-brand img#text{ .navbar .navbar-brand img#text{
width:235px; width:235px;
height:13px; height:13px;
float:left;
margin-top: 8px; margin-top: 8px;
margin-left:8px; margin-left:8px;
display: inline-block;
} }
.navbar-nav .nav-link{ .navbar-nav .nav-link{
@ -55,7 +59,7 @@ body {
margin-top: 6px; margin-top: 6px;
font-weight: bold; font-weight: bold;
color: #ff0000; color: #ff0000;
font-size: 22px; font-size: 18px;
margin-bottom: 0; margin-bottom: 0;
} }
@ -268,7 +272,12 @@ a {
background-clip: padding-box; background-clip: padding-box;
border: 1px solid #008926; border: 1px solid #008926;
} }
.form-group {
margin-bottom: 1rem;
}
.form-group:last-of-type {
margin-bottom:2.5rem;
}
/*fuku chrome*/ /*fuku chrome*/
input { input {
outline:none; outline:none;

View file

@ -24,7 +24,7 @@
</p> </p>
<b>Example:</b> <b>Example:</b>
<pre>curl -vvX GET 'https://funding.wownero.com/api/1/convert/wow-usd?wow=1000'</pre> <pre>curl -vvX GET 'https://funding.wownero.com/api/1/convert/wow-usd?amount=1000'</pre>
<b>Response:</b> <b>Response:</b>
<pre>{"usd": 6.7}</pre> <pre>{"usd": 6.7}</pre>

View file

@ -14,7 +14,7 @@
padding-left:4px; padding-left:4px;
} }
</style> </style>
<div class="col-lg-12" id="xox"> <div class="col-lg-8" id="xox">
<h3>Secure login </h3> <h3>Secure login </h3>
<img src="/static/nasa.png"/> <img src="/static/nasa.png"/>
<img src="/static/cyber.png"/> <img src="/static/cyber.png"/>
@ -46,9 +46,10 @@
</div> </div>
</form> </form>
</div> </div>
<div class="col-lg-12"> <div class="col-lg-5 center">
<a href="/register">Or register here</a> <a href="/register">Or register here</a>
</div> </div>
{% include 'sidebar.html' %}
</div> </div>
</div> </div>
<br> <br>

View file

@ -6,7 +6,7 @@
<th>Username</th> <th>Username</th>
<th id="date">Date</th> <th id="date">Date</th>
{% if _proposals and _proposals[0].status >= 2 %} {% if _proposals and _proposals[0].status >= 2 %}
<th style="text-align: right;">Funding</th> <th style="text-align: right;">Funded</th>
{% else %} {% else %}
<th></th> <th></th>
{% endif %} {% endif %}

View file

@ -11,11 +11,6 @@
<!-- Title --> <!-- Title -->
<h1 class="mt-4" style="margin-bottom: 0.1rem;"> <h1 class="mt-4" style="margin-bottom: 0.1rem;">
{{ proposal.headline }} {{ proposal.headline }}
<div id="point-wow-left">
<img src="/static/point-left.png" style="margin-left: 10px;width: 60px;">
<span style="color: #fc4dff;font-size: 16px;font-style: italic;font-weight: bold;margin-left: 6px;">wow</span>
</div>
</h1> </h1>
<p> <p>
@ -78,8 +73,8 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Progress</td> <td>Funded</td>
<td><span class="badge">{{proposal.balance['pct'] |round}} %</span></td> <td><span class="badge">{{proposal.balance['pct'] | round or 0}} %</span></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -100,17 +95,43 @@
<hr> <hr>
</div> </div>
<div class="col-lg-8"> <div class="col-lg-8">
{{proposal.balance['remaining'] or 0}} WOW available {{proposal.balance['available']|round(3) or 0 }} WOW Raised
{% if (proposal.funds_target-proposal.balance['available']|float|round(3)) > 0 %}
({{ (proposal.funds_target-proposal.balance['available']|float|round(3)|int) }} WOW until goal)
{% else %}
({{ (proposal.balance['available']-proposal.funds_target|float|round(3)|int) }} WOW past goal!)
{% endif %}
<div class="progress">
<div class="progress-bar progress-warning progress-bar" style="width: {{proposal.balance['pct']}}%;">
</div>
</div>
<hr>
</div>
<br/>
<br/>
<div class="col-lg-8">
{{proposal.spends['spent']|round(3) or 0}} WOW Paid out
<div class="progress">
<div class="progress-bar progress-warning progress-bar" style="width: {{proposal.spends['spent_remaining_pct']}}%;">
</div>
</div>
<hr>
</div>
<div class="col-lg-8">
{{(proposal.balance['available']-proposal.spends['spent']) |round(3) or 0}} WOW Available to 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>
</div> </div>
<hr>
</div> </div>
<br/>
</div> </div>
<div class="row" style="margin-top:16px;"> <div class="row" style="margin-top:16px;">
<div class="col-lg-12"> <div class="col-lg-12">
Donatation 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>
</div> </div>
@ -122,7 +143,6 @@
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<div class="alert alert-danger"> <div class="alert alert-danger">
<img src="/static/doge_head.png" style="width: 64px;margin-right: 8px;">
This proposal is disabled. This proposal is disabled.
</div> </div>
</div> </div>
@ -159,7 +179,7 @@
<br> <br>
<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>
<span style="float:right;color:#008926;font-weight:bold;"> <span style="float:right;color:#008926;font-weight:bold;">
+ {{tx['amount_human']|round(2)}} WOW + {{tx['amount_human']|round(3)}} WOW
{% if 'amount_usd' in tx %} {% if 'amount_usd' in tx %}
<small style="color: black"> <small style="color: black">
➞ $ {{tx['amount_usd']}} ➞ $ {{tx['amount_usd']}}
@ -176,6 +196,37 @@
<!-- /.row --> <!-- /.row -->
{% endif %} {% endif %}
{% if proposal.spends['txs'] %}
<div class="row">
<div class="col-md-12">
<div class="card my-6" id="incoming_txs">
<h5 class="card-header">Outgoing transactions <small>({{proposal.spends['txs']|length}})</small></h5>
<div class="card-body">
<ul class="list-group">
{% for tx in proposal.spends['txs'] %}
<li class="list-group-item">
{{tx['datetime'].strftime('%Y-%m-%d %H:%M')}}
<span style="float:right"><b>Blockheight</b>: {{tx['height']}}</span>
<br>
<a target="_blank" href="https://explore.wownero.com/tx/{{tx['txid']}}">{{tx['txid'][:32]}}...</a>
<span style="float:right;color:#890000;font-weight:bold;">
- {{tx['amount_human']|round(3)}} WOW
{% if 'amount_usd' in tx %}
<small style="color: black">
➞ $ {{tx['amount_usd']}}
</small>
{% endif %}
</span>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
<!-- /.row -->
{% endif %}
</div> </div>
<script> <script>

View file

@ -71,6 +71,21 @@
</div> </div>
</div> </div>
<!-- Side Widget -->
<div class="card my-4">
<h5 class="card-header">Newest Users</h5>
<div class="card-body">
<ul class="b">
{% for user in newest_users %}
<li>
<a href="/user/{{ user.username }}"> {{ user.username }} </a>
</li>
</a>
{% endfor %}
</li>
</div>
</div>
<script> <script>
var search_input = document.getElementById("search_input"); var search_input = document.getElementById("search_input");

View file

@ -4,35 +4,62 @@
{% if user %} {% if user %}
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-lg-8"> <div class="col-lg-12">
<span class="form-text text-muted" style="margin-top: -2px;"> <span class="form-text text-muted" style="margin-top: -2px;">
Details for '{{user.username}}' Proposals made by '{{user.username}}'
</span> </span>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-8"> <div class="col-lg-12">
{% if user.proposals %} {% if user.proposals %}
<table class="table table-proposal table-hover" style="margin-bottom:6px;"> <table class="table table-proposal table-hover" style="margin-bottom:6px;">
<tbody> <tbody>
{% for p in user.proposals %} <th>Proposal</th>
<th>Category</th>
<th>Target Amount</th>
<th>Amount Raised</th>
<th>Date</th>
{% for p in user.proposals | sort(attribute='date_added', reverse=True) %}
<tr> <tr>
<td><b><a href="/proposal/{{ p.id }}">{{ p.headline }}</a></b></td> <td><b><a href="/proposal/{{ p.id }}">{{ p.headline | truncate(42)}}</a></b></td>
<td><a href="/user/{{ p.user.username }}">{{ p.user.username }}</a></td> <td><a href="/proposals?cat={{ p.category }}">{{ p.category |capitalize}}</a></td>
<td>{{p.funds_target}}</td>
<td>{{p.funds_progress | round(3, 'floor')}}%</td>
<td>{{ p.date_added.strftime('%Y-%m-%d') }}</td> <td>{{ p.date_added.strftime('%Y-%m-%d') }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
This user did not submit any proposals yet. This user has not submitted any proposals yet.
{% endif %}
<hr>
</div>
</div>
<div class="row">
<div class="col-lg-12">
Comments made by {{user.username}}
{% if user.comments %}
<table class="table table-proposal table-hover" style="margin-bottom:6px;">
<tbody>
<th>Comment</th>
<th>Proposal</th>
<th>Date</th>
{% for y in user.comments | sort(attribute='date_added', reverse=True) %}
<tr>
<td><b><a href="/proposal/{{y.proposal.id}}#comment-{{ y.id }}">{{ y.message | truncate(32)}}</a></b></td>
<td><a href="/proposal/{{y.proposal.id}}">#{{y.proposal.id }} {{ y.proposal.headline | truncate(32) }}</a></td>
<td>{{ y.date_added.strftime('%Y-%m-%d') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
This user has not submitted any proposals yet.
{% endif %} {% endif %}
</div> </div>
{% include 'sidebar.html' %}
</div> </div>
<br> <br>
</div> </div>

View file

@ -1,4 +1,4 @@
from wowfunding.factory import create_app from funding.factory import create_app
import settings import settings
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -3,30 +3,40 @@ import socket
import collections import collections
import os import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SECRET = 'changeme' SECRET = ''
DEBUG = True DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', 'postgresql://postgres:@localhost/ffs') COINCODE = ''
PSQL_USER = ''
PSQL_PASS = ''
PSQL_DB = ''
SESSION_COOKIE_NAME = os.environ.get('WOW_SESSION_COOKIE_NAME', 'wow_id') SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', 'postgresql://{user}:{pw}@localhost/{db}').format(user=PSQL_USER, pw=PSQL_PASS, db=PSQL_DB)
SESSION_PREFIX = os.environ.get('WOW_SESSION_PREFIX', 'session:')
REDIS_HOST = os.environ.get('WOW_REDIS_HOST', '127.0.0.1') SESSION_COOKIE_NAME = os.environ.get('{coincode}_SESSION_COOKIE_NAME', '{coincode}_id').format(coincode=COINCODE)
REDIS_PORT = int(os.environ.get('WOW_REDIS_PORT', 6379)) SESSION_PREFIX = os.environ.get('{coincode}_SESSION_PREFIX', 'session:').format(coincode=COINCODE)
REDIS_PASSWD = os.environ.get('WOW_REDIS_PASSWD', None)
BIND_HOST = os.environ.get("WOW_BIND_HOST", "127.0.0.1") REDIS_HOST = os.environ.get('REDIS_HOST', '127.0.0.1')
REDIS_PORT = int(os.environ.get('REDIS_PORT', 6379))
REDIS_PASSWD = os.environ.get('REDIS_PASSWD', None)
BIND_HOST = os.environ.get("BIND_HOST", "0.0.0.0")
if not BIND_HOST: if not BIND_HOST:
raise Exception("WOW_BIND_HOST missing") raise Exception("BIND_HOST missing")
BIND_PORT = os.environ.get("WOW_BIND_PORT", 5004) BIND_PORT = os.environ.get("BIND_PORT", 5004)
if not BIND_PORT: if not BIND_PORT:
raise Exception("WOW_BIND_PORT missing") raise Exception("BIND_PORT missing")
HOSTNAME = os.environ.get("WOW_HOSTNAME", socket.gethostname()) HOSTNAME = os.environ.get("{coincode}_HOSTNAME", socket.gethostname()).format(coincode=COINCODE)
RPC_LOCATION = "http://127.0.0.1:45678/json_rpc" # If using a local RPC, no need for --rpc-login credentials unless you're binding wallet-rpc to 0.0.0.0. If you are, you're bad.
# elif, remote wallet-rpc, enable --rpc-login and enter credentials below.
RPC_HOST = '127.0.0.1'
RPC_PORT = '11182'
RPC_LOCATION = "http://{host}:{rpc_port}/json_rpc".format(host=RPC_HOST, rpc_port=RPC_PORT)
RPC_USERNAME = ""
RPC_PASSWORD = ""
FUNDING_CATEGORIES = [ FUNDING_CATEGORIES = [
'wallets', 'wallets',
@ -52,7 +62,7 @@ What problem(s) are you trying to solve?
#### How much? #### How much?
What is the total cost in WOW? List expenses per item. Total hours of work and per hour rate. What exchange rates are you using? What is the total cost in {coincode}? List expenses per item. Total hours of work and per hour rate. What exchange rates are you using?
#### What? #### What?
@ -69,4 +79,4 @@ What will be delivered? What goals will be reached?
#### Why you? #### Why you?
What skills and experience do you have? What skills and experience do you have?
""".strip() """.strip().format(coincode=COINCODE)