Initial commit
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
*__pycache__*
|
||||
settings.py
|
||||
.cache
|
||||
.env
|
||||
htmlcov
|
||||
.coverage
|
24
README.md
Normal 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
|
@ -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
|
@ -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
|
@ -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
25
wowfunding/api.py
Normal 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]
|
0
wowfunding/bin/__init__.py
Normal file
210
wowfunding/bin/anti_xss.py
Normal 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("<", "<")\
|
||||
.replace(">", ">")\
|
||||
.replace('"', """)\
|
||||
.replace("'", "'")
|
||||
|
||||
|
||||
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('>'):
|
||||
line = line[4:]
|
||||
line = '>%s' % line
|
||||
_lines.append(line)
|
||||
|
||||
return "\n".join(_lines)
|
52
wowfunding/bin/daemon.py
Normal 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
|
@ -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))
|
67
wowfunding/bin/utils_request.py
Normal 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
|
@ -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
|
@ -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
|
0
wowfunding/orm/__init__.py
Normal file
18
wowfunding/orm/connect.py
Normal 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
|
@ -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
|
@ -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)
|
8
wowfunding/routes_admin.py
Normal 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
After Width: | Height: | Size: 89 KiB |
1567
wowfunding/static/css/bootstrap-grid.css
vendored
Normal file
7
wowfunding/static/css/bootstrap-grid.min.css
vendored
Normal file
342
wowfunding/static/css/bootstrap-reboot.css
vendored
Normal 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 */
|
8
wowfunding/static/css/bootstrap-reboot.min.css
vendored
Normal 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
1
wowfunding/static/css/bootstrap.css.map
Normal file
7
wowfunding/static/css/bootstrap.min.css
vendored
Normal file
1
wowfunding/static/css/bootstrap.min.css.map
Normal file
328
wowfunding/static/css/simplemde.css
Normal 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;
|
||||
}
|
7
wowfunding/static/css/simplemde.min.css
vendored
Normal file
354
wowfunding/static/css/wow.css
Normal 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
After Width: | Height: | Size: 182 KiB |
BIN
wowfunding/static/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
wowfunding/static/ie9.png
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
wowfunding/static/java.png
Normal file
After Width: | Height: | Size: 18 KiB |
12
wowfunding/static/js/app.js
Normal 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
4
wowfunding/static/js/jquery-3.2.1.slim.min.js
vendored
Normal file
2028
wowfunding/static/js/simplemde.js
Normal file
15
wowfunding/static/js/simplemde.min.js
vendored
Normal file
BIN
wowfunding/static/nasa.png
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
wowfunding/static/point-left.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
wowfunding/static/wowdoge-a.jpg
Normal file
After Width: | Height: | Size: 180 KiB |
38
wowfunding/templates/about.html
Normal 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 %}
|
72
wowfunding/templates/base.html
Normal 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>
|
66
wowfunding/templates/comments.html
Normal 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>
|
||||
<!--<!– Comments Form –>-->
|
||||
<!--<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>-->
|
||||
|
||||
<!--<!– Single Comment –>-->
|
||||
<!--<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>-->
|
||||
|
||||
<!--<!– Comment with nested comments –>-->
|
||||
<!--<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>-->
|
68
wowfunding/templates/login.html
Normal 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 %}
|
37
wowfunding/templates/navbar.html
Normal 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>
|
127
wowfunding/templates/proposal.html
Normal 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 %}
|
150
wowfunding/templates/proposal_edit.html
Normal 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 %}
|
104
wowfunding/templates/proposals.html
Normal 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">-->
|
||||
|
||||
<!--<!– Pagination –>-->
|
||||
<!--<ul class="pagination mb-4">-->
|
||||
<!--<li class="page-item">-->
|
||||
<!--<a class="page-link" href="#">← Older</a>-->
|
||||
<!--</li>-->
|
||||
<!--<li class="page-item disabled">-->
|
||||
<!--<a class="page-link" href="#">Newer →</a>-->
|
||||
<!--</li>-->
|
||||
<!--</ul>-->
|
||||
<!--</div>-->
|
||||
<br>
|
||||
|
||||
</div>
|
||||
<!-- /.container -->
|
||||
{% endblock %}
|
56
wowfunding/templates/register.html
Normal 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 %}
|
39
wowfunding/templates/search.html
Normal 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 %}
|
80
wowfunding/templates/sidebar.html
Normal 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>
|
43
wowfunding/templates/user.html
Normal 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 %}
|