init skelly
parent
e00284bc7e
commit
637f213d4a
@ -1,2 +1 @@
|
||||
# lza-quart-app
|
||||
Template project for Quart (Python/Flask) applications.
|
||||
# web3-flipbook
|
@ -1,7 +0,0 @@
|
||||
from myapp.factory import create_app
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
@ -1,16 +0,0 @@
|
||||
version: '3'
|
||||
services:
|
||||
db:
|
||||
image: postgres:9.6.15-alpine
|
||||
ports:
|
||||
- 127.0.0.1:5432:5432
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${DB_PASS}
|
||||
POSTGRES_USER: ${DB_USER:-myapp}
|
||||
POSTGRES_DB: ${DB_NAME:-myapp}
|
||||
volumes:
|
||||
- ${DATA_DIR:-./data/postgresql}:/var/lib/postgresql/data
|
||||
cache:
|
||||
image: redis:latest
|
||||
ports:
|
||||
- 127.0.0.1:6379:6379
|
@ -1,16 +1,9 @@
|
||||
DB_PASS=xxxxxxxxxxxxxxxxxxx
|
||||
DB_USER=myapp
|
||||
DB_NAME=myapp
|
||||
DB_USER=flipbook
|
||||
DB_NAME=flipbook
|
||||
DB_HOST=localhost
|
||||
|
||||
XMR_WALLET_PATH=/data/xmr-wallet
|
||||
XMR_WALLET_PASS=xxxxxxxxxxxxxxxxxxx
|
||||
XMR_WALLET_RPC_USER=xxxxxxxxxx
|
||||
XMR_WALLET_RPC_PASS=xxxxxxxxxxxxxxxxxxx
|
||||
XMR_WALLET_RPC_ENDPOINT=http://localhost:9090
|
||||
XMR_DAEMON_URI=http://super.fast.node.xmr.pm:38089
|
||||
|
||||
SITE_NAME=myapp
|
||||
WEB3_PROVIDER_URI=wss://ropsten.infura.io/ws/v3/xxxx
|
||||
SITE_NAME=flipbook
|
||||
SECRET_KEY=xxxxxxxxxxxxxxxxxxx
|
||||
STATS_TOKEN=xxxxxxxxxxxxxxxxxxxx
|
||||
SERVER_NAME=localhost:5000
|
||||
|
@ -0,0 +1 @@
|
||||
from flipbook.factory import create_app
|
@ -0,0 +1,30 @@
|
||||
import click
|
||||
from quart import Blueprint, current_app
|
||||
|
||||
# from flipbook.models import MyThing
|
||||
from flipbook.factory import db
|
||||
|
||||
|
||||
bp = Blueprint('filters', 'filters')
|
||||
|
||||
@bp.cli.command('init')
|
||||
def init():
|
||||
import app.models
|
||||
db.create_all()
|
||||
|
||||
# @bp.cli.command('delete')
|
||||
# @click.argument('thing_id')
|
||||
# def delete(thing_id):
|
||||
# thing = MyThing.query.get(thing_id)
|
||||
# if thing:
|
||||
# db.session.delete(thing)
|
||||
# db.session.commit()
|
||||
# click.echo(f'MyThing {thing.id} was deleted')
|
||||
# else:
|
||||
# click.echo('MyThing ID does not exist')
|
||||
|
||||
# @bp.cli.command('list')
|
||||
# def list_things():
|
||||
# thing = MyThing.query.all()
|
||||
# for i in thing:
|
||||
# click.echo(i.id)
|
@ -0,0 +1,48 @@
|
||||
from pathlib import Path
|
||||
from json import loads
|
||||
from secrets import token_urlsafe
|
||||
from os import getenv
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Site meta
|
||||
SITE_NAME = getenv('SITE_NAME', 'Flipbook')
|
||||
SECRET_KEY = getenv('SECRET_KEY', token_urlsafe(12))
|
||||
SERVER_NAME = getenv('SERVER_NAME', '127.0.0.1:5000')
|
||||
|
||||
# Web3
|
||||
WEB3_PROVIDER_URI = getenv('WEB3_PROVIDER_URI')
|
||||
CONTRACT_ABI = loads(Path('flipbook/library/abi/flipbook.json').open().read())
|
||||
CONTRACT_ADDRESS = getenv('CONTRACT_ADDRESS')
|
||||
|
||||
# Uploads
|
||||
MAX_CONTENT_LENGTH = 50 * 1024 * 1024
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'svg'}
|
||||
UPLOADS_PATH = getenv('UPLOADS_PATH', 'data/uploads')
|
||||
SESSION_LIFETIME = getenv('SESSION_LIFETIME', 30)
|
||||
|
||||
# Database
|
||||
DB_HOST = getenv('DB_HOST', 'localhost')
|
||||
DB_PORT = getenv('DB_PORT', 5432)
|
||||
DB_NAME = getenv('DB_NAME', 'flipbook')
|
||||
DB_USER = getenv('DB_USER', 'flipbook')
|
||||
DB_PASS = getenv('DB_PASS')
|
||||
|
||||
# Redis
|
||||
REDIS_HOST = getenv('REDIS_HOST', 'localhost')
|
||||
REDIS_PORT = getenv('REDIS_PORT', 6379)
|
||||
|
||||
# Development
|
||||
TEMPLATES_AUTO_RELOAD = True
|
||||
DEBUG = False
|
||||
if SERVER_NAME == '127.0.0.1:5000':
|
||||
DEBUG = True
|
||||
|
||||
# Twitter
|
||||
TWITTER_CONSUMER_KEY = getenv('TWITTER_CONSUMER_KEY', None)
|
||||
TWITTER_CONSUMER_SECRET = getenv('TWITTER_CONSUMER_SECRET', None)
|
||||
TWITTER_ACCESS_TOKEN = getenv('TWITTER_ACCESS_TOKEN', None)
|
||||
TWITTER_ACCESS_SECRET = getenv('TWITTER_ACCESS_SECRET', None)
|
@ -0,0 +1,37 @@
|
||||
import quart.flask_patch
|
||||
from quart import Quart
|
||||
from flask_login import LoginManager
|
||||
|
||||
from flipbook import config
|
||||
|
||||
|
||||
async def setup_db(app: Quart):
|
||||
import peewee
|
||||
import flipbook.models
|
||||
models = peewee.Model.__subclasses__()
|
||||
for m in models:
|
||||
m.create_table()
|
||||
|
||||
def create_app():
|
||||
app = Quart(__name__)
|
||||
app.config.from_envvar('FLASK_SECRETS')
|
||||
login_manager = LoginManager(app)
|
||||
login_manager.logout_view = 'meta.logout'
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
from flipbook.models import Wallet
|
||||
Wallet = Wallet.get(user_id)
|
||||
return Wallet
|
||||
|
||||
@app.before_serving
|
||||
async def startup():
|
||||
from flipbook.routes import meta, api
|
||||
from flipbook import filters
|
||||
await setup_db(app)
|
||||
app.register_blueprint(meta.bp)
|
||||
app.register_blueprint(api.bp)
|
||||
app.register_blueprint(filters.bp)
|
||||
# app.register_blueprint(cli.bp)
|
||||
|
||||
return app
|
@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from quart import Blueprint, current_app
|
||||
from quart import Blueprint
|
||||
|
||||
|
||||
bp = Blueprint('filters', 'filters')
|
@ -0,0 +1,11 @@
|
||||
from web3.auto import w3
|
||||
from eth_account.messages import encode_defunct
|
||||
|
||||
|
||||
def verify_signature(message, signature, public_address):
|
||||
msg = encode_defunct(text=message)
|
||||
recovered = w3.eth.account.recover_message(msg, signature=signature)
|
||||
if recovered.lower() == public_address.lower():
|
||||
return True
|
||||
else:
|
||||
return False
|
@ -0,0 +1 @@
|
||||
{}
|
@ -0,0 +1,68 @@
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from peewee import *
|
||||
from PIL import Image
|
||||
from flask_login import login_user
|
||||
|
||||
from flipbook import config
|
||||
|
||||
|
||||
db = SqliteDatabase(f"data/flipbook.sqlite")
|
||||
|
||||
|
||||
def rand_id():
|
||||
return uuid4().hex
|
||||
|
||||
|
||||
class Wallet(Model):
|
||||
id = AutoField()
|
||||
address = CharField(null=False, unique=True)
|
||||
register_date = DateTimeField(default=datetime.utcnow)
|
||||
login_date = DateTimeField(null=True)
|
||||
opensea_handle = CharField(null=True)
|
||||
twitter_handle = CharField(null=True)
|
||||
nonce = CharField(default=rand_id())
|
||||
nonce_date = DateTimeField(default=datetime.utcnow)
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
def generate_nonce(self):
|
||||
return rand_id()
|
||||
|
||||
def change_nonce(self):
|
||||
self.nonce = rand_id()
|
||||
self.nonce_date = datetime.utcnow()
|
||||
self.save()
|
||||
|
||||
def login(self):
|
||||
self.change_nonce()
|
||||
self.last_login_date = datetime.utcnow()
|
||||
login_user(self)
|
||||
self.save()
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
|
||||
|
||||
class Upload(Model):
|
||||
id = AutoField()
|
||||
token_id = IntegerField()
|
||||
title = CharField()
|
||||
text = CharField(null=True)
|
||||
wallet = ForeignKeyField(Wallet)
|
||||
image_name = CharField()
|
||||
upload_date = DateTimeField(default=datetime.utcnow)
|
||||
|
||||
class Meta:
|
||||
database = db
|
@ -0,0 +1,92 @@
|
||||
import json
|
||||
from secrets import token_urlsafe
|
||||
|
||||
from quart import Blueprint, jsonify, request
|
||||
from flask_login import current_user
|
||||
|
||||
from flipbook.helpers import verify_signature
|
||||
from flipbook.models import Wallet, rand_id
|
||||
|
||||
|
||||
bp = Blueprint('api', 'api', url_prefix='/api/v1')
|
||||
|
||||
|
||||
@bp.route('/user_authenticated')
|
||||
async def user_authenticated():
|
||||
"""
|
||||
Check to see if sender is authenticated.
|
||||
Useful for AJAX calls to "check" we're still authenticated
|
||||
instead of assuming (especially with old, loaded forms/pages).
|
||||
"""
|
||||
return jsonify(current_user.is_authenticated)
|
||||
|
||||
|
||||
@bp.route('/user_exists/<wallet_address>')
|
||||
async def user_exists(wallet_address):
|
||||
"""
|
||||
Check to see if a given wallet exists in the database.
|
||||
This logic will help the login/connect MetaMask flow.
|
||||
"""
|
||||
nonce = rand_id()
|
||||
wallet = Wallet.select().where(
|
||||
Wallet.address == wallet_address.lower()
|
||||
).first()
|
||||
if wallet:
|
||||
nonce = wallet.nonce
|
||||
return jsonify({
|
||||
'user_exists': wallet is not None,
|
||||
'nonce': nonce,
|
||||
'success': True
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/authenticate/metamask', methods=['POST'])
|
||||
async def authenticate_metamask():
|
||||
"""
|
||||
This is the login/authenticate route for this dApp.
|
||||
Users POST a `signedData` blob, a message signed by the user with MetaMask
|
||||
(`personal_sign` method).
|
||||
This route will verify the signed data against the user's public ETH
|
||||
address. If no user exists, they get an entry in the database.
|
||||
If user does exist, they get logged in.
|
||||
"""
|
||||
data = await request.get_data()
|
||||
data = json.loads(data)
|
||||
if current_user.is_authenticated:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Already registered and authenticated.'
|
||||
})
|
||||
|
||||
_u = Wallet.select().where(
|
||||
Wallet.address == data['public_address']
|
||||
).first()
|
||||
|
||||
if _u:
|
||||
if data['message'].endswith(_u.nonce):
|
||||
if verify_signature(data['message'], data['signed_data'], data['public_address']):
|
||||
_u.login()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Logged in'
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Invalid signature'
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Invalid nonce in signed message'
|
||||
})
|
||||
else:
|
||||
w = Wallet(
|
||||
address=data['public_address'].lower()
|
||||
)
|
||||
w.save()
|
||||
w.login()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Registered'
|
||||
})
|
@ -0,0 +1,30 @@
|
||||
from quart import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask_login import logout_user
|
||||
|
||||
|
||||
bp = Blueprint('meta', 'meta')
|
||||
|
||||
@bp.route('/')
|
||||
async def index():
|
||||
return await render_template('index.html')
|
||||
|
||||
|
||||
@bp.route('/logout')
|
||||
async def logout():
|
||||
"""
|
||||
Log the current user out and redirect someplace within app if needed.
|
||||
If 'next' is in the request args and is valid route, redirect there,
|
||||
otherwise, redirect to peel off args and go home.
|
||||
"""
|
||||
logout_user()
|
||||
if 'type' in request.args:
|
||||
if request.args['type'] == 'accountsChanged':
|
||||
flash('Metamask accounts have been changed, logging you out.', 'info')
|
||||
if 'next' in request.args:
|
||||
next_url = request.args['next']
|
||||
if next_url.startswith('/'):
|
||||
return redirect(next_url)
|
||||
else:
|
||||
return redirect(url_for('meta.index'))
|
||||
|
||||
return redirect(url_for('meta.index'))
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{% include 'includes/head.html' %}
|
||||
<body>
|
||||
{% include 'includes/header.html' %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="one-half column" style="margin-top: 25%">
|
||||
<h4>Basic Page</h4>
|
||||
<p>This index.html page is a placeholder with the CSS, font and favicon. It's just waiting for you to add some content! If you need some help hit up the <a href="http://www.getskeleton.com">Skeleton documentation</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% include 'includes/footer.html' %}
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,13 @@
|
||||
<header id="header">
|
||||
<h1 id="logo"><a href="/">{{ config.SITE_NAME }}</a></h1>
|
||||
<nav id="nav">
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
{% if current_user.is_authenticated %}
|
||||
<li><a href="{{ url_for('meta.logout') }}" class="button">Logout</a></li>
|
||||
{% else %}
|
||||
<li><a href="#" id="connectWallet" class="button">Connect</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
@ -0,0 +1,73 @@
|
||||
<script src="/static/js/main.js"></script>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<p>{{ message }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- <script src="/static/js/vendor/metamask-onboarding-1.0.1.bundle.js"></script> -->
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const connectButton = document.getElementById('connectWallet');
|
||||
let nonce = 0;
|
||||
|
||||
async function getSignedData(publicAddress, jsonData) {
|
||||
const signedData = await window.ethereum.request({
|
||||
method: 'eth_signTypedData_v3',
|
||||
params: [publicAddress, JSON.stringify(jsonData)]
|
||||
});
|
||||
console.log(signedData);
|
||||
return signedData
|
||||
}
|
||||
|
||||
async function connectWallet() {
|
||||
let userExists;
|
||||
const allAccounts = await window.ethereum.request({
|
||||
method: 'eth_requestAccounts',
|
||||
});
|
||||
await fetch('/api/v1/user_exists/' + allAccounts[0])
|
||||
.then((resp) => resp.json())
|
||||
.then(function(data) {
|
||||
if (!data['success']) {
|
||||
console.log('error checking user_exists!')
|
||||
return
|
||||
}
|
||||
console.log(data);
|
||||
nonce = data['nonce'];
|
||||
})
|
||||
|
||||
const msg = 'Authentication request from Flipbook app! Verifying message with nonce ' + nonce
|
||||
const signedData = await window.ethereum.request({
|
||||
method: 'personal_sign',
|
||||
params: [msg, allAccounts[0]]
|
||||
});
|
||||
console.log('Signing data with msg (' + msg + '), address (' + allAccounts[0] + '), and nonce (' + nonce + ')')
|
||||
console.log(signedData);
|
||||
|
||||
await fetch('{{ url_for("api.authenticate_metamask" ) }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'signed_data': signedData,
|
||||
'public_address': allAccounts[0],
|
||||
'nonce': nonce,
|
||||
'message': msg,
|
||||
})
|
||||
})
|
||||
.then((resp) => resp.json())
|
||||
.then(function(data) {
|
||||
console.log(data)
|
||||
if (data['success']) {
|
||||
window.location.href = '/'
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
if (connectButton) connectButton.onclick = async () => connectWallet();
|
||||
});
|
||||
</script>
|
@ -0,0 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<p>fuck this</p>
|
||||
{% endblock %}
|
@ -1,30 +0,0 @@
|
||||
import click
|
||||
from quart import Blueprint, current_app
|
||||
|
||||
from myapp.models import MyThing
|
||||
from myapp.factory import db
|
||||
|
||||
|
||||
bp = Blueprint('filters', 'filters')
|
||||
|
||||
@bp.cli.command('init')
|
||||
def init():
|
||||
import app.models
|
||||
db.create_all()
|
||||
|
||||
@bp.cli.command('delete')
|
||||
@click.argument('thing_id')
|
||||
def delete(thing_id):
|
||||
thing = MyThing.query.get(thing_id)
|
||||
if thing:
|
||||
db.session.delete(thing)
|
||||
db.session.commit()
|
||||
click.echo(f'MyThing {thing.id} was deleted')
|
||||
else:
|
||||
click.echo('MyThing ID does not exist')
|
||||
|
||||
@bp.cli.command('list')
|
||||
def list_things():
|
||||
thing = MyThing.query.all()
|
||||
for i in thing:
|
||||
click.echo(i.id)
|
@ -1,33 +0,0 @@
|
||||
from dotenv import load_dotenv
|
||||
from secrets import token_urlsafe
|
||||
from os import getenv
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Site meta
|
||||
SITE_NAME = getenv('SITE_NAME', 'MyApp')
|
||||
SECRET_KEY = getenv('SECRET_KEY')
|
||||
STATS_TOKEN = getenv('STATS_TOKEN', token_urlsafe(8))
|
||||
SERVER_NAME = getenv('SERVER_NAME', 'localhost:5000')
|
||||
|
||||
# Crypto RPC
|
||||
XMR_WALLET_PASS = getenv('XMR_WALLET_PASS')
|
||||
XMR_WALLET_RPC_USER = getenv('XMR_WALLET_RPC_USER')
|
||||
XMR_WALLET_RPC_PASS = getenv('XMR_WALLET_RPC_PASS')
|
||||
XMR_WALLET_RPC_ENDPOINT = getenv('XMR_WALLET_RPC_ENDPOINT')
|
||||
XMR_DAEMON_URI = getenv('XMR_DAEMON_URI')
|
||||
|
||||
# Database
|
||||
DB_HOST = getenv('DB_HOST', 'localhost')
|
||||
DB_PORT = getenv('DB_PORT', 5432)
|
||||
DB_NAME = getenv('DB_NAME', 'myapp')
|
||||
DB_USER = getenv('DB_USER', 'myapp')
|
||||
DB_PASS = getenv('DB_PASS')
|
||||
|
||||
# Redis
|
||||
REDIS_HOST = getenv('REDIS_HOST', 'localhost')
|
||||
REDIS_PORT = getenv('REDIS_PORT', 6379)
|
||||
|
||||
# Development
|
||||
TEMPLATES_AUTO_RELOAD = True
|
@ -1,37 +0,0 @@
|
||||
import quart.flask_patch
|
||||
from quart import Quart
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
from myapp import config
|
||||
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
async def _setup_db(app: Quart):
|
||||
uri = 'postgresql+psycopg2://{user}:{pw}@{host}:{port}/{db}'.format(
|
||||
user=config.DB_USER,
|
||||
pw=config.DB_PASS,
|
||||
host=config.DB_HOST,
|
||||
port=config.DB_PORT,
|
||||
db=config.DB_NAME
|
||||
)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = uri
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
def create_app():
|
||||
app = Quart(__name__)
|
||||
app.config.from_envvar('QUART_SECRETS')
|
||||
|
||||
|
||||
@app.before_serving
|
||||
async def startup():
|
||||
from myapp.routes import meta, api
|
||||
from myapp import filters
|
||||
await _setup_db(app)
|
||||
app.register_blueprint(meta.bp)
|
||||
app.register_blueprint(api.bp)
|
||||
app.register_blueprint(filters.bp)
|
||||
# app.register_blueprint(cli.bp)
|
||||
|
||||
return app
|
@ -1,8 +0,0 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, BooleanField
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
|
||||
class Login(FlaskForm):
|
||||
email = StringField('Email Address:', validators=[DataRequired()], render_kw={"placeholder": "Email", "class": "form-control", "type": "email"})
|
||||
password = StringField('Password:', validators=[DataRequired()], render_kw={"placeholder": "Password", "class": "form-control", "type": "password"})
|
@ -1,38 +0,0 @@
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from myapp.factory import db
|
||||
from myapp import config
|
||||
|
||||
|
||||
def rand_id():
|
||||
return uuid4().hex
|
||||
|
||||
class MyThing(db.Model):
|
||||
__tablename__ = 'swaps'
|
||||
|
||||
# Meta
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
# id = db.Column(db.String(80), primary_key=True, default=rand_id) # hex based id
|
||||
date = db.Column(db.DateTime, server_default=func.now())
|
||||
my_bool = db.Column(db.Boolean)
|
||||
my_int = db.Column(db.Integer)
|
||||
my_str = db.Column(db.String(150))
|
||||
completed = db.Column(db.Boolean, default=False)
|
||||
completed_date = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return self.id
|
||||
|
||||
def hours_elapsed(self):
|
||||
now = datetime.utcnow()
|
||||
if since_completed:
|
||||
if self.completed_date:
|
||||
diff = now - self.completed_date
|
||||
else:
|
||||
return 0
|
||||
else:
|
||||
diff = now - self.date
|
||||
return diff.total_seconds() / 60 / 60
|
@ -1,11 +0,0 @@
|
||||
from quart import Blueprint, jsonify
|
||||
|
||||
|
||||
bp = Blueprint('api', 'api')
|
||||
|
||||
@bp.route('/api/test')
|
||||
async def get_prices():
|
||||
return jsonify({
|
||||
'test': True,
|
||||
'message': 'This is only a test.'
|
||||
})
|
@ -1,8 +0,0 @@
|
||||
from quart import Blueprint, render_template
|
||||
|
||||
|
||||
bp = Blueprint('meta', 'meta')
|
||||
|
||||
@bp.route('/')
|
||||
async def index():
|
||||
return await render_template('index.html')
|
@ -1,9 +0,0 @@
|
||||
<header id="header">
|
||||
<h1 id="logo"><a href="/">{{ config.SITE_NAME }}</a></h1>
|
||||
<nav id="nav">
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/#search" class="button primary">Search</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
@ -1,9 +0,0 @@
|
||||
<script src="/static/js/main.js"></script>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<p>{{ message }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
@ -1,37 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
{% include 'includes/head.html' %}
|
||||
|
||||
<body class="is-preload landing">
|
||||
<div id="page-wrapper">
|
||||
|
||||
<header id="header">
|
||||
<h1 id="logo"><a href="/">MyThing sample app</a></h1>
|
||||
<nav id="nav">
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/#search" class="button primary">Search</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section id="banner">
|
||||
<div class="content">
|
||||
<header>
|
||||
<h2>MyThing</h2>
|
||||
<p>This is a sample app.</p>
|
||||
</header>
|
||||
<span class="image"><img src="/static/images/monero-logo.png" width=150px></span>
|
||||
</div>
|
||||
<a href="#swap" class="goto-next scrolly">Next</a>
|
||||
</section>
|
||||
|
||||
{% include 'includes/footer.html' %}
|
||||
|
||||
</div>
|
||||
|
||||
{% include 'includes/scripts.html' %}
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,14 +1,12 @@
|
||||
Flask
|
||||
Flask-SQLAlchemy
|
||||
Flask-WTF
|
||||
flask-login
|
||||
gunicorn
|
||||
hypercorn
|
||||
Pillow
|
||||
psycopg2-binary
|
||||
python-dotenv
|
||||
qrcode
|
||||
redis
|
||||
requests
|
||||
SQLAlchemy
|
||||
WTForms
|
||||
quart
|
||||
peewee
|
||||
arrow
|
||||
black
|
||||
web3
|
||||
|
Loading…
Reference in New Issue