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

- AEON (camthegeek) has contributed commits to upstream; WOW is grateful. + AEON (camthegeek) has contributed commits upstream; WOW is grateful.

diff --git a/funding/templates/base.html b/funding/templates/base.html index 6d57fe7..91885c7 100644 --- a/funding/templates/base.html +++ b/funding/templates/base.html @@ -64,7 +64,7 @@
-

WOW 2018

+

WOW 2018-2020

diff --git a/funding/templates/login.html b/funding/templates/login.html index 8f23a0f..9f4866c 100644 --- a/funding/templates/login.html +++ b/funding/templates/login.html @@ -27,7 +27,7 @@
-
+
@@ -46,10 +46,13 @@
+
+ Forgot your password? Out of luck! Create a new account lol +
diff --git a/funding/templates/proposal/macros/transaction.html b/funding/templates/proposal/macros/transaction.html index 227e264..ee7fd05 100644 --- a/funding/templates/proposal/macros/transaction.html +++ b/funding/templates/proposal/macros/transaction.html @@ -1,20 +1,28 @@ {% macro tx_item(tx) %}
  • - - {{tx['datetime'].strftime('%Y-%m-%d %H:%M')}} - - Blockheight: - {% if tx['type'] == 'pool' %} - soon^tm - {% else %} - {{tx['height']}} - {% endif %} + {% if tx['type'] == 'pool' %} + soon^tm + {% elif tx['type'] == 'out' %} + hidden + {% else %} + {{tx['block_height']}} + {% endif %}
    + {% if tx['type'] in ['in', 'pool'] %} {{tx['txid'][:32]}}... + {% else %} + {% set lulz = [ + 'vodka', 'hookers', 'booze', 'strippers', 'new lambo', + 'new ferrari', 'new villa', 'new vacation home', 'new tesla', + 'new watch', 'new home cinema set', 'throphy wife', 'drugs'] + %} + Sent to author. Enjoy the {{ lulz|random }}! + {% endif %} + {% if tx['type'] in ['in', 'pool'] %} + diff --git a/funding/templates/proposal/proposal.html b/funding/templates/proposal/proposal.html index 6569e64..19fcdb1 100644 --- a/funding/templates/proposal/proposal.html +++ b/funding/templates/proposal/proposal.html @@ -97,11 +97,13 @@ {{proposal.balance['available']|round(3) or 0 }} WOW Raised {% set remaining = proposal.funds_target - proposal.balance['available']|float|round(3) %} + {% if remaining > 0 %} ({{ (proposal.funds_target - proposal.balance['available']|float|round(3)|int) }} WOW until goal) {% elif remaining < 0 %} ({{ (proposal.balance['available']-proposal.funds_target|float|round(3)|int) }} WOW past goal!) {% endif %} +
    @@ -113,15 +115,16 @@
    - {{proposal.spends['spent']|round(3) or 0}} WOW Paid out + {{ proposal.spends['amount'] }} WOW Paid out ({{ proposal.spends['pct']|round(1) }}%)
    -
    + +

    - {{(proposal.balance['available']-proposal.spends['spent']) |round(3) or 0}} WOW Available to Payout + {{ (proposal.balance['available']-proposal.spends['amount']) |round(3) or 0}} WOW Available for payout :-)
    @@ -185,15 +188,15 @@ {% endif %} - {% if proposal.spends['txs'] %} + {% if proposal.payouts %}
    -
    Outgoing transactions ({{proposal.spends['txs']|length}})
    +
    Outgoing transactions
      - {% for tx in proposal.spends['txs'] %} - {{ tx_item(tx) }} + {% for payout in proposal.payouts %} + {{ tx_item(payout.as_tx) }} {% endfor %}
    diff --git a/requirements.txt b/requirements.txt index 12aad6b..ec3b4eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ sqlalchemy==1.3.4 -flask==0.12.3 +flask flask-yoloapi==0.1.5 flask_session flask-login @@ -12,3 +12,6 @@ requests pyqrcode pypng pillow-simd +Flask-Caching +flask-sqlalchemy +sqlalchemy_json \ No newline at end of file diff --git a/settings.py_example b/settings.py_example index 0132121..7e5efaf 100644 --- a/settings.py_example +++ b/settings.py_example @@ -7,11 +7,12 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__)) SECRET = '' DEBUG = True -COIN_ADDRESS_LENGTH = 97 +COIN_ADDRESS_LENGTH = [97, 108] COINCODE = '' -PSQL_USER = '' -PSQL_PASS = '' +PSQL_HOST = "127.0.0.1:5432" PSQL_DB = '' +PSQL_USER = 'postgres' +PSQL_PASS = '' SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', 'postgresql://{user}:{pw}@localhost/{db}').format(user=PSQL_USER, pw=PSQL_PASS, db=PSQL_DB)