- 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
This commit is contained in:
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)
|
||||||
|
|
||||||
ix_proposal_id = sa.Index("ix_proposal_id", proposal_id)
|
date_sent = db.Column(db.TIMESTAMP, default=datetime.now)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
p.addr_donation = Proposal.generate_donation_addr(p)
|
assert 'result' in blob
|
||||||
|
assert 'integrated_address' in blob['result']
|
||||||
|
assert 'payment_id' in blob['result']
|
||||||
|
except Exception as ex:
|
||||||
|
raise
|
||||||
|
|
||||||
db_session.commit()
|
p.addr_donation = blob['result']['integrated_address']
|
||||||
db_session.flush()
|
p.payment_id = blob['result']['payment_id']
|
||||||
|
|
||||||
# reset cached statistics
|
db.session.add(p)
|
||||||
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)}))
|
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…
Reference in New Issue