Initial commit

This commit is contained in:
Sander Ferdinand 2018-06-26 23:48:25 +02:00
commit edb3d8f57b
55 changed files with 15544 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
*__pycache__*
settings.py
.cache
.env
htmlcov
.coverage

24
README.md Normal file
View file

@ -0,0 +1,24 @@
# WOW funding
## installation (locally)
Create a Postgres user/database for this project
```
sudo apt install python-virtualenv python3 redis-server postgresql-server-dev-*
git clone ...
cd ffs_site
virtualenv -p /usr/bin/python3
source venv/bin/activate
pip install -r requirements.txt
cp settings.py_example settings.py
- change settings accordingly
python run_dev.py
```
### to-do
- rate limit posting of proposals per user
https://imgur.com/KKzFQe9
https://imgur.com/Dl3wRgD

11
requirements.txt Normal file
View file

@ -0,0 +1,11 @@
sqlalchemy==1.2.7
flask==0.12.3
flask-yoloapi==0.1.4
flask_session
flask-login
flask-bcrypt
redis
gunicorn
psycopg2
markdown2
requests

7
run_dev.py Normal file
View file

@ -0,0 +1,7 @@
from wowfunding.factory import create_app
import settings
if __name__ == '__main__':
app = create_app()
app.run(host=settings.BIND_HOST, port=settings.BIND_PORT,
debug=settings.DEBUG, use_reloader=False)

35
settings.py_example Normal file
View file

@ -0,0 +1,35 @@
import logging
import socket
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', 'postgresql://postgres:@localhost/ffs')
SESSION_COOKIE_NAME = os.environ.get('WOW_SESSION_COOKIE_NAME', 'wow_id')
SESSION_PREFIX = os.environ.get('WOW_SESSION_PREFIX', 'session:')
REDIS_HOST = os.environ.get('WOW_REDIS_HOST', '127.0.0.1')
REDIS_PORT = int(os.environ.get('WOW_REDIS_PORT', 6379))
REDIS_PASSWD = os.environ.get('WOW_REDIS_PASSWD', None)
BIND_HOST = os.environ.get("WOW_BIND_HOST", "127.0.0.1")
if not BIND_HOST:
raise Exception("WOW_BIND_HOST missing")
BIND_PORT = os.environ.get("WOW_BIND_PORT", 5004)
if not BIND_PORT:
raise Exception("WOW_BIND_PORT missing")
HOSTNAME = os.environ.get("WOW_HOSTNAME", socket.gethostname())
RPC_LOCATION = "http://127.0.0.1:45678/json_rpc"
FUNDING_CATEGORIES = [
'wallets',
'marketing',
'core',
'misc',
'design'
]

0
wowfunding/__init__.py Normal file
View file

25
wowfunding/api.py Normal file
View file

@ -0,0 +1,25 @@
from datetime import datetime
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_yoloapi import endpoint, parameter
import settings
from wowfunding.factory import app, db_session
from wowfunding.orm.orm import Proposal, User
@app.route('/api/1/proposals')
@endpoint.api(
parameter('status', type=int, location='args', default=0),
parameter('cat', type=str, location='args'),
parameter('limit', type=int, location='args', default=20),
parameter('offset', type=int, location='args', default=0)
)
def api_proposals_get(status, cat, limit, offset):
try:
proposals = Proposal.find_by_args(status=status, cat=cat, limit=limit, offset=offset)
except Exception as ex:
print(ex)
return 'error', 500
return [p.json for p in proposals]

View file

210
wowfunding/bin/anti_xss.py Normal file
View file

@ -0,0 +1,210 @@
# -*- coding: utf-8 -*-
"""
Python 富文本XSS过滤类
@package XssHtml
@version 0.1
@link http://phith0n.github.io/python-xss-filter
@since 20150407
@copyright (c) Phithon All Rights Reserved
Based on native Python module HTMLParser purifier of HTML, To Clear all javascript in html
You can use it in all python web framework
Written by Phithon <root@leavesongs.com> in 2015 and placed in the public domain.
phithon <root@leavesongs.com> 编写于20150407
From: XDSEC <www.xdsec.org> & 离别歌 <www.leavesongs.com>
GitHub Pages: https://github.com/phith0n/python-xss-filter
Usage:
parser = XssHtml()
parser.feed('<html code>')
parser.close()
html = parser.getHtml()
print html
Requirements
Python 2.6+ or 3.2+
Cannot defense xss in browser which is belowed IE7
浏览器版本IE7+ 或其他浏览器无法防御IE6及以下版本浏览器中的XSS
"""
import re
try:
from html.parser import HTMLParser
except:
from HTMLParser import HTMLParser
class XssHtml(HTMLParser):
allow_tags = ['a', 'img', 'br', 'strong', 'b', 'code', 'pre',
'p', 'div', 'em', 'span', 'h1', 'h2', 'h3', 'h4',
'h5', 'h6', 'blockquote', 'ul', 'ol', 'tr', 'th', 'td',
'hr', 'li', 'u', 'embed', 's', 'table', 'thead', 'tbody',
'caption', 'small', 'q', 'sup', 'sub']
common_attrs = ["style", "class", "name"]
nonend_tags = ["img", "hr", "br", "embed"]
tags_own_attrs = {
"img": ["src", "width", "height", "alt", "align"],
"a": ["href", "target", "rel", "title"],
"embed": ["src", "width", "height", "type", "allowfullscreen", "loop", "play", "wmode", "menu"],
"table": ["border", "cellpadding", "cellspacing"],
}
_regex_url = re.compile(r'^(http|https|ftp)://.*', re.I | re.S)
_regex_style_1 = re.compile(r'(\\|&#|/\*|\*/)', re.I)
_regex_style_2 = re.compile(r'e.*x.*p.*r.*e.*s.*s.*i.*o.*n', re.I | re.S)
def __init__(self, allows=[]):
HTMLParser.__init__(self)
self.allow_tags = allows if allows else self.allow_tags
self.result = []
self.start = []
self.data = []
def getHtml(self):
"""
Get the safe html code
"""
for i in range(0, len(self.result)):
self.data.append(self.result[i])
return ''.join(self.data)
def handle_startendtag(self, tag, attrs):
self.handle_starttag(tag, attrs)
def handle_starttag(self, tag, attrs):
if tag not in self.allow_tags:
return
end_diagonal = ' /' if tag in self.nonend_tags else ''
if not end_diagonal:
self.start.append(tag)
attdict = {}
for attr in attrs:
attdict[attr[0]] = attr[1]
attdict = self._wash_attr(attdict, tag)
if hasattr(self, "node_%s" % tag):
attdict = getattr(self, "node_%s" % tag)(attdict)
else:
attdict = self.node_default(attdict)
attrs = []
for (key, value) in attdict.items():
attrs.append('%s="%s"' % (key, self._htmlspecialchars(value)))
attrs = (' ' + ' '.join(attrs)) if attrs else ''
self.result.append('<' + tag + attrs + end_diagonal + '>')
def handle_endtag(self, tag):
if self.start and tag == self.start[len(self.start) - 1]:
self.result.append('</' + tag + '>')
self.start.pop()
def handle_data(self, data):
self.result.append(self._htmlspecialchars(data))
def handle_entityref(self, name):
if name.isalpha():
self.result.append("&%s;" % name)
def handle_charref(self, name):
if name.isdigit():
self.result.append("&#%s;" % name)
def node_default(self, attrs):
attrs = self._common_attr(attrs)
return attrs
def node_a(self, attrs):
attrs = self._common_attr(attrs)
attrs = self._get_link(attrs, "href")
attrs = self._set_attr_default(attrs, "target", "_blank")
attrs = self._limit_attr(attrs, {
"target": ["_blank", "_self"]
})
return attrs
def node_embed(self, attrs):
attrs = self._common_attr(attrs)
attrs = self._get_link(attrs, "src")
attrs = self._limit_attr(attrs, {
"type": ["application/x-shockwave-flash"],
"wmode": ["transparent", "window", "opaque"],
"play": ["true", "false"],
"loop": ["true", "false"],
"menu": ["true", "false"],
"allowfullscreen": ["true", "false"]
})
attrs["allowscriptaccess"] = "never"
attrs["allownetworking"] = "none"
return attrs
def _true_url(self, url):
if self._regex_url.match(url):
return url
else:
return "http://%s" % url
def _true_style(self, style):
if style:
style = self._regex_style_1.sub('_', style)
style = self._regex_style_2.sub('_', style)
return style
def _get_style(self, attrs):
if "style" in attrs:
attrs["style"] = self._true_style(attrs.get("style"))
return attrs
def _get_link(self, attrs, name):
if name in attrs:
attrs[name] = self._true_url(attrs[name])
return attrs
def _wash_attr(self, attrs, tag):
if tag in self.tags_own_attrs:
other = self.tags_own_attrs.get(tag)
else:
other = []
_attrs = {}
if attrs:
for (key, value) in attrs.items():
if key in self.common_attrs + other:
_attrs[key] = value
return _attrs
def _common_attr(self, attrs):
attrs = self._get_style(attrs)
return attrs
def _set_attr_default(self, attrs, name, default=''):
if name not in attrs:
attrs[name] = default
return attrs
def _limit_attr(self, attrs, limit={}):
for (key, value) in limit.items():
if key in attrs and attrs[key] not in value:
del attrs[key]
return attrs
def _htmlspecialchars(self, html):
return html.replace("<", "&lt;")\
.replace(">", "&gt;")\
.replace('"', "&quot;")\
.replace("'", "&#039;")
def such_xss(inp):
"""Very ghetto anti-xss countermeasures. Possibly unsafe! :D
Needs testing, or a proper solution. wow."""
parser = XssHtml()
parser.feed(inp)
parser.close()
result = parser.getHtml()
# oh noes teh markdown blockquotes
lines = result.split('\n')
_lines = []
for line in lines:
if line.startswith('&gt;'):
line = line[4:]
line = '>%s' % line
_lines.append(line)
return "\n".join(_lines)

52
wowfunding/bin/daemon.py Normal file
View file

@ -0,0 +1,52 @@
import settings
import requests
class WowneroDaemon:
def __init__(self):
self.url = settings.RPC_LOCATION
self.headers = {"User-Agent": "Mozilla"}
def create_address(self, label_name):
data = {
'method': 'create_address',
'params': {'account_index': 0, 'label': label_name},
'jsonrpc': '2.0',
'id': '0'
}
return self._make_request(data)
def get_address(self, index):
data = {
'method': 'get_address',
'params': {'address_index': [index], 'account_index': 0},
'jsonrpc': '2.0',
'id': '0'
}
try:
result = self._make_request(data)
return next(z for z in result['result']['addresses'] if z['address_index'] == index)
except:
return
def get_transfers_in(self, index):
data = {
"method":"get_transfers",
"params": {"in": True, "account_index": 0, "subaddr_indices": [index]},
"jsonrpc": "2.0",
"id": "0",
}
data = self._make_request(data)
data = data['result'].get('in', [])
for d in data:
d['amount_human'] = float(d['amount'])/1e11
return {
'sum': sum([float(z['amount'])/1e11 for z in data]),
'txs': data
}
def _make_request(self, data):
r = requests.post(self.url, json=data, headers=self.headers)
r.raise_for_status()
return r.json()

8
wowfunding/bin/utils.py Normal file
View file

@ -0,0 +1,8 @@
from flask.json import JSONEncoder
from datetime import datetime, date
def json_encoder(obj):
if isinstance(obj, (datetime, date)):
return obj.isoformat()
raise TypeError ("Type %s not serializable" % type(obj))

View file

@ -0,0 +1,67 @@
from datetime import datetime
from flask import session, g
import settings
from wowfunding.factory import app, db_session, summary_data
from wowfunding.orm.orm import Proposal, User
@app.context_processor
def template_vars():
global summary_data
return dict(summary_data=summary_data[1])
def fetch_summary():
global summary_data
if summary_data:
if (datetime.now() - summary_data[0]).total_seconds() <= 120:
return
data = {}
categories = settings.FUNDING_CATEGORIES
statuses = [0, 1, 2]
for cat in categories:
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 = 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()
summary_data = [datetime.now(), data]
@app.before_request
def before_request():
fetch_summary()
@app.after_request
def after_request(res):
res.headers.add('Accept-Ranges', 'bytes')
if settings.DEBUG:
res.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
res.headers['Pragma'] = 'no-cache'
res.headers['Expires'] = '0'
res.headers['Cache-Control'] = 'public, max-age=0'
return res
@app.teardown_appcontext
def shutdown_session(**kwargs):
db_session.remove()
@app.errorhandler(404)
def error(err):
return 'Error', 404

60
wowfunding/cache.py Normal file
View file

@ -0,0 +1,60 @@
import json
import redis
from flask_session import RedisSessionInterface
import settings
from wowfunding.bin.utils import json_encoder
def redis_args():
args = {
"host": settings.REDIS_HOST,
"port": settings.REDIS_PORT,
'socket_connect_timeout': 2,
'socket_timeout': 2,
'retry_on_timeout': True
}
if settings.REDIS_PASSWD:
args["password"] = settings.REDIS_PASSWD
return args
class JsonRedisSerializer:
@staticmethod
def loads(val):
try:
return json.loads(val).get("wow", {})
except ValueError:
return
@staticmethod
def dumps(val):
try:
return json.dumps({"wow": val})
except ValueError:
return
class JsonRedis(RedisSessionInterface):
serializer = JsonRedisSerializer
def __init__(self, key_prefix, use_signer=False, decode_responses=True):
super(JsonRedis, self).__init__(
redis=redis.Redis(decode_responses=decode_responses, **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)

67
wowfunding/factory.py Normal file
View file

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
import settings
from werkzeug.contrib.fixers import ProxyFix
from flask import Flask
app = None
sentry = None
cache = None
db_session = None
bcrypt = None
summary_data = []
def create_app():
global app
global db_session
global sentry
global cache
global bcrypt
from wowfunding.orm.connect import create_session
db_session = create_session()
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app)
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
# flask-login
from flask.ext.login import LoginManager
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
from flask.ext.bcrypt import Bcrypt
bcrypt = Bcrypt(app)
@login_manager.user_loader
def load_user(_id):
from wowfunding.orm.orm import User
return User.query.get(int(_id))
# session init
from wowfunding.cache import JsonRedis, WowCache
app.session_interface = JsonRedis(key_prefix=app.config['SESSION_PREFIX'], use_signer=False)
cache = WowCache()
# template vars
@app.context_processor
def _bootstrap_templating():
from flask.ext.login import current_user
return dict(logged_in=current_user.is_authenticated,
current_user=current_user)
# import routes
from wowfunding import routes
from wowfunding import api
from wowfunding.bin import utils_request
# generate some statistics
utils_request.fetch_summary()
app.app_context().push()
return app

View file

18
wowfunding/orm/connect.py Normal file
View file

@ -0,0 +1,18 @@
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 wowfunding.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

210
wowfunding/orm/orm.py Normal file
View file

@ -0,0 +1,210 @@
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
base = declarative_base(name="Model")
class User(base):
__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)
proposals = relationship('Proposal', back_populates="user")
def __init__(self, username, password, email):
from wowfunding.factory import bcrypt
self.username = username
self.password = bcrypt.generate_password_hash(password).decode('utf8')
self.email = email
self.registered_on = datetime.utcnow()
@property
def is_authenticated(self):
return True
@property
def is_active(self):
return True
@property
def is_anonymous(self):
return False
@property
def is_admin(self):
return self.admin
def get_id(self):
return self.id
def __repr__(self):
return '<User %r>' % self.username
class Proposal(base):
__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)
# the FFS target
funds_target = sa.Column(sa.Float, nullable=False)
# the FFS progress (cached)
funds_progress = sa.Column(sa.Float, nullable=False)
# the FFS withdrawal amount (paid to the author)
funds_withdrew = sa.Column(sa.Float, nullable=False, default=0)
# the FFS receiving and withdrawal addresses
addr_donation = sa.Column(sa.VARCHAR)
addr_receiving = sa.Column(sa.VARCHAR)
# proposal status:
# -1: disabled
# 0: proposed
# 1: wip
# 2: completed
status = sa.Column(sa.INTEGER, default=0)
user_id = sa.Column(sa.Integer, sa.ForeignKey('users.user_id'))
user = relationship("User", back_populates="proposals")
def __init__(self, headline, content, category, user: User):
if not headline or not content:
raise Exception('faulty proposal')
self.headline = headline
self.content = content
self.user_id = user.id
if category not in settings.FUNDING_CATEGORIES:
raise Exception('wrong category')
self.category = category
@property
def json(self):
return {
'date_posted_epoch': self.date_added.strftime('%s'),
'date_posted': self.date_added.strftime('%b %d %Y %H:%M:%S'),
'headline': self.headline,
'content': self.content,
'category': self.category,
'funds_target': self.funds_target,
'funded_pct': self.funds_progress,
'addr_donation': self.addr_donation,
'status': self.status,
'user': self.user.username
}
@classmethod
def find_by_id(cls, pid: int):
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:
Proposal.generate_donation_addr(result)
return result
@property
def balance(self):
"""This property retrieves the current funding status
of this proposal. It uses Redis cache to not spam the
wownerod too much. Returns a nice dictionary containing
all relevant proposal funding info"""
from wowfunding.factory import cache, db_session
rtn = {'sum': 0.0, 'txs': [], 'pct': 0.0}
cache_key = 'wow_balance_pid_%d' % self.id
data = cache.get(cache_key)
if not data:
from wowfunding.bin.daemon import WowneroDaemon
try:
data = WowneroDaemon().get_transfers_in(index=self.id)
if not isinstance(data, dict):
print('error; get_transfers; %d' % self.id)
return rtn
cache.set(cache_key, data=data, expiry=300)
except:
print('error; get_transfers; %d' % self.id)
return rtn
for tx in data['txs']:
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['remaining'] = data['sum'] - self.funds_withdrew
else:
data['pct'] = 0.0
data['remaining'] = 0.0
if data['pct'] != self.funds_progress:
self.funds_progress = data['pct']
db_session.commit()
db_session.flush()
if data['remaining']:
data['remaining_pct'] = 100 / float(data['sum'] / data['remaining'])
else:
data['remaining_pct'] = 0.0
return data
@staticmethod
def generate_donation_addr(cls):
from wowfunding.factory import db_session
from wowfunding.bin.daemon import WowneroDaemon
if cls.addr_donation:
return cls.addr_donation
try:
addr_donation = WowneroDaemon().get_address(index=cls.id)
if not isinstance(addr_donation, dict):
raise Exception('get_address, needs dict; %d' % cls.id)
except Exception as ex:
print('error: %s' % str(ex))
return
if addr_donation.get('address'):
cls.addr_donation = addr_donation['address']
db_session.commit()
db_session.flush()
return addr_donation['address']
@classmethod
def find_by_args(cls, status:int = None, cat: str = None, limit: int = 20, offset=0):
if status is None or not status >= 0 or not status <= 2:
raise NotImplementedError('missing status')
q = cls.query
q = q.filter(Proposal.status == status)
if cat:
q = q.filter(Proposal.category == cat)
q = q.order_by(Proposal.date_added.desc())
q = q.limit(limit)
if isinstance(offset, int):
q = q.offset(offset)
return q.all()
@classmethod
def search(cls, key: str):
key_ilike = '%' + key.replace('%', '') + '%'
q = Proposal.query
q = q.filter(sa.or_(
Proposal.headline.ilike(key_ilike),
Proposal.content.ilike(key_ilike)))
return q.all()

188
wowfunding/routes.py Normal file
View file

@ -0,0 +1,188 @@
from datetime import datetime
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_yoloapi import endpoint, parameter
import settings
from wowfunding.factory import app, db_session
from wowfunding.orm.orm import Proposal, User
@app.route('/')
def index():
return redirect(url_for('proposals'))
@app.route('/about')
def about():
return make_response(render_template('about.html'))
@app.route('/proposal/add')
def proposal_add():
if current_user.is_anonymous:
return make_response(redirect(url_for('login')))
return make_response(render_template(('proposal_edit.html')))
@app.route('/proposal/<int:pid>')
def proposal(pid):
p = Proposal.find_by_id(pid=pid)
if not p:
return make_response(redirect(url_for('proposals')))
return make_response(render_template(('proposal.html'), proposal=p))
@app.route('/api/proposal/add', methods=['POST'])
@endpoint.api(
parameter('title', type=str, required=True, location='json'),
parameter('content', type=str, required=True, location='json'),
parameter('pid', type=int, required=False, location='json'),
parameter('funds_target', type=float, required=True, location='json'),
parameter('addr_receiving', type=str, required=True, location='json')
)
def proposal_api_add(title, content, pid, funds_target, addr_receiving):
import markdown2
if current_user.is_anonymous:
return make_response(jsonify('err'), 500)
if len(title) <= 10:
return make_response(jsonify('title too short'), 500)
if len(content) <= 20:
return make_response(jsonify('content too short'), 500)
try:
from wowfunding.bin.anti_xss import such_xss
content_escaped = such_xss(content)
html = markdown2.markdown(content_escaped, safe_mode=True)
except Exception as ex:
return make_response(jsonify('markdown error'), 500)
if pid:
p = Proposal.find_by_id(pid=pid)
if not p:
return make_response(jsonify('proposal not found'), 500)
if p.user.id != current_user.id and not current_user.admin:
return make_response(jsonify('no rights to edit this proposal'), 500)
p.headline = title
p.content = content
p.html = html
if addr_receiving:
p.addr_receiving = addr_receiving
p.last_edited = datetime.now()
else:
if funds_target <= 1:
return make_response(jsonify('proposal asking less than 1 error :)'), 500)
if len(addr_receiving) != 97:
return make_response(jsonify('faulty addr_receiving address, should be of length 72'), 500)
p = Proposal(headline=title, content=content, category='misc', user=current_user)
p.html = html
p.last_edited = datetime.now()
p.funds_target = funds_target
p.addr_receiving = addr_receiving
db_session.add(p)
db_session.commit()
db_session.flush()
return make_response(jsonify({'url': url_for('proposal', pid=p.id)}))
@app.route('/proposal/<int:pid>/edit')
def proposal_edit(pid):
p = Proposal.find_by_id(pid=pid)
if not p:
return make_response(redirect(url_for('proposals')))
return make_response(render_template(('proposal_edit.html'), proposal=p))
@app.route('/search')
@endpoint.api(
parameter('key', type=str, required=False)
)
def search(key=None):
if not key:
return make_response(render_template('search.html', results=None, key='Empty!'))
results = Proposal.search(key=key)
return make_response(render_template('search.html', results=results, key=key))
@app.route('/user/<path:name>')
def user(name):
q = db_session.query(User)
q = q.filter(User.username == name)
user = q.first()
return render_template('user.html', user=user)
@app.route('/proposals')
@endpoint.api(
parameter('status', type=int, location='args', default=0),
parameter('page', type=int, location='args'),
parameter('cat', type=str, location='args')
)
def proposals(status, page, cat):
try:
proposals = Proposal.find_by_args(status=status, cat=cat)
except:
return make_response(redirect(url_for('proposals') + '?status=0'))
return make_response(render_template('proposals.html', proposals=proposals, status=status, cat=cat))
@app.route('/register', methods=['GET', 'POST'])
def register():
if settings.USER_REG_DISABLED:
return 'user reg disabled ;/'
if request.method == 'GET':
return make_response(render_template('register.html'))
try:
user = User(request.form['username'], request.form['password'], request.form['email'])
db_session.add(user)
db_session.commit()
flash('User successfully registered')
return redirect(url_for('login'))
except Exception as ex:
flash('Could not register user.')
return make_response(render_template('register.html'))
@app.route('/login', methods=['GET', 'POST'])
@endpoint.api(
parameter('username', type=str, location='form'),
parameter('password', type=str, location='form')
)
def login(username, password):
if request.method == 'GET':
return make_response(render_template('login.html'))
from wowfunding.factory import bcrypt
user = User.query.filter_by(username=username).first()
if user is None or not bcrypt.check_password_hash(user.password, password):
flash('Username or Password is invalid', 'error')
return make_response(render_template('login.html'))
login_user(user)
response = redirect(request.args.get('next') or url_for('index'))
response.headers['X-Set-Cookie'] = True
return response
@app.route('/logout', methods=['GET'])
def logout():
logout_user()
response = redirect(request.args.get('next') or url_for('login'))
response.headers['X-Set-Cookie'] = True
flash('Logout successfully')
return response
@app.route('/static/<path:path>')
def static_route(path):
return send_from_directory('static', path)

View file

@ -0,0 +1,8 @@
from flask.ext.login import login_required
from wowfunding.factory import app, db_session
@app.route('/admin/index')
@login_required
def admin_home():
return 'yep'

BIN
wowfunding/static/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

1567
wowfunding/static/css/bootstrap-grid.css vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,342 @@
/*!
* Bootstrap Reboot v4.0.0-beta.2 (https://getbootstrap.com)
* Copyright 2011-2017 The Bootstrap Authors
* Copyright 2011-2017 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
}
@-ms-viewport {
width: device-width;
}
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
[tabindex="-1"]:focus {
outline: none !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
dfn {
font-style: italic;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 80%;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre,
code,
kbd,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
-ms-overflow-style: scrollbar;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
a,
area,
button,
[role="button"],
input:not([type="range"]),
label,
select,
summary,
textarea {
-ms-touch-action: manipulation;
touch-action: manipulation;
}
table {
border-collapse: collapse;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #868e96;
text-align: left;
caption-side: bottom;
}
th {
text-align: inherit;
}
label {
display: inline-block;
margin-bottom: .5rem;
}
button {
border-radius: 0;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
button,
html [type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="radio"],
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
-webkit-appearance: listbox;
}
textarea {
overflow: auto;
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
max-width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
color: inherit;
white-space: normal;
}
progress {
vertical-align: baseline;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
outline-offset: -2px;
-webkit-appearance: none;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
summary {
display: list-item;
}
template {
display: none;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

View file

@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v4.0.0-beta.2 (https://getbootstrap.com)
* Copyright 2011-2017 The Bootstrap Authors
* Copyright 2011-2017 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input:not([type=range]),label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#868e96;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

8981
wowfunding/static/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,328 @@
.CodeMirror {
height: auto;
min-height: 300px;
border: 1px solid #ddd;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
padding: 10px;
font: inherit;
z-index: 1;
}
.CodeMirror-scroll {
min-height: 300px
}
.CodeMirror-fullscreen {
background: #fff;
position: fixed !important;
top: 50px;
left: 0;
right: 0;
bottom: 0;
height: auto;
z-index: 9;
}
.CodeMirror-sided {
width: 50% !important;
}
.editor-toolbar {
position: relative;
opacity: .6;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
padding: 0 10px;
border-top: 1px solid #bbb;
border-left: 1px solid #bbb;
border-right: 1px solid #bbb;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.editor-toolbar:after,
.editor-toolbar:before {
display: block;
content: ' ';
height: 1px;
}
.editor-toolbar:before {
margin-bottom: 8px
}
.editor-toolbar:after {
margin-top: 8px
}
.editor-toolbar:hover,
.editor-wrapper input.title:focus,
.editor-wrapper input.title:hover {
opacity: .8
}
.editor-toolbar.fullscreen {
width: 100%;
height: 50px;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
padding-top: 10px;
padding-bottom: 10px;
box-sizing: border-box;
background: #fff;
border: 0;
position: fixed;
top: 0;
left: 0;
opacity: 1;
z-index: 9;
}
.editor-toolbar.fullscreen::before {
width: 20px;
height: 50px;
background: -moz-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 1)), color-stop(100%, rgba(255, 255, 255, 0)));
background: -webkit-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
background: -o-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
background: -ms-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
background: linear-gradient(to right, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
position: fixed;
top: 0;
left: 0;
margin: 0;
padding: 0;
}
.editor-toolbar.fullscreen::after {
width: 20px;
height: 50px;
background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(100%, rgba(255, 255, 255, 1)));
background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
position: fixed;
top: 0;
right: 0;
margin: 0;
padding: 0;
}
.editor-toolbar a {
display: inline-block;
text-align: center;
text-decoration: none!important;
color: #2c3e50!important;
width: 30px;
height: 30px;
margin: 0;
border: 1px solid transparent;
border-radius: 3px;
cursor: pointer;
}
.editor-toolbar a.active,
.editor-toolbar a:hover {
background: #fcfcfc;
border-color: #95a5a6;
}
.editor-toolbar a:before {
line-height: 30px
}
.editor-toolbar i.separator {
display: inline-block;
width: 0;
border-left: 1px solid #d9d9d9;
border-right: 1px solid #fff;
color: transparent;
text-indent: -10px;
margin: 0 6px;
}
.editor-toolbar a.fa-header-x:after {
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
font-size: 65%;
vertical-align: text-bottom;
position: relative;
top: 2px;
}
.editor-toolbar a.fa-header-1:after {
content: "1";
}
.editor-toolbar a.fa-header-2:after {
content: "2";
}
.editor-toolbar a.fa-header-3:after {
content: "3";
}
.editor-toolbar a.fa-header-bigger:after {
content: "▲";
}
.editor-toolbar a.fa-header-smaller:after {
content: "▼";
}
.editor-toolbar.disabled-for-preview a:not(.no-disable) {
pointer-events: none;
background: #fff;
border-color: transparent;
text-shadow: inherit;
}
@media only screen and (max-width: 700px) {
.editor-toolbar a.no-mobile {
display: none;
}
}
.editor-statusbar {
padding: 8px 10px;
font-size: 12px;
color: #959694;
text-align: right;
}
.editor-statusbar span {
display: inline-block;
min-width: 4em;
margin-left: 1em;
}
.editor-statusbar .lines:before {
content: 'lines: '
}
.editor-statusbar .words:before {
content: 'words: '
}
.editor-statusbar .characters:before {
content: 'characters: '
}
.editor-preview {
padding: 10px;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: #fafafa;
z-index: 7;
overflow: auto;
display: none;
box-sizing: border-box;
}
.editor-preview-side {
padding: 10px;
position: fixed;
bottom: 0;
width: 50%;
top: 50px;
right: 0;
background: #fafafa;
z-index: 9;
overflow: auto;
display: none;
box-sizing: border-box;
border: 1px solid #ddd;
}
.editor-preview-active-side {
display: block
}
.editor-preview-active {
display: block
}
.editor-preview>p,
.editor-preview-side>p {
margin-top: 0
}
.editor-preview pre,
.editor-preview-side pre {
background: #eee;
margin-bottom: 10px;
}
.editor-preview table td,
.editor-preview table th,
.editor-preview-side table td,
.editor-preview-side table th {
border: 1px solid #ddd;
padding: 5px;
}
.CodeMirror .CodeMirror-code .cm-tag {
color: #63a35c;
}
.CodeMirror .CodeMirror-code .cm-attribute {
color: #795da3;
}
.CodeMirror .CodeMirror-code .cm-string {
color: #183691;
}
.CodeMirror .CodeMirror-selected {
background: #d9d9d9;
}
.CodeMirror .CodeMirror-code .cm-header-1 {
font-size: 200%;
line-height: 200%;
}
.CodeMirror .CodeMirror-code .cm-header-2 {
font-size: 160%;
line-height: 160%;
}
.CodeMirror .CodeMirror-code .cm-header-3 {
font-size: 125%;
line-height: 125%;
}
.CodeMirror .CodeMirror-code .cm-header-4 {
font-size: 110%;
line-height: 110%;
}
.CodeMirror .CodeMirror-code .cm-comment {
background: rgba(0, 0, 0, .05);
border-radius: 2px;
}
.CodeMirror .CodeMirror-code .cm-link {
color: #7f8c8d;
}
.CodeMirror .CodeMirror-code .cm-url {
color: #aab2b3;
}
.CodeMirror .CodeMirror-code .cm-strikethrough {
text-decoration: line-through;
}
.CodeMirror .CodeMirror-placeholder {
opacity: .5;
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,354 @@
body {
padding-top: 54px;
}
@media (min-width: 992px) {
body {
padding-top: 56px;
}
}
.container>div:first-child {
padding-top: 40px;
}
.mb-4, .my-4{
margin-top:0 !important;
}
.mt-4, .my-4 {
margin-top: 0rem !important;
}
.proposal-info-table td {
padding: .35rem !important;
}
.proposal_address {
white-space: pre-wrap;
padding: 2px;
border-radius: 2px;
margin-top: 6px;
font-weight: bold;
color: #ff0000;
font-size: 22px;
}
.proposal_content blockquote {
background-color: #efefef;
padding: 8px;
}
.proposal_content h1, .proposal_content h2, .proposal_content h3, .proposal_content h4{
margin-bottom: 16px;
margin-top: 16px;
}
.proposal_content p {
margin-bottom: 0.5rem;
}
.proposal_content img {
width: 100%;
}
.proposal_content code {
background: #ebebeb;
padding: 2px;
}
.table td, .table th {
padding: 0.35rem;
}
/* Sticky footer styles
-------------------------------------------------- */
html {
position: relative;
min-height: 100%;
}
body {
margin-bottom: 60px; /* Margin bottom by footer height */
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;
background-image: url("/static/bg.png");
background-repeat: repeat;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
height: 60px; /* Set the fixed height of the footer here */
line-height: 60px; /* Vertically center the text there */
background-color: #f5f5f5;
}
.bg-dark {
background: linear-gradient(90deg, #d253c8, #ffa93e);
}
.navbar-dark .navbar-brand {
color: #fff;
font-size: 18pt;
text-shadow: 2px 2px #667ff952;
font-family: "Comic Sans MS", "Comic Sans", cursive;
}
.table-proposal td {
font-size: 15px;
}
.table-hover tbody tr:hover {
background-color: rgba(0,0,0,.075);
}
.table th, .table td {
border-top: 1px solid #008926;
padding-top: .4rem;
}
.table-tilted{
-ms-transform: rotate(0.5deg);
-webkit-transform: rotate(0.5deg);
transform: rotate(0.5deg);
}
.table-tilted-v{
-ms-transform: rotate(-0.2deg);
-webkit-transform: rotate(-0.2deg);
transform: rotate(-0.2deg);
}
.table-no-header thead{
display: none;
}
.table-wow{
background-image: url(https://wownero.win/static/game-table-bg.png);
background-size: cover;
}
.table a{
color: #006400;
}
.table thead th {
border-bottom: 1px solid #008926 !important;
border-top: none;
}
.table tbody td{
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.nav-tabs .nav-item.show .nav-link, .nav-tabs .nav-link.active {
color: #495057;
background-color: #fff;
border-left-color: #008926;
border-left-width: 2px;
border-top-color: #008926;
border-top-width: 1px;
border-right-color: #008926;
border-right-width: 1px;
}
.nav-tabs .nav-link {
border: 1px solid transparent;
border-top-left-radius: .25rem;
border-top-right-radius: .25rem;
border-bottom-color: #008926;
}
.nav-tabs .nav-link:focus, .nav-tabs .nav-link:hover {
border-color: #e9ecef #e9ecef #dee2e6;
}
.nav-tabs .nav-link:focus, .nav-tabs .nav-link:hover {
border-color: #00892630 #00892630 #008926;
}
a {
color: #008926;
}
.btn-group-toggle a{
color: white;
}
.btn-group-toggle a:hover{
text-decoration: none;
}
.btn-primary{
background-color: #28a745;
border-color: #28a745;
}
.btn-primary:hover{
color: #fff;
background-color: #1e7e34;
border-color: #1c7430;
}
.card-body {
background-color: #00000008;
}
.card {
background-color: #ffffff00;
border: 0px solid rgba(0,0,0,.125);
border-radius: .25rem;
}
.card-header {
padding: .75rem 1.25rem;
margin-bottom: 0;
background-color: rgba(0,0,0,.03);
background: linear-gradient(90deg, #d253c829, #ffa93e69);
/*background: linear-gradient(90deg, #40d61e1f, #ffa93e69);*/
border-radius: calc(.25rem - 1px) calc(.25rem - 1px) 0 0;
border-bottom: 1px solid #e6e6e6;
border-bottom: 0px;
}
.proposal_content{
background-color: #00000008;
padding: .75rem 1.25rem;
}
.form-control {
padding: .375rem .75rem;
font-size: 1rem;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #008926;
}
/*fuku chrome*/
input {
outline:none;
}
input:focus {
outline: 0;
}
*:focus {
outline: none;
}
input[type="text"], textarea {
outline: none;
box-shadow:none !important;
border: 1px solid #008926 !important;
}
.navbar-dark .navbar-nav .nav-link {
color: white;
}
nav .nav-link{
color: white;
}
nav .nav-link .active{
text-shadow: none;
}
.proposal-info-table td{
border-top: 0;
}
.proposal-info-table td span.badge{
font-size:20px;
}
.table-proposal .progress {
max-width: 70px !important;
min-width: 50px !important;
float:right;
}
.table-proposal .progress-bar {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
-ms-flex-pack: center;
justify-content: center;
color: #fff;
text-shadow: 1px 1px #000000;
text-align: center;
white-space: nowrap;
background-color: #009845;
transition: width .6s ease;
font-family: monospace;
padding: 6px;
}
#point-wow-left{
float: right;
}
@media only screen and (max-width: 600px) {
.proposal-info-table tr:first-child>td, .proposal-info-table tr:first-child>td>span {
font-size: 28px !important;
font-weight: 100;
}
.navbar-dark .navbar-brand {
font-size: 13pt;
}
.proposal-info-table tr:first-child>td, .proposal-info-table tr:first-child>td>span {
font-size: 14px !important;
}
.proposal-info-table td span.badge {
font-size: 14px;
}
.proposal_content {
background-color: #00000008;
padding: 0.1rem 0.75rem;
}
.card-body{
padding: 0.5rem;
}
#point-wow-left{
display: none;
}
.table-proposal thead th#date, .table-proposal tbody td#date{
display: none;
}
.table-proposal, .table-proposal td{
font-size:14px !important;
}
.navbar-brand {
display: none;
}
.navbar-brand-mobile{
display: block !important;
color: white;
font-size:14px;
}
pre.proposal_address{
font-size:14px;
}
label {
font-size:12px !important;
}
}
@media only screen and (max-width: 768px) {
#point-wow-left {
display: none;
}
}

BIN
wowfunding/static/cyber.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
wowfunding/static/ie9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
wowfunding/static/java.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,12 @@
function clickDataHref(d){
window.location.href = d.getAttribute("data-href");
}
function hideShow(element_id) {
var x = document.getElementById(element_id);
if (x.style.display === "none") {
x.style.display = "block";
} else {
x.style.display = "none";
}
}

6
wowfunding/static/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

15
wowfunding/static/js/simplemde.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
wowfunding/static/nasa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View file

@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-lg-8">
<h3>About</h3>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<hr>
<p>
A simple funding system made with:
</p>
<ul>
<li>Python 3.5+</li>
<li>Flask microframework</li>
<li>Postgres 9.5+</li>
<li>Redis</li>
</ul>
<p>
We do not keep access logs or install tracking cookies. All static resources (javascript/stylesheets/images) are self-hosted.
</p>
<p>
When you encounter problems; please visit #wownero on chat.freenode.org
</p>
</div>
{% include 'sidebar.html' %}
</div>
<br>
</div>
<!-- /.container -->
{% endblock %}

View file

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<!--
░░░░░░░█▐▓▓░████▄▄▄█▀▄▓▓▓▌█ very website
░░░░░▄█▌▀▄▓▓▄▄▄▄▀▀▀▄▓▓▓▓▓▌█
░░░▄█▀▀▄▓█▓▓▓▓▓▓▓▓▓▓▓▓▀░▓▌█
░░█▀▄▓▓▓███▓▓▓███▓▓▓▄░░▄▓▐█▌ such html
░█▌▓▓▓▀▀▓▓▓▓███▓▓▓▓▓▓▓▄▀▓▓▐█
▐█▐██▐░▄▓▓▓▓▓▀▄░▀▓▓▓▓▓▓▓▓▓▌█▌ WOW
█▌███▓▓▓▓▓▓▓▓▐░░▄▓▓███▓▓▓▄▀▐█
█▐█▓▀░░▀▓▓▓▓▓▓▓▓▓██████▓▓▓▓▐█
▌▓▄▌▀░▀░▐▀█▄▓▓██████████▓▓▓▌█▌
▌▓▓▓▄▄▀▀▓▓▓▀▓▓▓▓▓▓▓▓█▓█▓█▓▓▌█▌ many donations
█▐▓▓▓▓▓▓▄▄▄▓▓▓▓▓▓█▓█▓█▓█▓▓▓▐█ gib lambo
-->
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320">
<link rel="shortcut icon" href="/static/favicon.ico" type="image/x-icon" />
<meta property="fb:app_id" content="0" />
<meta property="og:image" content="https://funding.wownero.com/static/wowdoge-a.jpg" />
<meta property="og:description" content="The Wownero forum funding system" />
<meta property="og:url" content="https://funding.wownero.com/" />
<meta property="og:title" content="WFS" />
<meta property="og:type" content="website" />
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="theme-color" content="#ffffff">
<meta name="apple-mobile-web-app-title" content="WOW WFS">
<meta name="application-name" content="WOWNERO WFS">
<meta name="msapplication-TileColor" content="#da532c">
{% if proposal %}
<meta name="description" content="{{proposal.content[:50]}}" />
<title>WOW WFS - {{proposal.headline[:20]}}</title>
{% else %}
<meta name="description" content="The Wownero Funding System" />
<title>WOW WFS</title>
{% endif %}
<meta name="keywords" content="monero, xmr, bitmonero, cryptocurrency, crypto money, mining crypto currencies, virtual currency">
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/bootstrap-grid.min.css" rel="stylesheet">
<link href="/static/css/bootstrap-reboot.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/css/simplemde.min.css">
<script src="/static/js/simplemde.min.js"></script>
<link href="/static/css/wow.css" rel="stylesheet">
<script src="/static/js/app.js"></script>
<script src="/static/js/jquery-3.2.1.slim.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<body>
{% include 'navbar.html' %}
<!-- Page Content -->
{% block content %} {% endblock %}
<!-- Footer -->
<footer class="bg-dark footer">
<div class="container">
<p class="m-0 text-center text-white">WOW 2018</p>
</div>
</footer>
</body>
</html>

View file

@ -0,0 +1,66 @@
<div class="row">
<div class="col-md-12">
<div class="card my-6" id="incoming_txs">
<h5 class="card-header">Comments</h5>
<div class="card-body">
Comment functionality not made yet!<br>
Press <b>F</b> to pay respects.
</div>
</div>
</div>
</div>
<!--&lt;!&ndash; Comments Form &ndash;&gt;-->
<!--<div class="card my-4">-->
<!--<h5 class="card-header">Leave a Comment:</h5>-->
<!--<div class="card-body">-->
<!--<form>-->
<!--<div class="form-group">-->
<!--<textarea class="form-control" rows="3"></textarea>-->
<!--</div>-->
<!--<button type="submit" class="btn btn-primary">Submit</button>-->
<!--</form>-->
<!--</div>-->
<!--</div>-->
<!--&lt;!&ndash; Single Comment &ndash;&gt;-->
<!--<div class="media mb-4">-->
<!--<img class="d-flex mr-3 rounded-circle" src="http://placehold.it/50x50" alt="">-->
<!--<div class="media-body">-->
<!--<h5 class="mt-0">Commenter Name</h5>-->
<!--Cras sit amet nibh libero, in gravida nulla. Nulla vel metus scelerisque ante sollicitudin. Cras-->
<!--purus odio, vestibulum in vulputate at, tempus viverra turpis. Fusce condimentum nunc ac nisi-->
<!--vulputate fringilla. Donec lacinia congue felis in faucibus.-->
<!--</div>-->
<!--</div>-->
<!--&lt;!&ndash; Comment with nested comments &ndash;&gt;-->
<!--<div class="media mb-4">-->
<!--<img class="d-flex mr-3 rounded-circle" src="http://placehold.it/50x50" alt="">-->
<!--<div class="media-body">-->
<!--<h5 class="mt-0">Commenter Name</h5>-->
<!--Cras sit amet nibh libero, in gravida nulla. Nulla vel metus scelerisque ante sollicitudin. Cras-->
<!--purus odio, vestibulum in vulputate at, tempus viverra turpis. Fusce condimentum nunc ac nisi-->
<!--vulputate fringilla. Donec lacinia congue felis in faucibus.-->
<!--<div class="media mt-4">-->
<!--<img class="d-flex mr-3 rounded-circle" src="http://placehold.it/50x50" alt="">-->
<!--<div class="media-body">-->
<!--<h5 class="mt-0">Commenter Name</h5>-->
<!--Cras sit amet nibh libero, in gravida nulla. Nulla vel metus scelerisque ante sollicitudin.-->
<!--Cras purus odio, vestibulum in vulputate at, tempus viverra turpis. Fusce condimentum nunc-->
<!--ac nisi vulputate fringilla. Donec lacinia congue felis in faucibus.-->
<!--</div>-->
<!--</div>-->
<!--<div class="media mt-4">-->
<!--<img class="d-flex mr-3 rounded-circle" src="http://placehold.it/50x50" alt="">-->
<!--<div class="media-body">-->
<!--<h5 class="mt-0">Commenter Name</h5>-->
<!--Cras sit amet nibh libero, in gravida nulla. Nulla vel metus scelerisque ante sollicitudin.-->
<!--Cras purus odio, vestibulum in vulputate at, tempus viverra turpis. Fusce condimentum nunc-->
<!--ac nisi vulputate fringilla. Donec lacinia congue felis in faucibus.-->
<!--</div>-->
<!--</div>-->
<!--</div>-->
<!--</div>-->

View file

@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block content %}
<!--main-->
<div class="container" id="main">
<div class="row">
<div class="col-lg-12">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="alert alert-danger">
{% for message in messages %}
{{ message }}<br>
{% endfor %}
</div>
{% endif %}
{% endwith %}
</div>
</div>
<div class="row">
<style>
#xox>*{
float:left;
}
#xox>img{
height:42px;
padding-left:4px;
}
</style>
<div class="col-lg-12" id="xox">
<h3>Secure login </h3>
<img src="/static/nasa.png"/>
<img src="/static/cyber.png"/>
<img src="/static/ie9.png"/>
<img src="/static/java.png"/>
<hr>
</div>
</div>
<div class="row">
<div class="col-lg-3">
<form class="form-horizontal" action="" method=post>
<div class="form-group">
<label class="sr-only" for="inlineFormInput">Name</label>
<input type="text" class="form-control mb-2 mr-sm-2 mb-sm-0" id="inlineFormInput"
placeholder="Username" name="username">
</div>
<div class="form-group">
<label class="sr-only" for="password">Password</label>
<div class="input-group mb-2 mr-sm-2 mb-sm-0">
<input class="form-control" type="password" id="password" placeholder="Password"
name="password">
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary btn-sm">Login</button>
</div>
</form>
</div>
<div class="col-lg-12">
<a href="/register">Or register here</a>
</div>
</div>
</div>
<br>
<!--/main-->
{% endblock %}

View file

@ -0,0 +1,37 @@
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<div class="container">
<a class="navbar-brand" href="/">Wownero Funding System (WFS)</a>
<a class="navbar-brand-mobile" href="/" style="font-family:monospace;display:none;">Wownero Funding System</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive"
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
<li class="nav-item active">
<a class="nav-link" href="/proposals">Proposals
<span class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/proposal/add">Add Proposal</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about">About</a>
</li>
{% if logged_in %}
<li class="nav-item">
<a class="nav-link" href="/logout">Logout</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="/login">Login</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>

View file

@ -0,0 +1,127 @@
{% extends "base.html" %}
{% block content %}
<!-- Page Content -->
<div class="container" style="margin-bottom:140px;">
<div class="row">
<!-- Post Content Column -->
<div class="col-lg-12">
<!-- Title -->
<h1 class="mt-4">{{ proposal.headline }}</h1>
<p class="lead">
<p>Posted on {{ proposal.date_added.strftime('%Y-%m-%d') }} by <a href="/user/{{ proposal.user.username }}">{{ proposal.user.username}}</a></p>
</p>
<p>
{% if proposal.user.username == current_user.username %}
<a href="/proposal/{{proposal.id}}/edit">
<button type="button" class="btn btn-success btn-sm">Edit</button>
</a>
{% endif %}
</p>
<hr>
<div class="row">
<div class="col-md-12">
<!-- Side Widget -->
<div class="card my-4">
<h5 class="card-header">Funds</h5>
<div class="card-body">
<div class="row the-bar">
<div class="col-lg-4">
<table class="table proposal-info-table">
<tbody>
<tr>
<td>Target</td>
<td><span class="badge">{{proposal.funds_target|round}} WOW</span></td>
</tr>
<tr>
<td>Progress</td>
<td><span class="badge">{{proposal.balance['pct'] |round}} %</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-lg-8">
{{proposal.balance['txs'] | length}} individual contributions
{% if proposal.balance['txs'] %}
<small>
<a style="margin:4px;" href="#incoming_txs">Details...</a>
</small>
{% endif %}
<div class="progress">
<div class="progress-bar progress-monero progress-bar-striped" style="width: 100.0%;">
</div>
</div>
<hr>
</div>
<div class="col-lg-8">
{{proposal.balance['remaining'] or 0}} WOW available
<div class="progress">
<div class="progress-bar progress-warning progress-bar" style="width: {{proposal.balance['remaining_pct']}}%;">
</div>
</div>
</div>
</div>
<div class="row" style="margin-top:16px;">
<div class="col-lg-12">
Donatation address:
<pre class="proposal_address">{{ proposal.addr_donation }}</pre>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="proposal_content">
<!-- Post Content -->
{{proposal.html | safe}}
</div>
</div>
</div>
{% include 'comments.html' %}
<style>
#incoming_txs li.list-group-item {
padding-top: 4px;
padding-bottom: 4px;
}
</style>
{% if proposal.balance['txs'] %}
<div class="row">
<div class="col-md-12">
<div class="card my-6" id="incoming_txs">
<h5 class="card-header">Incoming transactions <small>({{proposal.balance['txs']|length}})</small></h5>
<div class="card-body">
<ul class="list-group">
{% for tx in proposal.balance['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:#008926;font-weight:bold;">+ {{tx['amount_human']|round(2)}} WOW</span>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
<!-- /.row -->
{% endif %}
</div>
<!-- /.container -->
{% endblock %}

View file

@ -0,0 +1,150 @@
{% extends "base.html" %}
{% block content %}
<!-- Page Content -->
<div class="container">
{% if logged_in %}
{% if proposal %}
{% set headline = proposal.headline %}
{% set content = proposal.content %}
{% set date_added = proposal.date_added.strftime('%Y-%m-%d') %}
{% set pid = proposal.id %}
{% set funds_target = proposal.funds_target %}
{% set addr_receiving = proposal.addr_receiving if proposal.addr_receiving else '' %}
{% else %}
{% set headline = '' %}
{% set content = '' %}
{% set date_added = '' %}
{% set pid = '' %}
{% set funds_target = '' %}
{% set addr_receiving = '' %}
{% endif %}
<div class="row">
<!-- Post Content Column -->
<div class="col-lg-8">
<h2 style="padding-top:0">Add proposal</h2>
<p style="padding-top:0">
This page allows you to add your funding proposal. Some help:
</p>
<ol>
<li>Proposals are written in Markdown format. Use the HTML preview functionality of the editor.</li>
<li>Introduce yourself, investors would like to know who they are funding</li>
<li>Try to seek out community approval for your idea(s) before submitting a request</li>
<li>Keep it short and clean</li>
<li>Good luck!</li>
</ol>
<form id="news_post" role="form" lpformnum="2" _lpchecked="1" onsubmit="return false;">
{% if pid %}
<input style="display:none" name="pid" value="{{pid}}">
{% endif %}
<!-- text input -->
<div class="form-group">
<label>Title</label>
<input id="title" type="text" name="title" class="form-control" placeholder="Title" value="{{ headline }}">
</div>
<div class="form-group">
<label>Proposal Date</label>
<input type="text" class="form-control" value="{{ date_added }}" disabled="">
</div>
<div class="form-group">
<label>Target <small>(In WOW)</small></label>
<input {% if funds_target %}disabled{% endif %} id="funds_target" type="text" name="funds_target" class="form-control" placeholder="Target" value="{{ funds_target }}">
</div>
<div class="form-group">
<label>Receiving address for withdrawals <small>(WOW address)</small></label>
<input {% if addr_receiving %}disabled{% endif %} id="addr_receiving" type="text" name="addr_receiving" class="form-control" placeholder="WOW..." value="{{ addr_receiving }}">
</div>
<div class="form-group">
<label>Content</label>
<textarea id="content" class="textarea" name="content" placeholder="Place some text here" style="width: 100%; height: 600px; font-size: 14px; line-height: 18px; border: 1px solid #dddddd; padding: 10px;">{{ content }}</textarea>
</div>
<div style="display:none" id="error" class="alert alert-danger">
</div>
<div class="form-group">
<button onclick="submit_proposal();" type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</div>
{% include 'sidebar.html' %}
</div>
<!-- /.row -->
{% else %}
<div class="row">
<!-- Post Content Column -->
<div class="col-lg-8">
To submit your proposal, <a href="/login">login</a> or <a href="/register">register</a>.
</div>
</div>
{% endif %}
</div>
<!-- /.container -->
<script>
var pid = {{pid or 'null'}};
var simplemde = new SimpleMDE({
element: document.getElementById("content"),
spellChecker: false
});
function submit_proposal() {
function error(msg){
var error_div = document.getElementById("error");
error_div.style.display = "block";
error_div.innerHTML = "Error: " + msg;
}
function success(data){
var data = JSON.parse(data);
window.location.href = data.url;
}
var data = {
'title': document.getElementById('title').value,
'content': simplemde.value(),
'funds_target': parseFloat(document.getElementById('funds_target').value),
'addr_receiving': document.getElementById('addr_receiving').value
};
if (pid) {
data.pid = pid;
}
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/proposal/add', true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
success(xhr.responseText);
} else {
error(xhr.responseText);
}
}
};
xhr.onerror = function () {
error(xhr.responseText);
};
xhr.send(JSON.stringify(data));
}
</script>
{% endblock %}

View file

@ -0,0 +1,104 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="col-lg-8" style="padding-bottom: 0px;">
<div class="row">
<div class="form-group" style="text-align: center;">
<div>
<div data-toggle="buttons" class="btn-group btn-group-toggle">
<label onclick="clickDataHref(this);" data-href="{{ url_for('proposals', status=0) }}" class="btn btn-success {% if status == 0 %}active{% endif %}">
Proposals <small>({{summary_data['statuses'][0]}})</small>
</label>
<label onclick="clickDataHref(this);" data-href="{{ url_for('proposals', status=1) }}" class="btn btn-success {% if status == 1 %}active{% endif %}">
Work in progress <small>({{summary_data['statuses'][1]}})</small>
</label>
<label onclick="clickDataHref(this);" data-href="{{ url_for('proposals', status=2) }}" class="btn btn-success {% if status == 2 %}active{% endif %}">
Completed <small>({{summary_data['statuses'][2]}})</small>
</label>
</div>
<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>
</div>
</div>
{% if cat %}
<small class="form-text text-muted" style="margin-top: -2px;">
Results limited by category '{{cat}}'. <a href="{{ url_for('proposals', status=status) }}">Undo filter</a>.
</small>
<br>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-lg-8">
{% if proposals %}
{% for p in proposals %}
{% if loop.index == 1 %}
<table class="table table-proposal table-hover table-tilted" style="margin-bottom:6px;">
<thead>
<tr>
<th style="font-size: x-large;">Proposal</th>
<th>Username</th>
<th id="date">Date</th>
{% if status == 0 %}
<th style="text-align: right;">Funding</th>
{% else %}
<th></th>
{% endif %}
</tr>
</thead>
<tbody>
{% else %}
{% if loop.index % 3 == 0 %}
</tbody>
</table>
<table class="table table-proposal table-hover {% if (loop.index/3) % 2 == 0 %}table-tilted{% else %}table-tilted-v{% endif %}">
<tbody>
{% endif %}
{% endif %}
<tr>
<td><b><a href="/proposal/{{ p.id }}">{{ p.headline }}</a></b></td>
<td><a href="/user/{{ p.user.username }}">{{ p.user.username }}</a></td>
<td id="date"><small>{{ p.date_added.strftime('%Y-%m-%d %H:%M') }}</small></td>
<td>
<span style="float:right;">
{% if p.funds_progress >= 0.1 and status == 0 %}
{{p.funds_progress|int}}%
{% else %}
-
{% endif %}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
No proposals here yet.
{% endif %}
</div>
{% include 'sidebar.html' %}
</div>
<!--<div class="col-md-12">-->
<!--&lt;!&ndash; Pagination &ndash;&gt;-->
<!--<ul class="pagination mb-4">-->
<!--<li class="page-item">-->
<!--<a class="page-link" href="#">&larr; Older</a>-->
<!--</li>-->
<!--<li class="page-item disabled">-->
<!--<a class="page-link" href="#">Newer &rarr;</a>-->
<!--</li>-->
<!--</ul>-->
<!--</div>-->
<br>
</div>
<!-- /.container -->
{% endblock %}

View file

@ -0,0 +1,56 @@
{% extends "base.html" %}
{% block content %}
<!--main-->
<div class="container" id="main">
<div class="row">
<div class="col-lg-12">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="alert alert-danger">
{% for message in messages %}
{{ message }}<br>
{% endfor %}
</div>
{% endif %}
{% endwith %}
</div>
</div>
<div class="row">
<div class="col-lg-12">
<h3>Register an account </h3>
<hr>
</div>
</div>
<div class="row">
<div class="col-lg-3">
<form class="form-horizontal" action="" method=post>
<div class="form-group">
<input type="text" id="username" name="username" class="form-control mb-2 mr-sm-2 mb-sm-0"
placeholder="Enter Username" value="{{ request.form.username }}"
required>
</div>
<div class="form-group">
<input type="password" id="password" name="password" class="form-control mb-2 mr-sm-2 mb-sm-0"
placeholder="Enter Password" value="{{ request.form.password }}"
required>
</div>
<div class="form-group">
<input type="email" id="email" name="email" class="form-control mb-2 mr-sm-2 mb-sm-0"
placeholder="Enter Email" value="{{ request.form.username }}"
required>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success">Signup</button>
</div>
</form>
</div>
</div>
</div>
<br>
<!--/main-->
{% endblock %}

View file

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-lg-8">
<span class="form-text text-muted" style="margin-top: -2px;">
Results for '{{key}}'
</span>
</div>
</div>
<div class="row">
<div class="col-lg-8">
{% if results %}
<table class="table table-proposal table-hover" style="margin-bottom:6px;">
<tbody>
{% for p in results %}
<tr>
<td><b><a href="/proposal/{{ p.id }}">{{ p.headline }}</a></b></td>
<td><a href="/user/{{ p.user.username }}">{{ p.user.username }}</a></td>
<td>{{ p.date_added.strftime('%Y-%m-%d') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
No search results.
{% endif %}
</div>
{% include 'sidebar.html' %}
</div>
<br>
</div>
<!-- /.container -->
{% endblock %}

View file

@ -0,0 +1,80 @@
{% if not status %}
{% set status = 0 %}
{% endif %}
<!-- Sidebar Widgets Column -->
<div class="col-md-4">
<!-- Search Widget -->
<div class="card my-4" style="margin-top: 0 !important;">
<h5 style="transform: rotate(-0.4deg);" class="card-header">Search</h5>
<div class="card-body">
<div class="input-group">
<input id="search_input" type="text" class="form-control" placeholder="Search for..." value="{% if key %}{{key}}{% endif %}">
<span class="input-group-btn">
<button onclick="do_search()" style="border-top-left-radius: 0px;border-bottom-left-radius: 0px;" class="btn btn-primary" type="button">
Go!
</button>
</span>
</div>
</div>
</div>
<!-- Categories Widget -->
<div class="card my-4">
<h5 style="transform: rotate(0.6deg);" class="card-header">Categories</h5>
<div class="card-body">
<div class="row">
<div class="col-lg-6">
<ul class="list-unstyled mb-0">
<li>
<a href="{{ url_for('proposals', status=status, cat="wallets") }}">Wallets <small>({{summary_data['cats']['wallets']}})</small></a>
</li>
<li>
<a href="{{ url_for('proposals', status=status, cat="core") }}">Core <small>({{summary_data['cats']['core']}})</small></a>
</li>
<li>
<a href="{{ url_for('proposals', status=status, cat="marketing") }}">Marketing <small>({{summary_data['cats']['marketing']}})</small></a>
</li>
</ul>
</div>
<div class="col-lg-6">
<ul class="list-unstyled mb-0">
<li>
<a href="{{ url_for('proposals', status=status, cat="misc") }}">Misc <small>({{summary_data['cats']['misc']}})</small></a>
</li>
<li>
<a href="{{ url_for('proposals', status=status, cat="design") }}">Design <small>({{summary_data['cats']['design']}})</small></a>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Side Widget -->
<div class="card my-4">
<h5 class="card-header">Cool widget</h5>
<div class="card-body">
Many widgets. Such sidebar. Wow.
</div>
</div>
<script>
var search_input = document.getElementById("search_input");
function do_search(){
var url = '{{url_for('search')}}?key=' + search_input.value;
location.replace(url);
}
search_input.addEventListener("keyup", function(event) {
event.preventDefault();
if (event.keyCode === 13) {
do_search();
}
});
</script>
</div>

View file

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block content %}
{% if user %}
<div class="container">
<div class="row">
<div class="col-lg-8">
<span class="form-text text-muted" style="margin-top: -2px;">
Details for '{{user.username}}'
</span>
</div>
</div>
<div class="row">
<div class="col-lg-8">
{% if user.proposals %}
<table class="table table-proposal table-hover" style="margin-bottom:6px;">
<tbody>
{% for p in user.proposals %}
<tr>
<td><b><a href="/proposal/{{ p.id }}">{{ p.headline }}</a></b></td>
<td><a href="/user/{{ p.user.username }}">{{ p.user.username }}</a></td>
<td>{{ p.date_added.strftime('%Y-%m-%d') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
This user did not submit any proposals yet.
{% endif %}
</div>
{% include 'sidebar.html' %}
</div>
<br>
</div>
{% else %}
No user found by that name.
{% endif %}
<!-- /.container -->
{% endblock %}