import proxadmin as example app
|
@ -1,57 +1,164 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from werkzeug.utils import secure_filename
|
||||
from flask import (
|
||||
Flask,
|
||||
g,
|
||||
render_template,
|
||||
jsonify,
|
||||
send_from_directory,
|
||||
request,
|
||||
redirect,
|
||||
url_for
|
||||
)
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
from flask.json import JSONEncoder
|
||||
from flask_bootstrap import Bootstrap
|
||||
from flask_mail import Mail
|
||||
from flask_login import LoginManager
|
||||
from flask_pagedown import PageDown
|
||||
from flask_wtf.csrf import CSRFProtect, CSRFError
|
||||
from flask_babel import Babel, lazy_gettext
|
||||
from flask_moment import Moment
|
||||
#from flask_httpauth import import import HTTPBasicAuth
|
||||
from werkzeug.contrib.fixers import ProxyFix
|
||||
from config import config
|
||||
|
||||
sys.stderr.write("worker uid={} gid={}".format(os.getuid(), os.getgid()))
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object("project.config.Config")
|
||||
app.config.from_object("forest.config.Config")
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
#db = SQLAlchemy(session_options = { "autoflush": False })
|
||||
db.init_app(app)
|
||||
|
||||
#apiauth = HTTPBasicAuth()
|
||||
lm = LoginManager()
|
||||
lm.init_app(app)
|
||||
lm.login_view = 'auth.login'
|
||||
lm.login_message = 'Login Required.'
|
||||
lm.session_protection = 'strong'
|
||||
#lm.session_protection = 'basic'
|
||||
|
||||
mail = Mail()
|
||||
mail.init_app(app)
|
||||
|
||||
bootstrap = Bootstrap()
|
||||
bootstrap.init_app(app)
|
||||
|
||||
pagedown = PageDown(app)
|
||||
|
||||
csrf = CSRFProtect(app)
|
||||
#csrf.init_app(app)
|
||||
|
||||
babel = Babel()
|
||||
babel.init_app(app)
|
||||
|
||||
moment = Moment(app)
|
||||
moment.init_app(app)
|
||||
|
||||
from .main import main as main_blueprint
|
||||
app.register_blueprint(main_blueprint)
|
||||
|
||||
from .panel import panel as panel_blueprint
|
||||
app.register_blueprint(panel_blueprint, url_prefix='/panel')
|
||||
|
||||
from .auth import auth as auth_blueprint
|
||||
app.register_blueprint(auth_blueprint, url_prefix='/auth')
|
||||
|
||||
from .admin import admin as admin_blueprint
|
||||
app.register_blueprint(admin_blueprint, url_prefix='/' + app.config['ADMIN_PREFIX'])
|
||||
|
||||
from .settings import settings as settings_blueprint
|
||||
app.register_blueprint(settings_blueprint, url_prefix='/settings')
|
||||
|
||||
class CustomJSONEncoder(JSONEncoder):
|
||||
"""This class adds support for lazy translation texts to Flask's
|
||||
JSON encoder. This is necessary when flashing translated texts."""
|
||||
def default(self, obj):
|
||||
from speaklater import is_lazy_string
|
||||
if is_lazy_string(obj):
|
||||
try:
|
||||
return unicode(obj) # python 2
|
||||
except NameError:
|
||||
return str(obj) # python 3
|
||||
return super(CustomJSONEncoder, self).default(obj)
|
||||
|
||||
#app.json_encoder = CustomJSONEncoder
|
||||
|
||||
@app.errorhandler(403)
|
||||
def forbidden(e):
|
||||
if request.accept_mimetypes.accept_json and \
|
||||
not request.accept_mimetypes.accept_html:
|
||||
response = jsonify({'error': 'forbidden'})
|
||||
response.status_code = 403
|
||||
return response
|
||||
return render_template('errors/403.html'), 403
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
if request.accept_mimetypes.accept_json and \
|
||||
not request.accept_mimetypes.accept_html:
|
||||
response = jsonify({'error': 'not found'})
|
||||
response.status_code = 404
|
||||
return response
|
||||
return render_template('errors/404.html'), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_server_error(e):
|
||||
if request.accept_mimetypes.accept_json and \
|
||||
not request.accept_mimetypes.accept_html:
|
||||
response = jsonify({'error': 'internal server error'})
|
||||
response.status_code = 500
|
||||
return response
|
||||
return render_template('errors/500.html'), 500
|
||||
|
||||
@app.errorhandler(503)
|
||||
def service_unavailable(e):
|
||||
if request.accept_mimetypes.accept_json and \
|
||||
not request.accept_mimetypes.accept_html:
|
||||
response = jsonify({'error': 'service unavailable'})
|
||||
response.status_code = 503
|
||||
return response
|
||||
return render_template('errors/503.html'), 503
|
||||
|
||||
@app.errorhandler(CSRFError)
|
||||
def handle_csrf_error(e):
|
||||
return render_template('errors/csrf_error.html', reason=e.description), 400
|
||||
|
||||
@babel.localeselector
|
||||
def get_locale():
|
||||
return request.accept_languages.best_match(app.config['SUPPORTED_LOCALES'])
|
||||
|
||||
#@app.before_request
|
||||
#def before_request():
|
||||
# g.request_start_time = time.time()
|
||||
# g.request_time = lambda: '%.5fs' % (time.time() - g.request_start_time)
|
||||
# g.pjax = 'X-PJAX' in request.headers
|
||||
|
||||
if not app.config['DEBUG'] == 1 and app.config['MAIL_SERVER'] != '':
|
||||
import logging
|
||||
from logging.handlers import SMTPHandler
|
||||
credentials = None
|
||||
secure = None
|
||||
if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
|
||||
credentials = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
|
||||
if app.config['MAIL_USE_TLS'] is None:
|
||||
secure = ()
|
||||
mail_handler = SMTPHandler(
|
||||
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
|
||||
fromaddr=app.config['MAIL_SENDER'],
|
||||
toaddrs=[app.config['MAIL_ADMIN']],
|
||||
subject=app.config['MAIL_SUBJECT_PREFIX'] + ' Application Error',
|
||||
credentials=credentials,
|
||||
secure=secure)
|
||||
mail_handler.setLevel(logging.ERROR)
|
||||
app.logger.addHandler(mail_handler)
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(128), unique=True, nullable=False)
|
||||
active = db.Column(db.Boolean(), default=True, nullable=False)
|
||||
|
||||
def __init__(self, email):
|
||||
self.email = email
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def hello_world():
|
||||
return jsonify(hello="world")
|
||||
|
||||
|
||||
@app.route("/static/<path:filename>")
|
||||
def staticfiles(filename):
|
||||
return send_from_directory(app.config["STATIC_FOLDER"], filename)
|
||||
|
||||
|
||||
@app.route("/media/<path:filename>")
|
||||
def mediafiles(filename):
|
||||
return send_from_directory(app.config["MEDIA_FOLDER"], filename)
|
||||
|
||||
|
||||
@app.route("/upload", methods=["GET", "POST"])
|
||||
def upload_file():
|
||||
if request.method == "POST":
|
||||
file = request.files["file"]
|
||||
filename = secure_filename(file.filename)
|
||||
file.save(os.path.join(app.config["MEDIA_FOLDER"], filename))
|
||||
return """
|
||||
<!doctype html>
|
||||
<title>upload new File</title>
|
||||
<form action="" method=post enctype=multipart/form-data>
|
||||
<p><input type=file name=file><input type=submit value=Upload>
|
||||
</form>
|
||||
"""
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
|
|
3
flask/forest/admin/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from flask import Blueprint
|
||||
admin = Blueprint('admin', __name__)
|
||||
from . import routes
|
34
flask/forest/admin/forms.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
import string
|
||||
import random
|
||||
|
||||
from .. import db
|
||||
from ..models import User, Role, Region
|
||||
|
||||
from flask_wtf import FlaskForm, RecaptchaField
|
||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectField, DecimalField
|
||||
from wtforms import validators, ValidationError
|
||||
from wtforms.fields.html5 import EmailField, DecimalRangeField
|
||||
|
||||
class OrderForm(FlaskForm):
|
||||
cpu = DecimalRangeField('Processor Cores', default=2)
|
||||
memory = DecimalRangeField('Memory', default=512)
|
||||
storage = DecimalRangeField('Storage', default=20)
|
||||
alias = StringField('Machine Alias:', [validators.Regexp(message='ex.: myservice1.com, myservice2.local', regex='^[a-zA-Z0-9][a-zA-Z0-9-_]{0,61}[a-zA-Z0-9]{0,1}\.([a-zA-Z]{1,6}|[a-zA-Z0-9-]{1,30}\.[a-zA-Z]{2,3})$'), validators.Length(6,64)])
|
||||
submit = SubmitField('Create')
|
||||
|
||||
class ChargeForm(FlaskForm):
|
||||
amount = DecimalField('Стойност:', [validators.DataRequired(), validators.NumberRange(min=1, max=500)])
|
||||
submit = SubmitField('Зареди')
|
||||
|
||||
class Addr2PoolForm(FlaskForm):
|
||||
#regions = Region.query.all()
|
||||
#region_choices = []
|
||||
#for region in regions:
|
||||
# region_choices.expand((region.pid, str(region.description)))
|
||||
region_choices = [(1, 'Plovdiv, Bulgaria')]
|
||||
region = SelectField('Region', choices=region_choices, coerce=int)
|
||||
ip = StringField('IP Address:', [validators.DataRequired(), validators.Regexp(message='172.16.0.1', regex='^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')])
|
||||
rdns = StringField('Reverse DNS:', [validators.Optional(), validators.Regexp(message='must be fqdn', regex='^[a-zA-Z0-9][a-zA-Z0-9-_]{0,61}[a-zA-Z0-9]{0,1}\.([a-zA-Z]{1,6}|[a-zA-Z0-9-]{1,30}\.[a-zA-Z]{2,3})$')])
|
||||
reserved = BooleanField('Reserved:')
|
||||
submit = SubmitField('Add IP')
|
||||
|
182
flask/forest/admin/routes.py
Normal file
|
@ -0,0 +1,182 @@
|
|||
from flask import render_template, abort, redirect, url_for, abort, flash, request, current_app, make_response, g
|
||||
from flask_login import fresh_login_required, login_user, logout_user
|
||||
from flask_sqlalchemy import get_debug_queries
|
||||
|
||||
from . import admin
|
||||
from .forms import ChargeForm, Addr2PoolForm, OrderForm
|
||||
|
||||
from .. import db
|
||||
from ..email import send_email
|
||||
from ..models import User, Transaction, Order, Server, Deployment, Service, Region, Address, Domain, contact_proxmaster
|
||||
from ..decorators import admin_required, permission_required
|
||||
|
||||
import base64
|
||||
import string
|
||||
import random
|
||||
from datetime import datetime, timedelta, date, time
|
||||
import ipaddress
|
||||
|
||||
#@admin.before_app_request
|
||||
#def before_request():
|
||||
# g.user = current_user
|
||||
# print('current_user: %s, g.user: %s, leaving bef_req' % (current_user, g.user))
|
||||
|
||||
@admin.after_app_request
|
||||
def after_request(response):
|
||||
for query in get_debug_queries():
|
||||
if query.duration >= current_app.config['SLOW_DB_QUERY_TIME']:
|
||||
current_app.logger.warning('Slow query: %s\nParameters: %s\nDuration: %fs\nContext: %s\n' % (query.statement, query.parameters, query.duration, query.context))
|
||||
return response
|
||||
|
||||
@admin.route("/", methods=['GET'])
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def index():
|
||||
return redirect(url_for('admin.list_users'))
|
||||
|
||||
@admin.route("/listorders", methods=['GET', 'POST'])
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def list_orders():
|
||||
NewOrders = Order.query.filter_by(status='new').order_by(Order.date_created.asc()).all()
|
||||
AcceptedOrders = Order.query.filter_by(status='accepted').order_by(Order.date_created.asc()).all()
|
||||
return render_template('admin/list_orders.html', neworders=NewOrders, oldorders=AcceptedOrders)
|
||||
|
||||
@admin.route("/listdeployments", methods=['GET'])
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def list_deployments():
|
||||
AllDeploymentsProtected = Deployment.query.filter_by(deleted=False).order_by(Deployment.daysleft.asc()).all()
|
||||
statuses = {}
|
||||
for deploy in AllDeploymentsProtected:
|
||||
data = { 'unit_id': int(deploy.machine_id),
|
||||
'type': 'kvm' }
|
||||
try:
|
||||
query = contact_proxmaster(data, 'status')
|
||||
status = { int(deploy.machine_id): str(query['status']) }
|
||||
statuses.update(status)
|
||||
except:
|
||||
pass
|
||||
return render_template('admin/list_deployments.html', deployments=AllDeploymentsProtected, status=statuses)
|
||||
|
||||
@admin.route("/listservices", methods=['GET'])
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def list_services():
|
||||
allservices = Service.query.filter_by(deleted=False).order_by(Service.daysleft.asc()).all()
|
||||
return render_template('admin/list_services.html', services=allservices)
|
||||
|
||||
@admin.route("/listdomains", methods=['GET'])
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def list_domains():
|
||||
alldomains = Domain.query.filter_by(deleted=False).order_by(Domain.daysleft.asc()).all()
|
||||
return render_template('admin/list_domains.html', domains=alldomains)
|
||||
|
||||
@admin.route("/listarchive", methods=['GET'])
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def list_archive():
|
||||
deployments = Deployment.query.filter_by(protected=False).order_by(Deployment.daysleft.asc()).all()
|
||||
services = Service.query.filter_by(deleted=True).all()
|
||||
domains = Domain.query.filter_by(deleted=True).all()
|
||||
return render_template('admin/list_archive.html', deployments=deployments, services=services, domains=domains)
|
||||
|
||||
@admin.route("/listusers/", defaults={'page': 1})
|
||||
@admin.route("/listusers/<int:page>", methods=['GET'])
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def list_users(page):
|
||||
sqlquery = User.query.filter_by(active=True).order_by(User.last_seen.desc()).paginate(page, current_app.config['ITEMS_PER_PAGE'], error_out=False)
|
||||
return render_template('admin/list_users.html', users=sqlquery.items, page=page)
|
||||
|
||||
@admin.route("/charge/<int:user_pid>", methods=['GET', 'POST'])
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def charge(user_pid=0):
|
||||
cuser = User.query.filter_by(pid=user_pid).first()
|
||||
form = ChargeForm()
|
||||
if form.validate_on_submit():
|
||||
transaction = Transaction(user_id=int(cuser.pid), description='Account charged by staff', value=float(form.amount.data))
|
||||
db.session.add(transaction)
|
||||
db.session.commit()
|
||||
|
||||
cuser.wallet += float(form.amount.data)
|
||||
db.session.add(cuser)
|
||||
db.session.commit()
|
||||
return redirect(url_for('admin.list_users'))
|
||||
return render_template('admin/charge.html', form=form, usr=cuser)
|
||||
|
||||
@admin.route("/listaddresses", methods=['GET'])
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def list_addresses():
|
||||
alladdresses = Address.query.all()
|
||||
alladdrlist = []
|
||||
for addr in alladdresses:
|
||||
alladdrlist.append(addr.ip)
|
||||
ipobjs = sorted(ipaddress.ip_address(addr) for addr in alladdrlist)
|
||||
ipnrml = []
|
||||
for ipobj in ipobjs:
|
||||
ipnrml.append(str(ipobj))
|
||||
alladdr = sorted(alladdresses, key=lambda o: ipnrml.index(o.ip))
|
||||
return render_template('admin/list_addresses.html', addresses=alladdr)
|
||||
|
||||
@admin.route("/addr2pool", methods=['GET', 'POST'])
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def addr2pool():
|
||||
alladdrlist = []
|
||||
alladdr = Address.query.all()
|
||||
for addr in alladdr:
|
||||
alladdrlist.append(str(addr.ip))
|
||||
#current_app.logger.info('Current IP pool: {}'.format(alladdrlist))
|
||||
form = Addr2PoolForm()
|
||||
if form.validate_on_submit():
|
||||
if form.ip.data in alladdrlist:
|
||||
flash('IP address {} is already in the pool!'.format(form.ip.data))
|
||||
return redirect(url_for('admin.addr2pool'))
|
||||
address = Address(ip=form.ip.data, rdns=form.rdns.data, region_id=form.region.data, enabled=True, reserved=form.reserved.data)
|
||||
db.session.add(address)
|
||||
db.session.commit()
|
||||
flash('Address {} added to region {}'.format(form.ip.data, form.region.data))
|
||||
return redirect(url_for('admin.addr2pool'))
|
||||
return render_template('admin/addr2pool.html', form=form, alladdresses=alladdrlist)
|
||||
|
||||
@admin.route("/listservers", methods=['GET'])
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def list_servers():
|
||||
allservers = Server.query.all()
|
||||
return render_template('admin/list_servers.html', servers=allservers)
|
||||
|
||||
@admin.route("/listtransactions/", defaults={'page': 1})
|
||||
@admin.route("/listtransactions/<int:page>", methods=['GET'])
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def list_transactions(page):
|
||||
sqlquery = Transaction.query.order_by(Transaction.date_created.desc()).paginate(page, current_app.config['ITEMS_PER_PAGE'], error_out=False)
|
||||
return render_template('admin/list_transactions.html', transactions=sqlquery.items, page=page)
|
||||
|
||||
@admin.route("/transaction/<int:user_pid>", methods=['GET'])
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def transaction(user_pid=0):
|
||||
cuser = User.query.filter_by(pid=user_pid).first()
|
||||
transactions = cuser.inv_transactions.order_by(Transaction.date_created.desc()).limit(20)
|
||||
|
||||
labelslist = ['today']
|
||||
translist = [cuser.wallet]
|
||||
prevvalue = cuser.wallet
|
||||
for tr in transactions:
|
||||
labelslist.insert(0, str(tr.date_created.strftime('%d.%m')))
|
||||
translist.insert(0, prevvalue - tr.value)
|
||||
prevvalue -= tr.value
|
||||
|
||||
if len(labelslist) <= 1:
|
||||
labelslist.insert(0, 'before')
|
||||
translist.insert(0, 0)
|
||||
|
||||
#current_app.logger.info('[{}] transactions: {} {} '.format(cuser.email, translist, labelslist))
|
||||
return render_template('uinvoice/transactions.html', transactions=transactions, translist=translist, labelslist=labelslist, cuser=cuser)
|
||||
|
4
flask/forest/auth/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from flask import Blueprint
|
||||
auth = Blueprint('auth', __name__)
|
||||
from . import routes
|
||||
|
51
flask/forest/auth/forms.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
from flask_wtf import FlaskForm, RecaptchaField
|
||||
|
||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectField, DecimalField
|
||||
from wtforms import validators, ValidationError
|
||||
from wtforms.fields.html5 import EmailField
|
||||
from ..models import User
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
email = EmailField('E-Mail', [validators.DataRequired(), validators.Length(1,64), validators.Email()])
|
||||
password = PasswordField('Password', [validators.DataRequired(), validators.Length(1,128)])
|
||||
remember_me = BooleanField('Remember me ?')
|
||||
#recaptcha = RecaptchaField()
|
||||
submit = SubmitField('Login')
|
||||
|
||||
class TwoFAForm(FlaskForm):
|
||||
token = StringField('Token', [validators.DataRequired(), validators.Length(6, 6)])
|
||||
submit = SubmitField('Confirm')
|
||||
|
||||
class RegistrationForm(FlaskForm):
|
||||
email = StringField('E-Mail', [validators.DataRequired(), validators.Length(6,35), validators.Email()])
|
||||
def validate_email(self, field):
|
||||
if User.query.filter_by(email=field.data).first():
|
||||
raise ValidationError('Error. Please try again.')
|
||||
password = PasswordField('Password', [validators.DataRequired(), validators.EqualTo('confirm', message='Both passwords must be equal')])
|
||||
confirm = PasswordField('Your password again', [validators.DataRequired()])
|
||||
accept_tos = BooleanField('I accept the <a href="/terms">Terms of Service</a>', [validators.DataRequired()])
|
||||
recaptcha = RecaptchaField()
|
||||
submit = SubmitField('REGISTER')
|
||||
|
||||
class ChangePasswordForm(FlaskForm):
|
||||
old_password = PasswordField('Old Password', [validators.DataRequired()])
|
||||
password = PasswordField('New Password', [validators.DataRequired(), validators.EqualTo('confirm', message='Both passwords must be equal')])
|
||||
confirm = PasswordField('Your password again')
|
||||
submit = SubmitField('Renew Password')
|
||||
|
||||
class PasswordResetRequestForm(FlaskForm):
|
||||
email = EmailField('E-Mail', [validators.DataRequired(), validators.Length(1,64), validators.Email()])
|
||||
recaptcha = RecaptchaField()
|
||||
submit = SubmitField('Reset password', [validators.DataRequired()])
|
||||
|
||||
class PasswordResetForm(FlaskForm):
|
||||
email = EmailField('E-Mail', [validators.DataRequired(), validators.Length(1,64), validators.Email()])
|
||||
password = PasswordField('Password', [validators.DataRequired(), validators.EqualTo('confirm', message='Both password fields must be equal')])
|
||||
confirm = PasswordField('Your password again', [validators.DataRequired()])
|
||||
submit = SubmitField('Change password')
|
||||
|
||||
def validate_email(self, field):
|
||||
if User.query.filter_by(email=field.data).first() is None:
|
||||
raise ValidationError('Error. Please try again.')
|
||||
|
234
flask/forest/auth/routes.py
Normal file
|
@ -0,0 +1,234 @@
|
|||
from flask import render_template, redirect, request, url_for, flash, session, abort, current_app
|
||||
from flask_login import login_required, login_user, logout_user, current_user
|
||||
|
||||
from . import auth
|
||||
from .. import db
|
||||
from ..models import User, Transaction
|
||||
from ..email import send_email
|
||||
from .forms import LoginForm, TwoFAForm, RegistrationForm, ChangePasswordForm,PasswordResetRequestForm, PasswordResetForm
|
||||
from ..decorators import admin_required, permission_required
|
||||
|
||||
from io import BytesIO
|
||||
import pyqrcode
|
||||
|
||||
def get_google_auth(state=None, token=None):
|
||||
if token:
|
||||
return OAuth2Session(current_app.config['CLIENT_ID'], token=token)
|
||||
if state:
|
||||
return OAuth2Session(
|
||||
current_app.config['CLIENT_ID'],
|
||||
state=state,
|
||||
redirect_uri=current_app.config['REDIRECT_URI'])
|
||||
oauth = OAuth2Session(
|
||||
current_app.config['CLIENT_ID'],
|
||||
redirect_uri=current_app.config['REDIRECT_URI'],
|
||||
scope=current_app.config['SCOPE'])
|
||||
return oauth
|
||||
|
||||
@auth.before_app_request
|
||||
def before_request():
|
||||
#print('session: %s' % str(session))
|
||||
if current_user.is_authenticated:
|
||||
current_user.ping()
|
||||
#print('request for {} from {}#{}'.format(request.endpoint, current_user.email, current_user.pid))
|
||||
if not current_user.confirmed and request.endpoint[:5] != 'auth.' and request.endpoint != 'static':
|
||||
print(request.endpoint)
|
||||
return redirect(url_for('auth.unconfirmed'))
|
||||
|
||||
|
||||
@auth.route('/unconfirmed')
|
||||
def unconfirmed():
|
||||
if current_user.is_anonymous or current_user.confirmed:
|
||||
return redirect(url_for('main.index'))
|
||||
return render_template('auth/unconfirmed.html')
|
||||
|
||||
@auth.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
page = { 'title': 'Login' }
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(email=form.email.data).first()
|
||||
|
||||
if user is not None and user.verify_password(form.password.data):
|
||||
if user.active == False:
|
||||
flash('User disabled.')
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
if user.twofactor:
|
||||
# redirect to the two-factor auth page, passing username in session
|
||||
session['email'] = user.email
|
||||
session['memberberry'] = form.remember_me.data
|
||||
return redirect(url_for('auth.twofactor'))
|
||||
|
||||
#print('remember: ' + str(form.remember_me.data))
|
||||
login_user(user, form.remember_me.data)
|
||||
previp = user.last_ip
|
||||
if request.headers.getlist("X-Forwarded-For"):
|
||||
lastip = request.headers.getlist("X-Forwarded-For")[0]
|
||||
else:
|
||||
lastip = request.remote_addr
|
||||
user.last_ip = lastip
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
send_email(current_app.config['MAIL_USERNAME'], user.email + ' logged in.', 'auth/email/adm_loginnotify', user=user, ipaddr=lastip )
|
||||
#flash('Last Login: {} from {}'.format(user.last_seen.strftime("%a %d %B %Y %H:%M"), previp))
|
||||
flash('Last Login: {}'.format(user.last_seen.strftime("%a %d %B %Y %H:%M")))
|
||||
return redirect(request.args.get('next') or url_for('panel.dashboard'))
|
||||
else:
|
||||
flash('Invalid username or password.')
|
||||
|
||||
return render_template('auth/login.html', page=page, form=form)
|
||||
|
||||
|
||||
@auth.route('/twofactor', methods=['GET', 'POST'])
|
||||
def twofactor():
|
||||
if 'email' not in session:
|
||||
abort(404)
|
||||
if 'memberberry' not in session:
|
||||
abort(404)
|
||||
page = { 'title': '2-Factor Login' }
|
||||
form = TwoFAForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(email=session['email']).first()
|
||||
del session['email']
|
||||
|
||||
if user is not None and user.verify_totp(form.token.data):
|
||||
print('remember: ' + str(session['memberberry']))
|
||||
login_user(user, session['memberberry'])
|
||||
del session['memberberry']
|
||||
|
||||
if request.headers.getlist("X-Forwarded-For"):
|
||||
lastip = request.headers.getlist("X-Forwarded-For")[0]
|
||||
else:
|
||||
lastip = request.remote_addr
|
||||
user.last_ip = lastip
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
#send_email(current_app.config['MAIL_USERNAME'], user.email + ' logged in.', 'auth/email/adm_loginnotify', user=user, ipaddr=lastip )
|
||||
return redirect(request.args.get('next') or url_for('panel.dashboard'))
|
||||
else:
|
||||
flash('Invalid token.')
|
||||
return render_template('auth/2fa.html', page=page, form=form)
|
||||
|
||||
@auth.route('/qrcode')
|
||||
@login_required
|
||||
def qrcode():
|
||||
#if 'email' not in session:
|
||||
# abort(404)
|
||||
#user = User.query.filter_by(email=session['email']).first()
|
||||
#if user is None:
|
||||
# abort(404)
|
||||
|
||||
# for added security, remove username from session
|
||||
#del session['email']
|
||||
|
||||
# render qrcode for FreeTOTP
|
||||
url = pyqrcode.create(current_user.get_totp_uri())
|
||||
stream = BytesIO()
|
||||
url.svg(stream, scale=6)
|
||||
return stream.getvalue(), 200, {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'}
|
||||
|
||||
@auth.route("/logout", methods=['GET'])
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
flash('You have logged out')
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
@auth.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
#print(current_app.secret_key)
|
||||
page = { 'title': 'Register' }
|
||||
form = RegistrationForm()
|
||||
if form.validate_on_submit():
|
||||
user = User(email=form.email.data, password=form.password.data, wallet=current_app.config['REGISTER_BONUS'])
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
#transaction = Transaction(user_id=int(user.pid), description='Registered account bonus', value=current_app.config['REGISTER_BONUS'])
|
||||
#db.session.add(transaction)
|
||||
#db.session.commit()
|
||||
token = user.generate_confirmation_token()
|
||||
send_email(user.email, 'Потвърдете Вашата регистрация', 'auth/email/confirm', user=user, token=token)
|
||||
#notify admin
|
||||
newip = request.remote_addr
|
||||
if request.headers.getlist("X-Forwarded-For"):
|
||||
newip = request.headers.getlist("X-Forwarded-For")[0]
|
||||
else:
|
||||
newip = request.remote_addr
|
||||
send_email(current_app.config['MAIL_USERNAME'], user.email + ' registered!', 'auth/email/adm_regnotify', user=user, ipaddr=newip )
|
||||
flash('Благодарим за регистрацията! Моля проверете вашият email за потвърждение')
|
||||
return redirect(url_for('auth.login'))
|
||||
return render_template('auth/register.html', page=page, form=form)
|
||||
|
||||
@auth.route('/confirm/<token>')
|
||||
@login_required
|
||||
def confirm(token):
|
||||
if current_user.confirmed:
|
||||
return redirect(url_for('main.index'))
|
||||
if current_user.confirm(token):
|
||||
flash('Вашият акаунт е потвърден. Благодаря!')
|
||||
else:
|
||||
flash('Времето за потвърждение на вашият код изтече.')
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
@auth.route('/confirm')
|
||||
@login_required
|
||||
def resend_confirmation():
|
||||
token = current_user.generate_confirmation_token()
|
||||
send_email(current_user.email, 'Confirm_your_account',
|
||||
'auth/email/confirm', user=current_user, token=token)
|
||||
flash('New confirmation code was sent.')
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
@auth.route('/change-password', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def change_password():
|
||||
form = ChangePasswordForm()
|
||||
if form.validate_on_submit():
|
||||
if current_user.verify_password(form.old_password.data):
|
||||
current_user.password = form.password.data
|
||||
db.session.add(current_user)
|
||||
db.session.commit()
|
||||
flash('Your password was changed')
|
||||
return redirect(url_for('main.index'))
|
||||
else:
|
||||
flash('Wrong password.')
|
||||
return render_template("auth/change_password.html", form=form)
|
||||
|
||||
@auth.route('/reset', methods=['GET', 'POST'])
|
||||
def password_reset_request():
|
||||
if not current_user.is_anonymous:
|
||||
return redirect(url_for('main.index'))
|
||||
form = PasswordResetRequestForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(email=form.email.data).first()
|
||||
if user:
|
||||
token = user.generate_reset_token()
|
||||
send_email(user.email, 'Reset Your Password',
|
||||
'auth/email/reset_password',
|
||||
user=user, token=token,
|
||||
next=request.args.get('next'))
|
||||
flash('An email with instructions to reset your password has been sent to you.')
|
||||
return redirect(url_for('auth.login'))
|
||||
return render_template('auth/reset_password.html', form=form)
|
||||
|
||||
@auth.route('/reset/<token>', methods=['GET', 'POST'])
|
||||
def password_reset(token):
|
||||
if not current_user.is_anonymous:
|
||||
return redirect(url_for('main.index'))
|
||||
form = PasswordResetForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(email=form.email.data).first()
|
||||
if user is None:
|
||||
return redirect(url_for('main.index'))
|
||||
if user.reset_password(token, form.password.data):
|
||||
flash('Your password has been updated.')
|
||||
return redirect(url_for('auth.login'))
|
||||
else:
|
||||
return redirect(url_for('main.index'))
|
||||
return render_template('auth/reset_password.html', form=form)
|
||||
|
25
flask/forest/decorators.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from functools import wraps
|
||||
from flask import abort
|
||||
from flask_login import current_user
|
||||
from .models import Permission
|
||||
from threading import Thread
|
||||
|
||||
def async(f):
|
||||
def wrapper(*args, **kwargs):
|
||||
thr = Thread(target=f, args=args, kwargs=kwargs)
|
||||
thr.start()
|
||||
return wrapper
|
||||
|
||||
def permission_required(permission):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.can(permission):
|
||||
abort(403)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
def admin_required(f):
|
||||
return permission_required(Permission.ADMINISTER)(f)
|
||||
|
24
flask/forest/email.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from threading import Thread
|
||||
from flask import current_app, render_template
|
||||
from flask_mail import Message
|
||||
from . import app, mail
|
||||
from .decorators import async
|
||||
|
||||
@async
|
||||
def send_async_email(app, msg):
|
||||
with app.app_context():
|
||||
mail.send(msg)
|
||||
|
||||
def send_email(to, subject, template, **kwargs):
|
||||
if len(subject) > 50:
|
||||
newsubject = subject[:50] + '...'
|
||||
else:
|
||||
newsubject = subject
|
||||
app = current_app._get_current_object()
|
||||
msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + ' ' + newsubject, sender=app.config['MAIL_SENDER'], recipients=[to])
|
||||
msg.body = render_template(template + '.txt', **kwargs)
|
||||
msg.html = render_template(template + '.html', **kwargs)
|
||||
thr = Thread(target=send_async_email, args=[app, msg])
|
||||
thr.start()
|
||||
return thr
|
||||
|
2
flask/forest/exceptions.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
class ValidationError(ValueError):
|
||||
pass
|
57
flask/forest/init.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
import os
|
||||
|
||||
from werkzeug.utils import secure_filename
|
||||
from flask import (
|
||||
Flask,
|
||||
jsonify,
|
||||
send_from_directory,
|
||||
request,
|
||||
redirect,
|
||||
url_for
|
||||
)
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object("project.config.Config")
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(128), unique=True, nullable=False)
|
||||
active = db.Column(db.Boolean(), default=True, nullable=False)
|
||||
|
||||
def __init__(self, email):
|
||||
self.email = email
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def hello_world():
|
||||
return jsonify(hello="world")
|
||||
|
||||
|
||||
@app.route("/static/<path:filename>")
|
||||
def staticfiles(filename):
|
||||
return send_from_directory(app.config["STATIC_FOLDER"], filename)
|
||||
|
||||
|
||||
@app.route("/media/<path:filename>")
|
||||
def mediafiles(filename):
|
||||
return send_from_directory(app.config["MEDIA_FOLDER"], filename)
|
||||
|
||||
|
||||
@app.route("/upload", methods=["GET", "POST"])
|
||||
def upload_file():
|
||||
if request.method == "POST":
|
||||
file = request.files["file"]
|
||||
filename = secure_filename(file.filename)
|
||||
file.save(os.path.join(app.config["MEDIA_FOLDER"], filename))
|
||||
return """
|
||||
<!doctype html>
|
||||
<title>upload new File</title>
|
||||
<form action="" method=post enctype=multipart/form-data>
|
||||
<p><input type=file name=file><input type=submit value=Upload>
|
||||
</form>
|
||||
"""
|
211
flask/forest/models.py
Normal file
|
@ -0,0 +1,211 @@
|
|||
# FAT MODEL
|
||||
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
|
||||
|
||||
from flask import current_app, request, url_for
|
||||
from flask_login import UserMixin, AnonymousUserMixin
|
||||
from app.exceptions import ValidationError
|
||||
from . import db, lm
|
||||
|
||||
import os
|
||||
import random
|
||||
import uuid
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import date, time, datetime, timedelta
|
||||
import onetimepass
|
||||
|
||||
class Permission:
|
||||
DEPLOY = 0x01
|
||||
ADMINISTER = 0xff
|
||||
|
||||
class Role(db.Model):
|
||||
__tablename__ = 'roles'
|
||||
pid = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String, unique=True)
|
||||
default = db.Column(db.Boolean, default=False, index=True)
|
||||
permissions = db.Column(db.Integer)
|
||||
users = db.relationship('User', backref='role', lazy='dynamic')
|
||||
|
||||
@staticmethod
|
||||
def insert_roles():
|
||||
roles = {
|
||||
'User': (Permission.DEPLOY, True),
|
||||
'Administrator': (Permission.ADMINISTER, False)
|
||||
}
|
||||
for r in roles:
|
||||
role = Role.query.filter_by(name=r).first()
|
||||
if role is None:
|
||||
role = Role(name=r)
|
||||
role.permissions = roles[r][0]
|
||||
role.default = roles[r][1]
|
||||
db.session.add(role)
|
||||
db.session.commit()
|
||||
|
||||
def __repr__(self):
|
||||
return '<Role %r>' % self.name
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(128), unique=True, nullable=False)
|
||||
active = db.Column(db.Boolean(), default=True, nullable=False)
|
||||
confirmed = db.Column(db.Boolean, default=False)
|
||||
|
||||
role_id = db.Column(db.ForeignKey('roles.pid')) #FK
|
||||
password_hash = db.Column(db.String)
|
||||
tokens = db.Column(db.Text)
|
||||
|
||||
member_since = db.Column(db.DateTime(), default=datetime.utcnow)
|
||||
last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
|
||||
last_ip = db.Column(db.String)
|
||||
twofactor = db.Column(db.Boolean, default=False) #optional 2factor auth
|
||||
otp_secret = db.Column(db.String)
|
||||
avatar_hash = db.Column(db.String)
|
||||
uuid = db.Column(db.String)
|
||||
|
||||
name = db.Column(db.Unicode)
|
||||
address = db.Column(db.Unicode)
|
||||
city = db.Column(db.Unicode)
|
||||
postcode = db.Column(db.String)
|
||||
country = db.Column(db.String, default='BG')
|
||||
phone = db.Column(db.String)
|
||||
|
||||
group = db.Column(db.String, default='User')
|
||||
language = db.Column(db.String, default='BG')
|
||||
wallet = db.Column(db.Float)
|
||||
currency = db.Column(db.String, default='BGN')
|
||||
|
||||
inv_items = db.relationship('Item', backref='owner', lazy='dynamic')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(User, self).__init__(**kwargs)
|
||||
|
||||
if self.role is None:
|
||||
if self.email == current_app.config['ADMIN_EMAIL']:
|
||||
#if email match config admin name create admin user
|
||||
self.role = Role.query.filter_by(permissions=0xff).first()
|
||||
if self.role is None:
|
||||
#if role is stil not set, create default user role
|
||||
self.role = Role.query.filter_by(default=True).first()
|
||||
|
||||
if self.avatar_hash is None and self.email is not None:
|
||||
self.avatar_hash = hashlib.md5(self.email.encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
if self.otp_secret is None:
|
||||
# generate a random secret
|
||||
self.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8')
|
||||
|
||||
if self.uuid is None:
|
||||
# generate uuid
|
||||
self.uuid = uuid.uuid4()
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
raise AttributeError('password is not a readable attribute')
|
||||
|
||||
@password.setter
|
||||
def password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def verify_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def get_totp_uri(self):
|
||||
return 'otpauth://totp/DataPanel:{0}?secret={1}&issuer=datapanel'.format(self.email, self.otp_secret)
|
||||
|
||||
def verify_totp(self, token):
|
||||
return onetimepass.valid_totp(token, self.otp_secret)
|
||||
|
||||
def generate_confirmation_token(self, expiration=86400):
|
||||
s = Serializer(current_app.config['SECRET_KEY'], expiration)
|
||||
return s.dumps({'confirm': self.pid})
|
||||
|
||||
def confirm(self, token):
|
||||
s = Serializer(current_app.config['SECRET_KEY'])
|
||||
try:
|
||||
data = s.loads(token)
|
||||
except:
|
||||
return False
|
||||
if data.get('confirm') != self.pid:
|
||||
return False
|
||||
self.confirmed = True
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
def generate_reset_token(self, expiration=86400):
|
||||
s = Serializer(current_app.config['SECRET_KEY'], expiration)
|
||||
return s.dumps({'reset': self.pid})
|
||||
|
||||
def reset_password(self, token, new_password):
|
||||
s = Serializer(current_app.config['SECRET_KEY'])
|
||||
try:
|
||||
data = s.loads(token)
|
||||
except:
|
||||
return False
|
||||
if data.get('reset') != self.pid:
|
||||
return False
|
||||
self.password = new_password
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
def can(self, permissions):
|
||||
return self.role is not None and (self.role.permissions & permissions) == permissions
|
||||
|
||||
def is_administrator(self):
|
||||
if self.can(Permission.ADMINISTER):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def ping(self):
|
||||
self.last_seen = datetime.utcnow()
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
def gravatar(self, size=100, default='identicon', rating='g'):
|
||||
#this check is disabled because it didnt work for me but forcing https to gravatar is okay.
|
||||
#if request.is_secure:
|
||||
# url = 'https://secure.gravatar.com/avatar'
|
||||
#else:
|
||||
# url = 'http://www.gravatar.com/avatar'
|
||||
url = 'https://secure.gravatar.com/avatar'
|
||||
hash = self.avatar_hash or hashlib.md5(self.email.encode('utf-8')).hexdigest()
|
||||
return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(url=url, hash=hash, size=size, default=default, rating=rating)
|
||||
|
||||
def is_authenticated(self):
|
||||
return self.is_authenticated
|
||||
|
||||
def get_id(self):
|
||||
return str(self.pid)
|
||||
|
||||
def __repr__(self):
|
||||
return '<User %r>' % self.email
|
||||
|
||||
class AnonymousUser(AnonymousUserMixin):
|
||||
def can(self, permissions):
|
||||
return False
|
||||
|
||||
def is_administrator(self):
|
||||
return False
|
||||
|
||||
lm.anonymous_user = AnonymousUser
|
||||
|
||||
@lm.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
#ITEMS
|
||||
class Item(db.Model):
|
||||
__tablename__ = 'items'
|
||||
id = db.Column(db.Integer, primary_key=True) #PK
|
||||
user_id = db.Column(db.ForeignKey('users.id')) #FK
|
||||
date_created = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
description = db.Column(db.Unicode)
|
3
flask/forest/panel/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from flask import Blueprint
|
||||
panel = Blueprint('panel', __name__)
|
||||
from . import routes
|
27
flask/forest/panel/forms.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectField, DecimalField
|
||||
from flask_pagedown.fields import PageDownField
|
||||
from wtforms import validators, ValidationError
|
||||
from wtforms.fields.html5 import EmailField, DecimalRangeField
|
||||
|
||||
from .. import db
|
||||
|
||||
class OrderForm(FlaskForm):
|
||||
region_choices = [(1, 'Plovdiv, Bulgaria'), (2, 'International Space Station')]
|
||||
region = SelectField('Region:', choices=region_choices, coerce=int)
|
||||
|
||||
recipe_choices = [(1, 'RootVPS')]
|
||||
recipe = SelectField('Type:', choices=recipe_choices, coerce=int)
|
||||
|
||||
cpu = DecimalRangeField('Processor Cores', default=2)
|
||||
memory = DecimalRangeField('Memory', default=2048)
|
||||
storage = DecimalRangeField('Storage', default=20)
|
||||
|
||||
alias = StringField('Name:', [validators.Regexp(message='ex.: myservice1.com, myservice2.local', regex='^[a-zA-Z0-9][a-zA-Z0-9-_]{0,61}[a-zA-Z0-9]{0,1}\.([a-zA-Z]{1,6}|[a-zA-Z0-9-]{1,30}\.[a-zA-Z]{2,3})$'), validators.Length(6,64)])
|
||||
|
||||
submit = SubmitField('DEPLOY')
|
||||
|
||||
class MessageForm(FlaskForm):
|
||||
line = PageDownField('Enter your message...', validators=[validators.DataRequired()])
|
||||
submit = SubmitField('Submit')
|
||||
|
165
flask/forest/panel/routes.py
Normal file
|
@ -0,0 +1,165 @@
|
|||
from flask import render_template, abort, redirect, url_for, abort, flash, request, current_app, make_response, g
|
||||
from flask_login import login_required, login_user, logout_user, current_user
|
||||
from flask_sqlalchemy import get_debug_queries
|
||||
|
||||
from . import panel
|
||||
from .forms import OrderForm, MessageForm
|
||||
from .. import db
|
||||
from ..email import send_email
|
||||
from ..models import User, Permission, Recipe, Order, Server, Deployment, Service, Region, Address, Domain, SupportTopic, SupportLine, contact_proxmaster
|
||||
|
||||
import base64
|
||||
from datetime import date, time, datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
@panel.after_app_request
|
||||
def after_request(response):
|
||||
for query in get_debug_queries():
|
||||
if query.duration >= current_app.config['SLOW_DB_QUERY_TIME']:
|
||||
current_app.logger.warning('Slow query: %s\nParameters: %s\nDuration: %fs\nContext: %s\n' % (query.statement, query.parameters, query.duration, query.context))
|
||||
return response
|
||||
|
||||
@panel.route("/deploy", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def deploy():
|
||||
if current_user.name is None:
|
||||
return redirect(url_for('settings.profile'))
|
||||
|
||||
form = OrderForm()
|
||||
if form.validate_on_submit():
|
||||
region = Region.query.filter_by(pid=int(form.region.data)).first()
|
||||
recipe = Recipe.query.filter_by(pid=int(form.recipe.data)).first()
|
||||
new_order = Order(user_id=int(current_user.pid), region_id=int(region.pid), recipe_id=int(recipe.pid), parameter1=str(form.alias.data), parameter2=str(form.cpu.data), parameter3=str(form.memory.data), parameter4=str(form.storage.data), status='new')
|
||||
db.session.add(new_order)
|
||||
db.session.commit()
|
||||
send_email(current_app.config['MAIL_USERNAME'], 'New order from {}'.format(current_user.email),
|
||||
'panel/email/adm_neworder', user=current_user)
|
||||
return redirect(request.args.get('next') or url_for('panel.dashboard'))
|
||||
|
||||
return render_template('panel/deploy.html', form=form)
|
||||
|
||||
#DASHBOARD
|
||||
@panel.route("/dashboard", defaults={'user_pid': 0}, methods=['GET'])
|
||||
@panel.route("/dashboard/<int:user_pid>", methods=['GET'])
|
||||
@login_required
|
||||
def dashboard(user_pid):
|
||||
sys_regions = Region.query.all()
|
||||
|
||||
if user_pid == 0:
|
||||
cuser = current_user
|
||||
else:
|
||||
cuser = User.query.filter_by(pid=user_pid).first()
|
||||
if cuser == None:
|
||||
abort(404)
|
||||
if not current_user.is_administrator():
|
||||
abort(404) #hidden 403
|
||||
|
||||
inv_addresses = cuser.inv_addresses.order_by(Address.ip.asc()).all()
|
||||
inv_deployments = cuser.inv_deployments.filter_by(deleted=False).order_by(Deployment.machine_alias.asc()).all()
|
||||
regions = {}
|
||||
for region in sys_regions:
|
||||
regions[region.pid] = region.description
|
||||
|
||||
inv_deploycubeids = []
|
||||
warnflag = False
|
||||
for invcls in inv_deployments:
|
||||
if invcls.user_id == cuser.pid:
|
||||
inv_deploycubeids.extend([invcls.machine_id])
|
||||
#warning detector
|
||||
if invcls.warning == True or invcls.enabled == False:
|
||||
warnflag = True
|
||||
|
||||
inv_services = cuser.inv_services.filter_by(deleted=False).order_by(Service.date_last_charge.asc()).all()
|
||||
inv_domains = cuser.inv_domains.filter_by(deleted=False).order_by(Domain.date_created.desc()).all()
|
||||
|
||||
#extract rrd and status from the deployments
|
||||
rrd = {}
|
||||
statuses = {}
|
||||
#current_app.logger.warning(str(inv_deploycubeids))
|
||||
for unit_id in inv_deploycubeids:
|
||||
data = { 'unit_id': int(unit_id),
|
||||
'type': 'kvm' }
|
||||
try:
|
||||
query = contact_proxmaster(data, 'vmrrd')
|
||||
graphs_list = ['net', 'cpu', 'mem', 'hdd']
|
||||
rrd[unit_id] = {}
|
||||
for graph in graphs_list:
|
||||
raw = query[graph]['image'].encode('raw_unicode_escape')
|
||||
rrd[unit_id][graph] = base64.b64encode(raw).decode()
|
||||
status = { unit_id : query['status'] }
|
||||
statuses.update(status)
|
||||
except Exception as e:
|
||||
current_app.logger.error(e)
|
||||
for invcls in inv_deployments:
|
||||
if invcls.machine_id == unit_id:
|
||||
inv_deployments.remove(invcls)
|
||||
flash('Support is notified about {}.'.format(str(cuser.inv_deployments.filter_by(machine_id=unit_id).first().machine_alias)))
|
||||
if not current_user.is_administrator():
|
||||
send_email(current_app.config['MAIL_USERNAME'], '{} experienced an error'.format(cuser.email), 'vmanager/email/adm_unreachable', user=current_user, unit_id=unit_id, error=repr(e))
|
||||
continue
|
||||
supportform = MessageForm()
|
||||
return render_template('panel/dashboard.html', sys_regions=sys_regions, inv_deployments=inv_deployments, inv_services=inv_services, inv_domains=inv_domains, inv_addresses=inv_addresses, rrd=rrd, status=statuses, warnflag=warnflag, regions=regions, form=supportform)
|
||||
|
||||
|
||||
#SUPPORT
|
||||
@panel.route("/list", methods=['GET'])
|
||||
@login_required
|
||||
def support_list():
|
||||
""" general enquiry and list all open support tasks """
|
||||
cuser = current_user
|
||||
form = MessageForm()
|
||||
|
||||
alltopics = cuser.inv_topics.all()
|
||||
return render_template('panel/support_list.html', form=form, inv_topics=alltopics)
|
||||
|
||||
@panel.route("/topic/<string:topic>/", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def support(topic):
|
||||
""" block item for support chatbox. invoked from vdc_pool or supportlist """
|
||||
cuser = current_user
|
||||
form = MessageForm()
|
||||
|
||||
if request.method == "GET":
|
||||
support_topic = SupportTopic.query.filter_by(hashtag=str(topic)).first()
|
||||
if support_topic == None:
|
||||
class EmptySupport():
|
||||
hashtag=str(topic)
|
||||
timestamp=datetime.utcnow()
|
||||
support_topic = EmptySupport()
|
||||
return render_template('panel/support_item.html', form=form, support=support_topic)
|
||||
else:
|
||||
if support_topic.user_id != cuser.pid:
|
||||
abort(403) #TODO: hidden 403. there is a topic like that but its not yours!
|
||||
else:
|
||||
#topic is yours. show it.
|
||||
return render_template('panel/support_item.html', form=form, support=support_topic)
|
||||
|
||||
if request.method == "POST" and form.validate_on_submit():
|
||||
support_topic = SupportTopic.query.filter_by(hashtag=str(topic)).first()
|
||||
if support_topic == None:
|
||||
#no topic. create one?
|
||||
if cuser.inv_topics.all() != []:
|
||||
#check if other topics exist, and ratelimit
|
||||
last_topic = cuser.inv_topics.order_by(SupportTopic.timestamp.desc()).first()
|
||||
now = datetime.utcnow()
|
||||
time_last_topic = last_topic.timestamp
|
||||
expiry = time_last_topic + relativedelta(time_last_topic, minutes=+5)
|
||||
if now < expiry:
|
||||
flash('ratelimit. try again later')
|
||||
return redirect(url_for('panel.support_list'))
|
||||
#create new topic
|
||||
new_topic = SupportTopic(user_id=cuser.pid, hashtag=str(topic))
|
||||
db.session.add(new_topic)
|
||||
new_line = SupportLine(topic_id=new_topic.pid, line=str(form.line.data))
|
||||
db.session.add(new_line)
|
||||
|
||||
else:
|
||||
if support_topic.user_id == cuser.pid:
|
||||
new_line = SupportLine(topic_id=support_topic.pid, line=form.line.data)
|
||||
db.session.add(new_line)
|
||||
else:
|
||||
abort(403) #TODO: hidden 404
|
||||
|
||||
db.session.commit()
|
||||
return redirect(url_for('panel.support_list'))
|
||||
|
3
flask/forest/settings/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from flask import Blueprint
|
||||
settings = Blueprint('settings', __name__)
|
||||
from . import routes
|
66
flask/forest/settings/forms.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
from iso3166 import countries
|
||||
import string
|
||||
import random
|
||||
from ..models import User, Role
|
||||
|
||||
from flask_wtf import FlaskForm, RecaptchaField
|
||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectField, DecimalField
|
||||
from wtforms import validators, ValidationError
|
||||
from wtforms.fields.html5 import EmailField
|
||||
|
||||
class EditProfileForm(FlaskForm):
|
||||
name = StringField('Name:', [validators.DataRequired(), validators.Length(3, 60)])
|
||||
address = StringField('Address:', [validators.DataRequired(), validators.Length(2, 50)])
|
||||
city = StringField('City:', [validators.DataRequired(), validators.Length(2,40)])
|
||||
|
||||
postcode = StringField('Postcode:')
|
||||
|
||||
clist = []
|
||||
for c in countries:
|
||||
clist.append((c.alpha2, c.name))
|
||||
country = SelectField('Country:', choices=clist, default='BG')
|
||||
|
||||
phone = StringField('Phone:')
|
||||
org_account = BooleanField('This is a business account.')
|
||||
org_companyname = StringField('Company Name:')
|
||||
org_regaddress = StringField('Company Address:')
|
||||
org_responsible = StringField('Accountable Person:')
|
||||
org_vatnum = StringField('VAT Number:')
|
||||
twofactor = BooleanField('Enable 2-factor authentication')
|
||||
submit = SubmitField('Update')
|
||||
|
||||
class EditProfileAdminForm(FlaskForm):
|
||||
email = StringField('Електроннa поща (логин):', [validators.DataRequired(), validators.Length(1, 64), validators.Email()])
|
||||
confirmed = BooleanField('Activated')
|
||||
role = SelectField('Role', coerce=int)
|
||||
|
||||
name = StringField('Лице за контакт:', [validators.DataRequired(), validators.Length(3, 60)])
|
||||
address = StringField('Адрес:', [validators.DataRequired(), validators.Length(2, 50)])
|
||||
city = StringField('Град:', [validators.DataRequired(), validators.Length(2,40)])
|
||||
postcode = DecimalField('Пощенски Код:')
|
||||
|
||||
clist = []
|
||||
for c in countries:
|
||||
clist.append((c.alpha2, c.name))
|
||||
country = SelectField('Държава:', choices=clist)
|
||||
|
||||
phone = DecimalField('Телефон:', [validators.DataRequired()])
|
||||
org_account = BooleanField('This is a business account')
|
||||
org_companyname = StringField('Company Name:')
|
||||
org_regaddress = StringField('Company Address:')
|
||||
org_responsible = StringField('Primary Contact:')
|
||||
org_vatnum = StringField('ДДС Номер:')
|
||||
twofactor = BooleanField('2-factor authentication')
|
||||
submit = SubmitField('Обнови')
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super(EditProfileAdminForm, self).__init__(*args, **kwargs)
|
||||
self.role.choices = [(role.pid, role.name)
|
||||
for role in Role.query.order_by(Role.name).all()]
|
||||
self.user = user
|
||||
|
||||
def validate_email(self, field):
|
||||
if field.data != self.user.email and User.query.filter_by(email=field.data).first():
|
||||
raise ValidationError('Email-а е вече регистриран.')
|
||||
|
||||
|
53
flask/forest/settings/routes.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
from flask import render_template, redirect, request, url_for, flash, session, abort, current_app
|
||||
from flask_login import login_required, login_user, logout_user, current_user
|
||||
from sqlalchemy import desc
|
||||
|
||||
from . import settings
|
||||
from .forms import EditProfileForm, EditProfileAdminForm
|
||||
|
||||
from ..email import send_email
|
||||
from .. import db
|
||||
from ..models import User
|
||||
import sys
|
||||
|
||||
#PROFILE
|
||||
@settings.route('/profile', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def profile():
|
||||
page = { 'title': 'Edit Profile' }
|
||||
form = EditProfileForm()
|
||||
if form.validate_on_submit():
|
||||
current_user.name = form.name.data
|
||||
current_user.address = form.address.data
|
||||
current_user.city = form.city.data
|
||||
current_user.postcode = form.postcode.data
|
||||
current_user.country = form.country.data
|
||||
current_user.phone = form.phone.data
|
||||
current_user.org_account = form.org_account.data
|
||||
current_user.org_companyname = form.org_companyname.data
|
||||
current_user.org_regaddress = form.org_regaddress.data
|
||||
current_user.org_responsible = form.org_responsible.data
|
||||
current_user.org_vatnum = form.org_vatnum.data
|
||||
current_user.twofactor = form.twofactor.data
|
||||
db.session.add(current_user)
|
||||
db.session.commit()
|
||||
flash('Info Updated!')
|
||||
|
||||
form.name.data = current_user.name
|
||||
form.address.data = current_user.address
|
||||
form.city.data = current_user.city
|
||||
form.postcode.data = current_user.postcode
|
||||
form.country.data = current_user.country
|
||||
form.phone.data = current_user.phone
|
||||
form.org_account.data = current_user.org_account
|
||||
form.org_companyname.data = current_user.org_companyname
|
||||
form.org_regaddress.data = current_user.org_regaddress
|
||||
form.org_responsible.data = current_user.org_responsible
|
||||
form.org_vatnum.data = current_user.org_vatnum
|
||||
form.twofactor.data = current_user.twofactor
|
||||
|
||||
wallet = "%.2f" % round(current_user.wallet, 3)
|
||||
#current_app.logger.info('[{}] wallet: {}'.format(current_user.email, wallet))
|
||||
|
||||
return render_template('settings/profile.html', page=page, form=form, wallet=wallet, cuser=current_user)
|
||||
|
277
flask/forest/static/css/bootstrap-slider.css
vendored
Normal file
|
@ -0,0 +1,277 @@
|
|||
/*! =======================================================
|
||||
VERSION 9.4.1
|
||||
========================================================= */
|
||||
/*! =========================================================
|
||||
* bootstrap-slider.js
|
||||
*
|
||||
* Maintainers:
|
||||
* Kyle Kemp
|
||||
* - Twitter: @seiyria
|
||||
* - Github: seiyria
|
||||
* Rohit Kalkur
|
||||
* - Twitter: @Rovolutionary
|
||||
* - Github: rovolution
|
||||
*
|
||||
* =========================================================
|
||||
*
|
||||
* bootstrap-slider is released under the MIT License
|
||||
* Copyright (c) 2016 Kyle Kemp, Rohit Kalkur, and contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* ========================================================= */
|
||||
.slider {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
}
|
||||
.slider.slider-horizontal {
|
||||
width: 210px;
|
||||
height: 20px;
|
||||
}
|
||||
.slider.slider-horizontal .slider-track {
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
margin-top: -5px;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
}
|
||||
.slider.slider-horizontal .slider-selection,
|
||||
.slider.slider-horizontal .slider-track-low,
|
||||
.slider.slider-horizontal .slider-track-high {
|
||||
height: 100%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.slider.slider-horizontal .slider-tick,
|
||||
.slider.slider-horizontal .slider-handle {
|
||||
margin-left: -10px;
|
||||
}
|
||||
.slider.slider-horizontal .slider-tick.triangle,
|
||||
.slider.slider-horizontal .slider-handle.triangle {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-width: 0 10px 10px 10px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-bottom-color: #0480be;
|
||||
margin-top: 0;
|
||||
}
|
||||
.slider.slider-horizontal .slider-tick-container {
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.slider.slider-horizontal .slider-tick-label-container {
|
||||
white-space: nowrap;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.slider.slider-horizontal .slider-tick-label-container .slider-tick-label {
|
||||
padding-top: 4px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
.slider.slider-vertical {
|
||||
height: 210px;
|
||||
width: 20px;
|
||||
}
|
||||
.slider.slider-vertical .slider-track {
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
left: 25%;
|
||||
top: 0;
|
||||
}
|
||||
.slider.slider-vertical .slider-selection {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.slider.slider-vertical .slider-track-low,
|
||||
.slider.slider-vertical .slider-track-high {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.slider.slider-vertical .slider-tick,
|
||||
.slider.slider-vertical .slider-handle {
|
||||
margin-top: -10px;
|
||||
}
|
||||
.slider.slider-vertical .slider-tick.triangle,
|
||||
.slider.slider-vertical .slider-handle.triangle {
|
||||
border-width: 10px 0 10px 10px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
border-left-color: #0480be;
|
||||
margin-left: 0;
|
||||
}
|
||||
.slider.slider-vertical .slider-tick-label-container {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.slider.slider-vertical .slider-tick-label-container .slider-tick-label {
|
||||
padding-left: 4px;
|
||||
}
|
||||
.slider.slider-disabled .slider-handle {
|
||||
background-image: -webkit-linear-gradient(top, #dfdfdf 0%, #bebebe 100%);
|
||||
background-image: -o-linear-gradient(top, #dfdfdf 0%, #bebebe 100%);
|
||||
background-image: linear-gradient(to bottom, #dfdfdf 0%, #bebebe 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdfdfdf', endColorstr='#ffbebebe', GradientType=0);
|
||||
}
|
||||
.slider.slider-disabled .slider-track {
|
||||
background-image: -webkit-linear-gradient(top, #e5e5e5 0%, #e9e9e9 100%);
|
||||
background-image: -o-linear-gradient(top, #e5e5e5 0%, #e9e9e9 100%);
|
||||
background-image: linear-gradient(to bottom, #e5e5e5 0%, #e9e9e9 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5e5e5', endColorstr='#ffe9e9e9', GradientType=0);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.slider input {
|
||||
display: none;
|
||||
}
|
||||
.slider .tooltip.top {
|
||||
margin-top: -36px;
|
||||
}
|
||||
.slider .tooltip-inner {
|
||||
white-space: nowrap;
|
||||
max-width: none;
|
||||
}
|
||||
.slider .hide {
|
||||
display: none;
|
||||
}
|
||||
.slider-track {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #f9f9f9 100%);
|
||||
background-image: -o-linear-gradient(top, #f5f5f5 0%, #f9f9f9 100%);
|
||||
background-image: linear-gradient(to bottom, #f5f5f5 0%, #f9f9f9 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);
|
||||
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.slider-selection {
|
||||
position: absolute;
|
||||
background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-image: -o-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-image: linear-gradient(to bottom, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
|
||||
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.slider-selection.tick-slider-selection {
|
||||
background-image: -webkit-linear-gradient(top, #89cdef 0%, #81bfde 100%);
|
||||
background-image: -o-linear-gradient(top, #89cdef 0%, #81bfde 100%);
|
||||
background-image: linear-gradient(to bottom, #89cdef 0%, #81bfde 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef', endColorstr='#ff81bfde', GradientType=0);
|
||||
}
|
||||
.slider-track-low,
|
||||
.slider-track-high {
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.slider-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #337ab7;
|
||||
background-image: -webkit-linear-gradient(top, #149bdf 0%, #0480be 100%);
|
||||
background-image: -o-linear-gradient(top, #149bdf 0%, #0480be 100%);
|
||||
background-image: linear-gradient(to bottom, #149bdf 0%, #0480be 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);
|
||||
filter: none;
|
||||
-webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
.slider-handle.round {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.slider-handle.triangle {
|
||||
background: transparent none;
|
||||
}
|
||||
.slider-handle.custom {
|
||||
background: transparent none;
|
||||
}
|
||||
.slider-handle.custom::before {
|
||||
line-height: 20px;
|
||||
font-size: 20px;
|
||||
content: '\2605';
|
||||
color: #726204;
|
||||
}
|
||||
.slider-tick {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-image: -o-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-image: linear-gradient(to bottom, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
|
||||
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
filter: none;
|
||||
opacity: 0.8;
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
.slider-tick.round {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.slider-tick.triangle {
|
||||
background: transparent none;
|
||||
}
|
||||
.slider-tick.custom {
|
||||
background: transparent none;
|
||||
}
|
||||
.slider-tick.custom::before {
|
||||
line-height: 20px;
|
||||
font-size: 20px;
|
||||
content: '\2605';
|
||||
color: #726204;
|
||||
}
|
||||
.slider-tick.in-selection {
|
||||
background-image: -webkit-linear-gradient(top, #89cdef 0%, #81bfde 100%);
|
||||
background-image: -o-linear-gradient(top, #89cdef 0%, #81bfde 100%);
|
||||
background-image: linear-gradient(to bottom, #89cdef 0%, #81bfde 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef', endColorstr='#ff81bfde', GradientType=0);
|
||||
opacity: 1;
|
||||
}
|
57
flask/forest/static/css/login.css
Normal file
|
@ -0,0 +1,57 @@
|
|||
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
|
||||
|
||||
.login-page {
|
||||
width: 360px;
|
||||
padding: 8% 0 0;
|
||||
margin: auto;
|
||||
}
|
||||
.form {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: #FFFFFF;
|
||||
max-width: 360px;
|
||||
margin: 0 auto 100px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
.form input {
|
||||
font-family: "Roboto", sans-serif;
|
||||
outline: 0;
|
||||
background: #f2f2f2;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 0 0 15px;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
.form button {
|
||||
font-family: "Roboto", sans-serif;
|
||||
text-transform: uppercase;
|
||||
outline: 0;
|
||||
background: #4CAF50;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 15px;
|
||||
color: #FFFFFF;
|
||||
font-size: 16px;
|
||||
-webkit-transition: all 0.3 ease;
|
||||
transition: all 0.3 ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.form button:hover,.form button:active,.form button:focus {
|
||||
background: #43A047;
|
||||
}
|
||||
.form .message {
|
||||
margin: 15px 0 0;
|
||||
color: #b3b3b3;
|
||||
font-size: 12px;
|
||||
}
|
||||
.form .message a {
|
||||
color: #4CAF50;
|
||||
text-decoration: none;
|
||||
}
|
||||
.form .register-form {
|
||||
display: none;
|
||||
}
|
90
flask/forest/static/css/login.css-dist
Normal file
|
@ -0,0 +1,90 @@
|
|||
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
|
||||
|
||||
.login-page {
|
||||
width: 360px;
|
||||
padding: 8% 0 0;
|
||||
margin: auto;
|
||||
}
|
||||
.form {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: #FFFFFF;
|
||||
max-width: 360px;
|
||||
margin: 0 auto 100px;
|
||||
padding: 45px;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
.form input {
|
||||
font-family: "Roboto", sans-serif;
|
||||
outline: 0;
|
||||
background: #f2f2f2;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 0 0 15px;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
.form button {
|
||||
font-family: "Roboto", sans-serif;
|
||||
text-transform: uppercase;
|
||||
outline: 0;
|
||||
background: #4CAF50;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 15px;
|
||||
color: #FFFFFF;
|
||||
font-size: 14px;
|
||||
-webkit-transition: all 0.3 ease;
|
||||
transition: all 0.3 ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.form button:hover,.form button:active,.form button:focus {
|
||||
background: #43A047;
|
||||
}
|
||||
.form .message {
|
||||
margin: 15px 0 0;
|
||||
color: #b3b3b3;
|
||||
font-size: 12px;
|
||||
}
|
||||
.form .message a {
|
||||
color: #4CAF50;
|
||||
text-decoration: none;
|
||||
}
|
||||
.form .register-form {
|
||||
display: none;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.container:before, .container:after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
.container .info {
|
||||
margin: 50px auto;
|
||||
text-align: center;
|
||||
}
|
||||
.container .info h1 {
|
||||
margin: 0 0 15px;
|
||||
padding: 0;
|
||||
font-size: 36px;
|
||||
font-weight: 300;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.container .info span {
|
||||
color: #4d4d4d;
|
||||
font-size: 12px;
|
||||
}
|
||||
.container .info span a {
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
}
|
||||
.container .info span .fa {
|
||||
color: #EF3B3A;
|
||||
}
|
94
flask/forest/static/css/navbar.css
Normal file
|
@ -0,0 +1,94 @@
|
|||
.navbar-default {
|
||||
background-color: #708d3f;
|
||||
border-color: #a36123;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-brand {
|
||||
background: url(/static/images/datapoint.png) center / contain no-repeat;
|
||||
width: 180px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.navbar-default .navbar-brand:hover,
|
||||
.navbar-default .navbar-brand:focus {
|
||||
color: #ffffff;
|
||||
}
|
||||
.navbar-default .navbar-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
.navbar-default .navbar-nav > li > a {
|
||||
color: #ffffff;
|
||||
}
|
||||
.navbar-default .navbar-nav > li > a:hover,
|
||||
.navbar-default .navbar-nav > li > a:focus {
|
||||
color: #ffffff;
|
||||
background-color: #a36123;
|
||||
}
|
||||
.navbar-default .navbar-nav > li > .dropdown-menu {
|
||||
background-color: #708d3f;
|
||||
}
|
||||
.navbar-default .navbar-nav > li > .dropdown-menu > li > a {
|
||||
color: #ffffff;
|
||||
}
|
||||
.navbar-default .navbar-nav > li > .dropdown-menu > li > a:hover,
|
||||
.navbar-default .navbar-nav > li > .dropdown-menu > li > a:focus {
|
||||
color: #ffffff;
|
||||
background-color: #a36123;
|
||||
}
|
||||
.navbar-default .navbar-nav > li > .dropdown-menu > li > .divider {
|
||||
background-color: #a36123;
|
||||
}
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > .active > a,
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
|
||||
color: #ffffff;
|
||||
background-color: #a36123;
|
||||
}
|
||||
.navbar-default .navbar-nav > .active > a,
|
||||
.navbar-default .navbar-nav > .active > a:hover,
|
||||
.navbar-default .navbar-nav > .active > a:focus {
|
||||
color: #ffffff;
|
||||
background-color: #a36123;
|
||||
}
|
||||
.navbar-default .navbar-nav > .open > a,
|
||||
.navbar-default .navbar-nav > .open > a:hover,
|
||||
.navbar-default .navbar-nav > .open > a:focus {
|
||||
color: #ffffff;
|
||||
background-color: #a36123;
|
||||
}
|
||||
.navbar-default .navbar-toggle {
|
||||
border-color: #a36123;
|
||||
}
|
||||
.navbar-default .navbar-toggle:hover,
|
||||
.navbar-default .navbar-toggle:focus {
|
||||
background-color: #a36123;
|
||||
}
|
||||
.navbar-default .navbar-toggle .icon-bar {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.navbar-default .navbar-collapse,
|
||||
.navbar-default .navbar-form {
|
||||
border-color: #ffffff;
|
||||
}
|
||||
.navbar-default .navbar-link {
|
||||
color: #ffffff;
|
||||
}
|
||||
.navbar-default .navbar-link:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > li > a {
|
||||
color: #ffffff;
|
||||
}
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {
|
||||
color: #ffffff;
|
||||
}
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > .active > a,
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
|
||||
color: #ffffff;
|
||||
background-color: #a36123;
|
||||
}
|
||||
}
|
53
flask/forest/static/css/no-more-tables.css
Normal file
|
@ -0,0 +1,53 @@
|
|||
.tg {border-collapse:collapse;border-spacing:0;}
|
||||
.tg td{font-family:Arial, sans-serif;font-size:14px;padding:1px 1px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;}
|
||||
.tg th{font-family:Arial, sans-serif;font-size:14px;padding:1px 1px;font-weight:normal;padding:border-style:solid;border-width:1px;overflow:hidden;word-break:normal;}
|
||||
.tg .tg-yw4l{vertical-align:top}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
/* Force table to not be like tables anymore */
|
||||
.no-more-tables table,
|
||||
.no-more-tables thead,
|
||||
.no-more-tables tbody,
|
||||
.no-more-tables th,
|
||||
.no-more-tables td,
|
||||
.no-more-tables tr {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Hide table headers (but not display: none;, for accessibility) */
|
||||
.no-more-tables thead tr {
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
}
|
||||
|
||||
.no-more-tables tr { border: 1px solid #ccc; }
|
||||
|
||||
.no-more-tables td {
|
||||
/* Behave like a "row" */
|
||||
border: none;
|
||||
border-bottom: 1px solid #eee;
|
||||
position: relative;
|
||||
padding-left: 50%;
|
||||
white-space: normal;
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
.no-more-tables td:before {
|
||||
/* Now like a table header */
|
||||
/* position: absolute; */
|
||||
/* Top/left values mimic padding */
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
width: 45%;
|
||||
padding-right: 10px;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/*
|
||||
Label the data
|
||||
*/
|
||||
.no-more-tables td:before { content: attr(data-title); }
|
||||
}
|
278
flask/forest/static/css/nouislider.css
Normal file
|
@ -0,0 +1,278 @@
|
|||
/*! nouislider - 9.1.0 - 2016-12-10 16:00:32 */
|
||||
|
||||
|
||||
/* Functional styling;
|
||||
* These styles are required for noUiSlider to function.
|
||||
* You don't need to change these rules to apply your design.
|
||||
*/
|
||||
.noUi-target,
|
||||
.noUi-target * {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
-webkit-user-select: none;
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
-ms-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.noUi-target {
|
||||
position: relative;
|
||||
direction: ltr;
|
||||
}
|
||||
.noUi-base {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1; /* Fix 401 */
|
||||
}
|
||||
.noUi-connect {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.noUi-origin {
|
||||
position: absolute;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
.noUi-handle {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.noUi-state-tap .noUi-connect,
|
||||
.noUi-state-tap .noUi-origin {
|
||||
-webkit-transition: top 0.3s, right 0.3s, bottom 0.3s, left 0.3s;
|
||||
transition: top 0.3s, right 0.3s, bottom 0.3s, left 0.3s;
|
||||
}
|
||||
.noUi-state-drag * {
|
||||
cursor: inherit !important;
|
||||
}
|
||||
|
||||
/* Painting and performance;
|
||||
* Browsers can paint handles in their own layer.
|
||||
*/
|
||||
.noUi-base,
|
||||
.noUi-handle {
|
||||
-webkit-transform: translate3d(0,0,0);
|
||||
transform: translate3d(0,0,0);
|
||||
}
|
||||
|
||||
/* Slider size and handle placement;
|
||||
*/
|
||||
.noUi-horizontal {
|
||||
height: 18px;
|
||||
}
|
||||
.noUi-horizontal .noUi-handle {
|
||||
width: 34px;
|
||||
height: 28px;
|
||||
left: -17px;
|
||||
top: -6px;
|
||||
}
|
||||
.noUi-vertical {
|
||||
width: 18px;
|
||||
}
|
||||
.noUi-vertical .noUi-handle {
|
||||
width: 28px;
|
||||
height: 34px;
|
||||
left: -6px;
|
||||
top: -17px;
|
||||
}
|
||||
|
||||
/* Styling;
|
||||
*/
|
||||
.noUi-target {
|
||||
background: #FAFAFA;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #D3D3D3;
|
||||
box-shadow: inset 0 1px 1px #F0F0F0, 0 3px 6px -5px #BBB;
|
||||
}
|
||||
.noUi-connect {
|
||||
background: #3FB8AF;
|
||||
box-shadow: inset 0 0 3px rgba(51,51,51,0.45);
|
||||
-webkit-transition: background 450ms;
|
||||
transition: background 450ms;
|
||||
}
|
||||
|
||||
/* Handles and cursors;
|
||||
*/
|
||||
.noUi-draggable {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.noUi-vertical .noUi-draggable {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
.noUi-handle {
|
||||
border: 1px solid #D9D9D9;
|
||||
border-radius: 3px;
|
||||
background: #FFF;
|
||||
cursor: default;
|
||||
box-shadow: inset 0 0 1px #FFF,
|
||||
inset 0 1px 7px #EBEBEB,
|
||||
0 3px 6px -3px #BBB;
|
||||
}
|
||||
.noUi-active {
|
||||
box-shadow: inset 0 0 1px #FFF,
|
||||
inset 0 1px 7px #DDD,
|
||||
0 3px 6px -3px #BBB;
|
||||
}
|
||||
|
||||
/* Handle stripes;
|
||||
*/
|
||||
.noUi-handle:before,
|
||||
.noUi-handle:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 14px;
|
||||
width: 1px;
|
||||
background: #E8E7E6;
|
||||
left: 14px;
|
||||
top: 6px;
|
||||
}
|
||||
.noUi-handle:after {
|
||||
left: 17px;
|
||||
}
|
||||
.noUi-vertical .noUi-handle:before,
|
||||
.noUi-vertical .noUi-handle:after {
|
||||
width: 14px;
|
||||
height: 1px;
|
||||
left: 6px;
|
||||
top: 14px;
|
||||
}
|
||||
.noUi-vertical .noUi-handle:after {
|
||||
top: 17px;
|
||||
}
|
||||
|
||||
/* Disabled state;
|
||||
*/
|
||||
|
||||
[disabled] .noUi-connect {
|
||||
background: #B8B8B8;
|
||||
}
|
||||
[disabled].noUi-target,
|
||||
[disabled].noUi-handle,
|
||||
[disabled] .noUi-handle {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
/* Base;
|
||||
*
|
||||
*/
|
||||
.noUi-pips,
|
||||
.noUi-pips * {
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.noUi-pips {
|
||||
position: absolute;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Values;
|
||||
*
|
||||
*/
|
||||
.noUi-value {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
}
|
||||
.noUi-value-sub {
|
||||
color: #ccc;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Markings;
|
||||
*
|
||||
*/
|
||||
.noUi-marker {
|
||||
position: absolute;
|
||||
background: #CCC;
|
||||
}
|
||||
.noUi-marker-sub {
|
||||
background: #AAA;
|
||||
}
|
||||
.noUi-marker-large {
|
||||
background: #AAA;
|
||||
}
|
||||
|
||||
/* Horizontal layout;
|
||||
*
|
||||
*/
|
||||
.noUi-pips-horizontal {
|
||||
padding: 10px 0;
|
||||
height: 80px;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.noUi-value-horizontal {
|
||||
-webkit-transform: translate3d(-50%,50%,0);
|
||||
transform: translate3d(-50%,50%,0);
|
||||
}
|
||||
|
||||
.noUi-marker-horizontal.noUi-marker {
|
||||
margin-left: -1px;
|
||||
width: 2px;
|
||||
height: 5px;
|
||||
}
|
||||
.noUi-marker-horizontal.noUi-marker-sub {
|
||||
height: 10px;
|
||||
}
|
||||
.noUi-marker-horizontal.noUi-marker-large {
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
/* Vertical layout;
|
||||
*
|
||||
*/
|
||||
.noUi-pips-vertical {
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
}
|
||||
.noUi-value-vertical {
|
||||
-webkit-transform: translate3d(0,50%,0);
|
||||
transform: translate3d(0,50%,0);
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
.noUi-marker-vertical.noUi-marker {
|
||||
width: 5px;
|
||||
height: 2px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
.noUi-marker-vertical.noUi-marker-sub {
|
||||
width: 10px;
|
||||
}
|
||||
.noUi-marker-vertical.noUi-marker-large {
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.noUi-tooltip {
|
||||
display: block;
|
||||
position: absolute;
|
||||
border: 1px solid #D9D9D9;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.noUi-horizontal .noUi-tooltip {
|
||||
-webkit-transform: translate(-50%, 0);
|
||||
transform: translate(-50%, 0);
|
||||
left: 50%;
|
||||
bottom: 120%;
|
||||
}
|
||||
.noUi-vertical .noUi-tooltip {
|
||||
-webkit-transform: translate(0, -50%);
|
||||
transform: translate(0, -50%);
|
||||
top: 50%;
|
||||
right: 120%;
|
||||
}
|
13
flask/forest/static/css/panel-transparent.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
.panel-transparent {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.panel-transparent .panel-heading {
|
||||
background: rgb(255, 255, 255); /* fallback */
|
||||
background: rgba(255, 255, 255, 0.6)!important;
|
||||
}
|
||||
|
||||
.panel-transparent .panel-body{
|
||||
background: rgb(255, 255, 255); /* fallback */
|
||||
background: rgba(255, 255, 255, 0.5)!important;
|
||||
}
|
86
flask/forest/static/css/range.css
Normal file
|
@ -0,0 +1,86 @@
|
|||
input[type=range] {
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
margin: 13.8px 0;
|
||||
}
|
||||
input[type=range]:focus {
|
||||
outline: none;
|
||||
}
|
||||
input[type=range]::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 8.4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
|
||||
background: #3071a9;
|
||||
border-radius: 1.3px;
|
||||
border: 0.2px solid #010101;
|
||||
}
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
|
||||
border: 1px solid #000000;
|
||||
height: 36px;
|
||||
width: 16px;
|
||||
border-radius: 3px;
|
||||
background: #f8ffff;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
margin-top: -14px;
|
||||
}
|
||||
input[type=range]:focus::-webkit-slider-runnable-track {
|
||||
background: #367ebd;
|
||||
}
|
||||
input[type=range]::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 8.4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
|
||||
background: #3071a9;
|
||||
border-radius: 1.3px;
|
||||
border: 0.2px solid #010101;
|
||||
}
|
||||
input[type=range]::-moz-range-thumb {
|
||||
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
|
||||
border: 1px solid #000000;
|
||||
height: 36px;
|
||||
width: 16px;
|
||||
border-radius: 3px;
|
||||
background: #f8ffff;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type=range]::-ms-track {
|
||||
width: 100%;
|
||||
height: 8.4px;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
input[type=range]::-ms-fill-lower {
|
||||
background: #2a6495;
|
||||
border: 0.2px solid #010101;
|
||||
border-radius: 2.6px;
|
||||
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
|
||||
}
|
||||
input[type=range]::-ms-fill-upper {
|
||||
background: #3071a9;
|
||||
border: 0.2px solid #010101;
|
||||
border-radius: 2.6px;
|
||||
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
|
||||
}
|
||||
input[type=range]::-ms-thumb {
|
||||
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
|
||||
border: 1px solid #000000;
|
||||
height: 36px;
|
||||
width: 16px;
|
||||
border-radius: 3px;
|
||||
background: #f8ffff;
|
||||
cursor: pointer;
|
||||
height: 8.4px;
|
||||
}
|
||||
input[type=range]:focus::-ms-fill-lower {
|
||||
background: #3071a9;
|
||||
}
|
||||
input[type=range]:focus::-ms-fill-upper {
|
||||
background: #367ebd;
|
||||
}
|
||||
|
138
flask/forest/static/css/simple-slideshow-styles.css
Normal file
|
@ -0,0 +1,138 @@
|
|||
.bss-slides{
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
.bss-slides:focus{
|
||||
outline: 0;
|
||||
}
|
||||
.bss-slides figure{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.bss-slides figure:first-child{
|
||||
position: relative;
|
||||
}
|
||||
.bss-slides figure img{
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 1.2s;
|
||||
transition: opacity 1.2s;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.bss-slides .bss-show{
|
||||
z-index: 2;
|
||||
}
|
||||
.bss-slides .bss-show img{
|
||||
opacity: 1;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bss-slides figcaption{
|
||||
position: absolute;
|
||||
font-family: sans-serif;
|
||||
font-size: .8em;
|
||||
bottom: .75em;
|
||||
right: .35em;
|
||||
padding: .25em;
|
||||
color: #fff;
|
||||
background: #000;
|
||||
background: rgba(0,0,0, .25);
|
||||
border-radius: 2px;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 1.2s;
|
||||
transition: opacity 1.2s;
|
||||
}
|
||||
.bss-slides .bss-show figcaption{
|
||||
z-index: 3;
|
||||
opacity: 1;
|
||||
}
|
||||
.bss-slides figcaption a{
|
||||
color: #fff;
|
||||
}
|
||||
.bss-next, .bss-prev{
|
||||
visibility: hidden;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
background: #000;
|
||||
background: rgba(0,0,0, .6);
|
||||
top: 50%;
|
||||
z-index: 4;
|
||||
font-size: 2em;
|
||||
margin-top: -1.2em;
|
||||
opacity: .3;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.bss-next:hover, .bss-prev:hover{
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
.bss-next{
|
||||
right: -1px;
|
||||
padding: 10px 5px 15px 10px;
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
.bss-prev{
|
||||
left: 0;
|
||||
padding: 10px 10px 15px 5px;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
.bss-fullscreen{
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba(0,0,0,.4) url(/static/images/arrows-alt_ffffff_64.png);
|
||||
-webkit-background-size: contain;
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
cursor: pointer;
|
||||
opacity: .3;
|
||||
}
|
||||
.bss-fullscreen:hover{
|
||||
opacity: .8;
|
||||
}
|
||||
:-webkit-full-screen .bss-fullscreen{
|
||||
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
|
||||
-webkit-background-size: contain;
|
||||
background-size: contain;
|
||||
}
|
||||
:-moz-full-screen .bss-fullscreen{
|
||||
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
|
||||
background-size: contain;
|
||||
}
|
||||
:-ms-fullscreen .bss-fullscreen{
|
||||
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
|
||||
background-size: contain;
|
||||
}
|
||||
:full-screen .bss-fullscreen{
|
||||
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
|
||||
-webkit-background-size: contain;
|
||||
background-size: contain;
|
||||
}
|
||||
:-webkit-full-screen .bss-fullscreen{
|
||||
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
|
||||
-webkit-background-size: contain;
|
||||
background-size: contain;
|
||||
}
|
||||
:-moz-full-screen .bss-fullscreen{
|
||||
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
|
||||
background-size: contain;
|
||||
}
|
||||
:-ms-fullscreen .bss-fullscreen{
|
||||
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
|
||||
background-size: contain;
|
||||
}
|
||||
:fullscreen .bss-fullscreen{
|
||||
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
|
||||
-webkit-background-size: contain;
|
||||
background-size: contain;
|
||||
}
|
295
flask/forest/static/css/style.css
Normal file
|
@ -0,0 +1,295 @@
|
|||
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
|
||||
|
||||
body {
|
||||
/* background: url('/static/images/purplebg.jpg') no-repeat center center fixed; */
|
||||
background-size: cover;
|
||||
-webkit-background-size: cover;
|
||||
-moz-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
padding-top: 0px;
|
||||
background-color: #edf0f5;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
top:0;
|
||||
left:0;
|
||||
width:100%;
|
||||
height:100%;
|
||||
font-size: 16px;
|
||||
/* font-weight: bold; */
|
||||
font-family: "Roboto", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.page_wrap {
|
||||
width: 88%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
max-width: 92%;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.container-fluid-index a:active {
|
||||
color: lightgray;
|
||||
}
|
||||
|
||||
.container-fluid-index a:link {
|
||||
color: lightgray;
|
||||
}
|
||||
|
||||
.container-fluid-index a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.container-fluid-index a:visited {
|
||||
color: lightgray;
|
||||
}
|
||||
|
||||
.roundavatar {
|
||||
border-radius: 50%;
|
||||
-moz-border-radius: 50%;
|
||||
-webkit-border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
#footer_cols {
|
||||
background:url('/static/images/footer_header_bg.png') repeat;
|
||||
border-bottom: 1px solid #2c2c2e;
|
||||
overflow: hidden;
|
||||
padding: 16px 0 20px 0;
|
||||
color: #818387;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear:both;
|
||||
}
|
||||
|
||||
.footerblock {
|
||||
max-width: 420px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.footerblock h4 {
|
||||
font-size: 18px;
|
||||
color: #FFF;
|
||||
background: url('/static/images/footer_cols_divider.png') no-repeat bottom center;
|
||||
padding-bottom: 13px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.last_col {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tweet_day {
|
||||
font-size: 14px;
|
||||
color: #484a4e;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.form_field {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.newsletter_input {
|
||||
background: url('/static/images/footer_newsletter_input.png') no-repeat;
|
||||
width: 190px;
|
||||
height: 38px;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.newsletter_submit {
|
||||
background: url('/static/images/footer_newsletter_input_btn.png') no-repeat;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 0;
|
||||
text-indent: -9999px;
|
||||
font-size: 0;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Footer Copyright */
|
||||
#footer_copyright {
|
||||
background-color: #536a2f;
|
||||
/* background: url(/static/images/footer_top_bg.png) repeat; */
|
||||
overflow: hidden;
|
||||
padding: 20px 0 5px 0;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
color: #fff;
|
||||
float: left;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.copyright a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.design_by {
|
||||
color: #fff;
|
||||
float: right;
|
||||
width: 300px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.design_by a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
header {
|
||||
background: url('/static/images/texture-diagonal.png' repeat, url('static/images/header-layer.jpg') no repeat 50% -25px, #493874 url('/static/images/bg-linear.jpg') repeat-x 50%, -25px;
|
||||
background-position: 50%, 0;
|
||||
clear: both;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #2DA6F7;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #2DA6F7;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0099f0;
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: #2DA6F7;
|
||||
}
|
||||
|
||||
.fluidMedia {
|
||||
position: relative;
|
||||
padding-bottom: 56.25%; /* proportion value to aspect ratio 16:9 (9 / 16 = 0.5625 or 56.25%) */
|
||||
padding-top: 30px;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fluidMedia iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
max-width: 350px;
|
||||
margin-top: 25%;
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
margin-top: 25%;
|
||||
}
|
||||
|
||||
.form {
|
||||
max-width: 660px;
|
||||
}
|
||||
|
||||
.padding-left-32 {
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
.padding-left-16 {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
|
||||
.btn-outline {
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.btn-primary.btn-outline {
|
||||
color: #428bca;
|
||||
}
|
||||
|
||||
.btn-success.btn-outline {
|
||||
color: #5cb85c;
|
||||
}
|
||||
|
||||
.btn-info.btn-outline {
|
||||
color: #5bc0de;
|
||||
}
|
||||
|
||||
.btn-warning.btn-outline {
|
||||
color: #f0ad4e;
|
||||
}
|
||||
|
||||
.btn-danger.btn-outline {
|
||||
color: #d9534f;
|
||||
}
|
||||
|
||||
.btn-primary.btn-outline:hover,
|
||||
.btn-success.btn-outline:hover,
|
||||
.btn-info.btn-outline:hover,
|
||||
.btn-warning.btn-outline:hover,
|
||||
.btn-danger.btn-outline:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary.btn-outline:focus,
|
||||
.btn-success.btn-outline:focus,
|
||||
.btn-info.btn-outline:focus,
|
||||
.btn-warning.btn-outline:focus,
|
||||
.btn-danger.btn-outline:focus {
|
||||
background-color: #fff;
|
||||
border-color: #070;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
padding: 6px 15px;
|
||||
border-bottom: 1px solid transparent;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
|
||||
.rrd + .tooltip > .tooltip-inner {
|
||||
background-color: #73AD21;
|
||||
color: #FFFFFF;
|
||||
border: 1px solid green;
|
||||
padding: 15px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* Tooltip on top */
|
||||
.rrd + .tooltip.top > .tooltip-arrow {
|
||||
border-top: 5px solid green;
|
||||
}
|
||||
|
||||
/* Tooltip on bottom */
|
||||
.rrd + .tooltip.bottom > .tooltip-arrow {
|
||||
border-bottom: 5px solid blue;
|
||||
}
|
||||
|
||||
/* Tooltip on left */
|
||||
.rrd + .tooltip.left > .tooltip-arrow {
|
||||
border-left: 5px solid red;
|
||||
}
|
||||
|
||||
/* Tooltip on right */
|
||||
.rrd + .tooltip.right > .tooltip-arrow {
|
||||
border-right: 5px solid black;
|
||||
}
|
BIN
flask/forest/static/images/220x180.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
flask/forest/static/images/VPS-Mission.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
flask/forest/static/images/VPS-Security.png
Normal file
After Width: | Height: | Size: 7 KiB |
BIN
flask/forest/static/images/VPS-Support.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
flask/forest/static/images/VPS-equipment.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
flask/forest/static/images/_bg.jpg
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
flask/forest/static/images/arrows-alt_ffffff_64.png
Normal file
After Width: | Height: | Size: 1,015 B |
BIN
flask/forest/static/images/bg-linear.jpg
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
flask/forest/static/images/bg.jpg
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
flask/forest/static/images/cloudsbg.jpeg
Normal file
After Width: | Height: | Size: 242 KiB |
BIN
flask/forest/static/images/cloudsbg.jpeg_disabled
Normal file
After Width: | Height: | Size: 8.2 MiB |
BIN
flask/forest/static/images/compress_ffffff_64.png
Normal file
After Width: | Height: | Size: 797 B |
BIN
flask/forest/static/images/createvm.gif
Normal file
After Width: | Height: | Size: 251 KiB |
BIN
flask/forest/static/images/datapoint-simple-logo.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
flask/forest/static/images/datapoint.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
flask/forest/static/images/fb.png
Normal file
After Width: | Height: | Size: 981 B |
BIN
flask/forest/static/images/footer_cols_divider.png
Normal file
After Width: | Height: | Size: 941 B |
BIN
flask/forest/static/images/footer_header_bg.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
flask/forest/static/images/footer_newsletter_input.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
flask/forest/static/images/footer_newsletter_input_btn.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
flask/forest/static/images/footer_top_bg.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
flask/forest/static/images/header-layer.jpg
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
flask/forest/static/images/hex24.png
Normal file
After Width: | Height: | Size: 999 B |
BIN
flask/forest/static/images/hex32.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
flask/forest/static/images/hex512.png
Normal file
After Width: | Height: | Size: 59 KiB |
After Width: | Height: | Size: 5.4 KiB |
BIN
flask/forest/static/images/panel/icons8-processor-40.png
Normal file
After Width: | Height: | Size: 568 B |
BIN
flask/forest/static/images/purplebg.jpg
Normal file
After Width: | Height: | Size: 311 KiB |
BIN
flask/forest/static/images/purplebg1.jpg
Normal file
After Width: | Height: | Size: 179 KiB |
BIN
flask/forest/static/images/purplebg2.jpg
Normal file
After Width: | Height: | Size: 311 KiB |
BIN
flask/forest/static/images/server.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
flask/forest/static/images/srv.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
flask/forest/static/images/texture-diagonal.png
Normal file
After Width: | Height: | Size: 85 B |
163
flask/forest/static/js/better-simple-slideshow.js
Normal file
|
@ -0,0 +1,163 @@
|
|||
var makeBSS = function (el, options) {
|
||||
var $slideshows = document.querySelectorAll(el), // a collection of all of the slideshow
|
||||
$slideshow = {},
|
||||
Slideshow = {
|
||||
init: function (el, options) {
|
||||
|
||||
options = options || {}; // if options object not passed in, then set to empty object
|
||||
options.auto = options.auto || false; // if options.auto object not passed in, then set to false
|
||||
this.opts = {
|
||||
selector: (typeof options.selector === "undefined") ? "figure" : options.selector,
|
||||
auto: (typeof options.auto === "undefined") ? false : options.auto,
|
||||
speed: (typeof options.auto.speed === "undefined") ? 1500 : options.auto.speed,
|
||||
pauseOnHover: (typeof options.auto.pauseOnHover === "undefined") ? false : options.auto.pauseOnHover,
|
||||
fullScreen: (typeof options.fullScreen === "undefined") ? false : options.fullScreen,
|
||||
swipe: (typeof options.swipe === "undefined") ? false : options.swipe
|
||||
};
|
||||
|
||||
this.counter = 0; // to keep track of current slide
|
||||
this.el = el; // current slideshow container
|
||||
this.$items = el.querySelectorAll(this.opts.selector); // a collection of all of the slides, caching for performance
|
||||
this.numItems = this.$items.length; // total number of slides
|
||||
this.$items[0].classList.add('bss-show'); // add show class to first figure
|
||||
this.injectControls(el);
|
||||
this.addEventListeners(el);
|
||||
if (this.opts.auto) {
|
||||
this.autoCycle(this.el, this.opts.speed, this.opts.pauseOnHover);
|
||||
}
|
||||
if (this.opts.fullScreen) {
|
||||
this.addFullScreen(this.el);
|
||||
}
|
||||
if (this.opts.swipe) {
|
||||
this.addSwipe(this.el);
|
||||
}
|
||||
},
|
||||
showCurrent: function (i) {
|
||||
// increment or decrement this.counter depending on whether i === 1 or i === -1
|
||||
if (i > 0) {
|
||||
this.counter = (this.counter + 1 === this.numItems) ? 0 : this.counter + 1;
|
||||
} else {
|
||||
this.counter = (this.counter - 1 < 0) ? this.numItems - 1 : this.counter - 1;
|
||||
}
|
||||
|
||||
// remove .show from whichever element currently has it
|
||||
// http://stackoverflow.com/a/16053538/2006057
|
||||
[].forEach.call(this.$items, function (el) {
|
||||
el.classList.remove('bss-show');
|
||||
});
|
||||
|
||||
// add .show to the one item that's supposed to have it
|
||||
this.$items[this.counter].classList.add('bss-show');
|
||||
},
|
||||
injectControls: function (el) {
|
||||
// build and inject prev/next controls
|
||||
// first create all the new elements
|
||||
var spanPrev = document.createElement("span"),
|
||||
spanNext = document.createElement("span"),
|
||||
docFrag = document.createDocumentFragment();
|
||||
|
||||
// add classes
|
||||
spanPrev.classList.add('bss-prev');
|
||||
spanNext.classList.add('bss-next');
|
||||
|
||||
// add contents
|
||||
spanPrev.innerHTML = '«';
|
||||
spanNext.innerHTML = '»';
|
||||
|
||||
// append elements to fragment, then append fragment to DOM
|
||||
docFrag.appendChild(spanPrev);
|
||||
docFrag.appendChild(spanNext);
|
||||
el.appendChild(docFrag);
|
||||
},
|
||||
addEventListeners: function (el) {
|
||||
var that = this;
|
||||
el.querySelector('.bss-next').addEventListener('click', function () {
|
||||
that.showCurrent(1); // increment & show
|
||||
}, false);
|
||||
|
||||
el.querySelector('.bss-prev').addEventListener('click', function () {
|
||||
that.showCurrent(-1); // decrement & show
|
||||
}, false);
|
||||
|
||||
el.onkeydown = function (e) {
|
||||
e = e || window.event;
|
||||
if (e.keyCode === 37) {
|
||||
that.showCurrent(-1); // decrement & show
|
||||
} else if (e.keyCode === 39) {
|
||||
that.showCurrent(1); // increment & show
|
||||
}
|
||||
};
|
||||
},
|
||||
autoCycle: function (el, speed, pauseOnHover) {
|
||||
var that = this,
|
||||
interval = window.setInterval(function () {
|
||||
that.showCurrent(1); // increment & show
|
||||
}, speed);
|
||||
|
||||
if (pauseOnHover) {
|
||||
el.addEventListener('mouseover', function () {
|
||||
interval = clearInterval(interval);
|
||||
}, false);
|
||||
el.addEventListener('mouseout', function () {
|
||||
interval = window.setInterval(function () {
|
||||
that.showCurrent(1); // increment & show
|
||||
}, speed);
|
||||
}, false);
|
||||
} // end pauseonhover
|
||||
|
||||
},
|
||||
addFullScreen: function(el){
|
||||
var that = this,
|
||||
fsControl = document.createElement("span");
|
||||
|
||||
fsControl.classList.add('bss-fullscreen');
|
||||
el.appendChild(fsControl);
|
||||
el.querySelector('.bss-fullscreen').addEventListener('click', function () {
|
||||
that.toggleFullScreen(el);
|
||||
}, false);
|
||||
},
|
||||
addSwipe: function(el){
|
||||
var that = this,
|
||||
ht = new Hammer(el);
|
||||
ht.on('swiperight', function(e) {
|
||||
that.showCurrent(-1); // decrement & show
|
||||
});
|
||||
ht.on('swipeleft', function(e) {
|
||||
that.showCurrent(1); // increment & show
|
||||
});
|
||||
},
|
||||
toggleFullScreen: function(el){
|
||||
// https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Using_full_screen_mode
|
||||
if (!document.fullscreenElement && // alternative standard method
|
||||
!document.mozFullScreenElement && !document.webkitFullscreenElement &&
|
||||
!document.msFullscreenElement ) { // current working methods
|
||||
if (document.documentElement.requestFullscreen) {
|
||||
el.requestFullscreen();
|
||||
} else if (document.documentElement.msRequestFullscreen) {
|
||||
el.msRequestFullscreen();
|
||||
} else if (document.documentElement.mozRequestFullScreen) {
|
||||
el.mozRequestFullScreen();
|
||||
} else if (document.documentElement.webkitRequestFullscreen) {
|
||||
el.webkitRequestFullscreen(el.ALLOW_KEYBOARD_INPUT);
|
||||
}
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
}
|
||||
}
|
||||
} // end toggleFullScreen
|
||||
|
||||
}; // end Slideshow object .....
|
||||
|
||||
// make instances of Slideshow as needed
|
||||
[].forEach.call($slideshows, function (el) {
|
||||
$slideshow = Object.create(Slideshow);
|
||||
$slideshow.init(el, options);
|
||||
});
|
||||
};
|
1807
flask/forest/static/js/bootstrap-slider.js
vendored
Normal file
49
flask/forest/static/js/clouds.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
var canvas;
|
||||
var ctx;
|
||||
|
||||
var background;
|
||||
var width = 300;
|
||||
var height = 200;
|
||||
|
||||
var cloud;
|
||||
var cloud_x;
|
||||
|
||||
function init() {
|
||||
canvas = document.getElementById("clouds");
|
||||
width = canvas.width;
|
||||
height = canvas.height;
|
||||
ctx = canvas.getContext("2d");
|
||||
|
||||
// init background
|
||||
background = new Image();
|
||||
background.src = 'http://silveiraneto.net/wp-content/uploads/2011/06/forest.png';
|
||||
|
||||
// init cloud
|
||||
cloud = new Image();
|
||||
cloud.src = 'http://silveiraneto.net/wp-content/uploads/2011/06/cloud.png';
|
||||
cloud.onload = function(){
|
||||
cloud_x = -cloud.width;
|
||||
};
|
||||
|
||||
return setInterval(main_loop, 10);
|
||||
}
|
||||
|
||||
function update(){
|
||||
cloud_x += 0.3;
|
||||
if (cloud_x > width ) {
|
||||
cloud_x = -cloud.width;
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
ctx.drawImage(background,0,0);
|
||||
ctx.drawImage(cloud, cloud_x, 0);
|
||||
}
|
||||
|
||||
function main_loop() {
|
||||
draw();
|
||||
update();
|
||||
}
|
||||
|
||||
init();
|
||||
|
4
flask/forest/static/js/jquery.js
vendored
Normal file
1850
flask/forest/static/js/mgui.js
Normal file
3
flask/forest/static/js/nouislider.min.js
vendored
Normal file
493
flask/forest/static/js/rangeslider.js
Normal file
|
@ -0,0 +1,493 @@
|
|||
(function(factory) {
|
||||
'use strict';
|
||||
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['jquery'], factory);
|
||||
} else if (typeof exports === 'object') {
|
||||
// CommonJS
|
||||
module.exports = factory(require('jquery'));
|
||||
} else {
|
||||
// Browser globals
|
||||
factory(jQuery);
|
||||
}
|
||||
}(function($) {
|
||||
'use strict';
|
||||
|
||||
// Polyfill Number.isNaN(value)
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
|
||||
Number.isNaN = Number.isNaN || function(value) {
|
||||
return typeof value === 'number' && value !== value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Range feature detection
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function supportsRange() {
|
||||
var input = document.createElement('input');
|
||||
input.setAttribute('type', 'range');
|
||||
return input.type !== 'text';
|
||||
}
|
||||
|
||||
var pluginName = 'rangeslider',
|
||||
pluginIdentifier = 0,
|
||||
hasInputRangeSupport = supportsRange(),
|
||||
defaults = {
|
||||
polyfill: true,
|
||||
orientation: 'horizontal',
|
||||
rangeClass: 'rangeslider',
|
||||
disabledClass: 'rangeslider--disabled',
|
||||
activeClass: 'rangeslider--active',
|
||||
horizontalClass: 'rangeslider--horizontal',
|
||||
verticalClass: 'rangeslider--vertical',
|
||||
fillClass: 'rangeslider__fill',
|
||||
handleClass: 'rangeslider__handle',
|
||||
startEvent: ['mousedown', 'touchstart', 'pointerdown'],
|
||||
moveEvent: ['mousemove', 'touchmove', 'pointermove'],
|
||||
endEvent: ['mouseup', 'touchend', 'pointerup']
|
||||
},
|
||||
constants = {
|
||||
orientation: {
|
||||
horizontal: {
|
||||
dimension: 'width',
|
||||
direction: 'left',
|
||||
directionStyle: 'left',
|
||||
coordinate: 'x'
|
||||
},
|
||||
vertical: {
|
||||
dimension: 'height',
|
||||
direction: 'top',
|
||||
directionStyle: 'bottom',
|
||||
coordinate: 'y'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delays a function for the given number of milliseconds, and then calls
|
||||
* it with the arguments supplied.
|
||||
*
|
||||
* @param {Function} fn [description]
|
||||
* @param {Number} wait [description]
|
||||
* @return {Function}
|
||||
*/
|
||||
function delay(fn, wait) {
|
||||
var args = Array.prototype.slice.call(arguments, 2);
|
||||
return setTimeout(function(){ return fn.apply(null, args); }, wait);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a debounced function that will make sure the given
|
||||
* function is not triggered too much.
|
||||
*
|
||||
* @param {Function} fn Function to debounce.
|
||||
* @param {Number} debounceDuration OPTIONAL. The amount of time in milliseconds for which we will debounce the function. (defaults to 100ms)
|
||||
* @return {Function}
|
||||
*/
|
||||
function debounce(fn, debounceDuration) {
|
||||
debounceDuration = debounceDuration || 100;
|
||||
return function() {
|
||||
if (!fn.debouncing) {
|
||||
var args = Array.prototype.slice.apply(arguments);
|
||||
fn.lastReturnVal = fn.apply(window, args);
|
||||
fn.debouncing = true;
|
||||
}
|
||||
clearTimeout(fn.debounceTimeout);
|
||||
fn.debounceTimeout = setTimeout(function(){
|
||||
fn.debouncing = false;
|
||||
}, debounceDuration);
|
||||
return fn.lastReturnVal;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a `element` is visible in the DOM
|
||||
*
|
||||
* @param {Element} element
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function isHidden(element) {
|
||||
return (
|
||||
element && (
|
||||
element.offsetWidth === 0 ||
|
||||
element.offsetHeight === 0 ||
|
||||
// Also Consider native `<details>` elements.
|
||||
element.open === false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hidden parentNodes of an `element`
|
||||
*
|
||||
* @param {Element} element
|
||||
* @return {[type]}
|
||||
*/
|
||||
function getHiddenParentNodes(element) {
|
||||
var parents = [],
|
||||
node = element.parentNode;
|
||||
|
||||
while (isHidden(node)) {
|
||||
parents.push(node);
|
||||
node = node.parentNode;
|
||||
}
|
||||
return parents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns dimensions for an element even if it is not visible in the DOM.
|
||||
*
|
||||
* @param {Element} element
|
||||
* @param {String} key (e.g. offsetWidth …)
|
||||
* @return {Number}
|
||||
*/
|
||||
function getDimension(element, key) {
|
||||
var hiddenParentNodes = getHiddenParentNodes(element),
|
||||
hiddenParentNodesLength = hiddenParentNodes.length,
|
||||
inlineStyle = [],
|
||||
dimension = element[key];
|
||||
|
||||
// Used for native `<details>` elements
|
||||
function toggleOpenProperty(element) {
|
||||
if (typeof element.open !== 'undefined') {
|
||||
element.open = (element.open) ? false : true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hiddenParentNodesLength) {
|
||||
for (var i = 0; i < hiddenParentNodesLength; i++) {
|
||||
|
||||
// Cache style attribute to restore it later.
|
||||
inlineStyle[i] = hiddenParentNodes[i].style.cssText;
|
||||
|
||||
// visually hide
|
||||
if (hiddenParentNodes[i].style.setProperty) {
|
||||
hiddenParentNodes[i].style.setProperty('display', 'block', 'important');
|
||||
} else {
|
||||
hiddenParentNodes[i].style.cssText += ';display: block !important';
|
||||
}
|
||||
hiddenParentNodes[i].style.height = '0';
|
||||
hiddenParentNodes[i].style.overflow = 'hidden';
|
||||
hiddenParentNodes[i].style.visibility = 'hidden';
|
||||
toggleOpenProperty(hiddenParentNodes[i]);
|
||||
}
|
||||
|
||||
// Update dimension
|
||||
dimension = element[key];
|
||||
|
||||
for (var j = 0; j < hiddenParentNodesLength; j++) {
|
||||
|
||||
// Restore the style attribute
|
||||
hiddenParentNodes[j].style.cssText = inlineStyle[j];
|
||||
toggleOpenProperty(hiddenParentNodes[j]);
|
||||
}
|
||||
}
|
||||
return dimension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parsed float or the default if it failed.
|
||||
*
|
||||
* @param {String} str
|
||||
* @param {Number} defaultValue
|
||||
* @return {Number}
|
||||
*/
|
||||
function tryParseFloat(str, defaultValue) {
|
||||
var value = parseFloat(str);
|
||||
return Number.isNaN(value) ? defaultValue : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize the first letter of string
|
||||
*
|
||||
* @param {String} str
|
||||
* @return {String}
|
||||
*/
|
||||
function ucfirst(str) {
|
||||
return str.charAt(0).toUpperCase() + str.substr(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin
|
||||
* @param {String} element
|
||||
* @param {Object} options
|
||||
*/
|
||||
function Plugin(element, options) {
|
||||
this.$window = $(window);
|
||||
this.$document = $(document);
|
||||
this.$element = $(element);
|
||||
this.options = $.extend( {}, defaults, options );
|
||||
this.polyfill = this.options.polyfill;
|
||||
this.orientation = this.$element[0].getAttribute('data-orientation') || this.options.orientation;
|
||||
this.onInit = this.options.onInit;
|
||||
this.onSlide = this.options.onSlide;
|
||||
this.onSlideEnd = this.options.onSlideEnd;
|
||||
this.DIMENSION = constants.orientation[this.orientation].dimension;
|
||||
this.DIRECTION = constants.orientation[this.orientation].direction;
|
||||
this.DIRECTION_STYLE = constants.orientation[this.orientation].directionStyle;
|
||||
this.COORDINATE = constants.orientation[this.orientation].coordinate;
|
||||
|
||||
// Plugin should only be used as a polyfill
|
||||
if (this.polyfill) {
|
||||
// Input range support?
|
||||
if (hasInputRangeSupport) { return false; }
|
||||
}
|
||||
|
||||
this.identifier = 'js-' + pluginName + '-' +(pluginIdentifier++);
|
||||
this.startEvent = this.options.startEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
|
||||
this.moveEvent = this.options.moveEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
|
||||
this.endEvent = this.options.endEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
|
||||
this.toFixed = (this.step + '').replace('.', '').length - 1;
|
||||
this.$fill = $('<div class="' + this.options.fillClass + '" />');
|
||||
this.$handle = $('<div class="' + this.options.handleClass + '" />');
|
||||
this.$range = $('<div class="' + this.options.rangeClass + ' ' + this.options[this.orientation + 'Class'] + '" id="' + this.identifier + '" />').insertAfter(this.$element).prepend(this.$fill, this.$handle);
|
||||
|
||||
// visually hide the input
|
||||
this.$element.css({
|
||||
'position': 'absolute',
|
||||
'width': '1px',
|
||||
'height': '1px',
|
||||
'overflow': 'hidden',
|
||||
'opacity': '0'
|
||||
});
|
||||
|
||||
// Store context
|
||||
this.handleDown = $.proxy(this.handleDown, this);
|
||||
this.handleMove = $.proxy(this.handleMove, this);
|
||||
this.handleEnd = $.proxy(this.handleEnd, this);
|
||||
|
||||
this.init();
|
||||
|
||||
// Attach Events
|
||||
var _this = this;
|
||||
this.$window.on('resize.' + this.identifier, debounce(function() {
|
||||
// Simulate resizeEnd event.
|
||||
delay(function() { _this.update(false, false); }, 300);
|
||||
}, 20));
|
||||
|
||||
this.$document.on(this.startEvent, '#' + this.identifier + ':not(.' + this.options.disabledClass + ')', this.handleDown);
|
||||
|
||||
// Listen to programmatic value changes
|
||||
this.$element.on('change.' + this.identifier, function(e, data) {
|
||||
if (data && data.origin === _this.identifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
var value = e.target.value,
|
||||
pos = _this.getPositionFromValue(value);
|
||||
_this.setPosition(pos);
|
||||
});
|
||||
}
|
||||
|
||||
Plugin.prototype.init = function() {
|
||||
this.update(true, false);
|
||||
|
||||
if (this.onInit && typeof this.onInit === 'function') {
|
||||
this.onInit();
|
||||
}
|
||||
};
|
||||
|
||||
Plugin.prototype.update = function(updateAttributes, triggerSlide) {
|
||||
updateAttributes = updateAttributes || false;
|
||||
|
||||
if (updateAttributes) {
|
||||
this.min = tryParseFloat(this.$element[0].getAttribute('min'), 0);
|
||||
this.max = tryParseFloat(this.$element[0].getAttribute('max'), 100);
|
||||
this.value = tryParseFloat(this.$element[0].value, Math.round(this.min + (this.max-this.min)/2));
|
||||
this.step = tryParseFloat(this.$element[0].getAttribute('step'), 1);
|
||||
}
|
||||
|
||||
this.handleDimension = getDimension(this.$handle[0], 'offset' + ucfirst(this.DIMENSION));
|
||||
this.rangeDimension = getDimension(this.$range[0], 'offset' + ucfirst(this.DIMENSION));
|
||||
this.maxHandlePos = this.rangeDimension - this.handleDimension;
|
||||
this.grabPos = this.handleDimension / 2;
|
||||
this.position = this.getPositionFromValue(this.value);
|
||||
|
||||
// Consider disabled state
|
||||
if (this.$element[0].disabled) {
|
||||
this.$range.addClass(this.options.disabledClass);
|
||||
} else {
|
||||
this.$range.removeClass(this.options.disabledClass);
|
||||
}
|
||||
|
||||
this.setPosition(this.position, triggerSlide);
|
||||
};
|
||||
|
||||
Plugin.prototype.handleDown = function(e) {
|
||||
e.preventDefault();
|
||||
this.$document.on(this.moveEvent, this.handleMove);
|
||||
this.$document.on(this.endEvent, this.handleEnd);
|
||||
|
||||
// add active class because Firefox is ignoring
|
||||
// the handle:active pseudo selector because of `e.preventDefault();`
|
||||
this.$range.addClass(this.options.activeClass);
|
||||
|
||||
// If we click on the handle don't set the new position
|
||||
if ((' ' + e.target.className + ' ').replace(/[\n\t]/g, ' ').indexOf(this.options.handleClass) > -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var pos = this.getRelativePosition(e),
|
||||
rangePos = this.$range[0].getBoundingClientRect()[this.DIRECTION],
|
||||
handlePos = this.getPositionFromNode(this.$handle[0]) - rangePos,
|
||||
setPos = (this.orientation === 'vertical') ? (this.maxHandlePos - (pos - this.grabPos)) : (pos - this.grabPos);
|
||||
|
||||
this.setPosition(setPos);
|
||||
|
||||
if (pos >= handlePos && pos < handlePos + this.handleDimension) {
|
||||
this.grabPos = pos - handlePos;
|
||||
}
|
||||
};
|
||||
|
||||
Plugin.prototype.handleMove = function(e) {
|
||||
e.preventDefault();
|
||||
var pos = this.getRelativePosition(e);
|
||||
var setPos = (this.orientation === 'vertical') ? (this.maxHandlePos - (pos - this.grabPos)) : (pos - this.grabPos);
|
||||
this.setPosition(setPos);
|
||||
};
|
||||
|
||||
Plugin.prototype.handleEnd = function(e) {
|
||||
e.preventDefault();
|
||||
this.$document.off(this.moveEvent, this.handleMove);
|
||||
this.$document.off(this.endEvent, this.handleEnd);
|
||||
|
||||
this.$range.removeClass(this.options.activeClass);
|
||||
|
||||
// Ok we're done fire the change event
|
||||
this.$element.trigger('change', { origin: this.identifier });
|
||||
|
||||
if (this.onSlideEnd && typeof this.onSlideEnd === 'function') {
|
||||
this.onSlideEnd(this.position, this.value);
|
||||
}
|
||||
};
|
||||
|
||||
Plugin.prototype.cap = function(pos, min, max) {
|
||||
if (pos < min) { return min; }
|
||||
if (pos > max) { return max; }
|
||||
return pos;
|
||||
};
|
||||
|
||||
Plugin.prototype.setPosition = function(pos, triggerSlide) {
|
||||
var value, newPos;
|
||||
|
||||
if (triggerSlide === undefined) {
|
||||
triggerSlide = true;
|
||||
}
|
||||
|
||||
// Snapping steps
|
||||
value = this.getValueFromPosition(this.cap(pos, 0, this.maxHandlePos));
|
||||
newPos = this.getPositionFromValue(value);
|
||||
|
||||
// Update ui
|
||||
this.$fill[0].style[this.DIMENSION] = (newPos + this.grabPos) + 'px';
|
||||
this.$handle[0].style[this.DIRECTION_STYLE] = newPos + 'px';
|
||||
this.setValue(value);
|
||||
|
||||
// Update globals
|
||||
this.position = newPos;
|
||||
this.value = value;
|
||||
|
||||
if (triggerSlide && this.onSlide && typeof this.onSlide === 'function') {
|
||||
this.onSlide(newPos, value);
|
||||
}
|
||||
};
|
||||
|
||||
// Returns element position relative to the parent
|
||||
Plugin.prototype.getPositionFromNode = function(node) {
|
||||
var i = 0;
|
||||
while (node !== null) {
|
||||
i += node.offsetLeft;
|
||||
node = node.offsetParent;
|
||||
}
|
||||
return i;
|
||||
};
|
||||
|
||||
Plugin.prototype.getRelativePosition = function(e) {
|
||||
// Get the offset DIRECTION relative to the viewport
|
||||
var ucCoordinate = ucfirst(this.COORDINATE),
|
||||
rangePos = this.$range[0].getBoundingClientRect()[this.DIRECTION],
|
||||
pageCoordinate = 0;
|
||||
|
||||
if (typeof e.originalEvent['client' + ucCoordinate] !== 'undefined') {
|
||||
pageCoordinate = e.originalEvent['client' + ucCoordinate];
|
||||
}
|
||||
else if (
|
||||
e.originalEvent.touches &&
|
||||
e.originalEvent.touches[0] &&
|
||||
typeof e.originalEvent.touches[0]['client' + ucCoordinate] !== 'undefined'
|
||||
) {
|
||||
pageCoordinate = e.originalEvent.touches[0]['client' + ucCoordinate];
|
||||
}
|
||||
else if(e.currentPoint && typeof e.currentPoint[this.COORDINATE] !== 'undefined') {
|
||||
pageCoordinate = e.currentPoint[this.COORDINATE];
|
||||
}
|
||||
|
||||
return pageCoordinate - rangePos;
|
||||
};
|
||||
|
||||
Plugin.prototype.getPositionFromValue = function(value) {
|
||||
var percentage, pos;
|
||||
percentage = (value - this.min)/(this.max - this.min);
|
||||
pos = (!Number.isNaN(percentage)) ? percentage * this.maxHandlePos : 0;
|
||||
return pos;
|
||||
};
|
||||
|
||||
Plugin.prototype.getValueFromPosition = function(pos) {
|
||||
var percentage, value;
|
||||
percentage = ((pos) / (this.maxHandlePos || 1));
|
||||
value = this.step * Math.round(percentage * (this.max - this.min) / this.step) + this.min;
|
||||
return Number((value).toFixed(this.toFixed));
|
||||
};
|
||||
|
||||
Plugin.prototype.setValue = function(value) {
|
||||
if (value === this.value && this.$element[0].value !== '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the new value and fire the `input` event
|
||||
this.$element
|
||||
.val(value)
|
||||
.trigger('input', { origin: this.identifier });
|
||||
};
|
||||
|
||||
Plugin.prototype.destroy = function() {
|
||||
this.$document.off('.' + this.identifier);
|
||||
this.$window.off('.' + this.identifier);
|
||||
|
||||
this.$element
|
||||
.off('.' + this.identifier)
|
||||
.removeAttr('style')
|
||||
.removeData('plugin_' + pluginName);
|
||||
|
||||
// Remove the generated markup
|
||||
if (this.$range && this.$range.length) {
|
||||
this.$range[0].parentNode.removeChild(this.$range[0]);
|
||||
}
|
||||
};
|
||||
|
||||
// A really lightweight plugin wrapper around the constructor,
|
||||
// preventing against multiple instantiations
|
||||
$.fn[pluginName] = function(options) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
|
||||
return this.each(function() {
|
||||
var $this = $(this),
|
||||
data = $this.data('plugin_' + pluginName);
|
||||
|
||||
// Create a new instance.
|
||||
if (!data) {
|
||||
$this.data('plugin_' + pluginName, (data = new Plugin(this, options)));
|
||||
}
|
||||
|
||||
// Make it possible to access methods from public.
|
||||
// e.g `$element.rangeslider('method');`
|
||||
if (typeof options === 'string') {
|
||||
data[options].apply(data, args);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return 'rangeslider.js is available in jQuery context e.g $(selector).rangeslider(options);';
|
||||
|
||||
}));
|
BIN
flask/forest/static/slideshow/1.jpg
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
flask/forest/static/slideshow/2.jpg
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
flask/forest/static/slideshow/3.jpg
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
flask/forest/static/slideshow/4.jpg
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
flask/forest/static/slideshow/5.jpg
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
flask/forest/static/slideshow/6.jpg
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
flask/forest/static/slideshow/robot.png
Normal file
After Width: | Height: | Size: 402 KiB |
BIN
flask/forest/static/slideshow/robot2.png
Normal file
After Width: | Height: | Size: 424 KiB |
65
flask/forest/templates/admin/addr2pool.html
Normal file
|
@ -0,0 +1,65 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Add IP address to ip pool{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="row">
|
||||
{% include "admin/admin_tasks.html" %}
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">Add IP address to the pool</div>
|
||||
<div class="panel-body">
|
||||
|
||||
<form method="POST" action="{{ url_for('admin.addr2pool') }}">
|
||||
<p>
|
||||
{{ form.region.label }} {{ form.region }}<br />
|
||||
{% for error in form.region.errors %}
|
||||
{{ error }}<br />
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.ip.label }} {{ form.ip }}<br />
|
||||
{% for error in form.ip.errors %}
|
||||
{{ error }}<br />
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.rdns.label }} {{ form.rdns }}<br />
|
||||
{% for error in form.rdns.errors %}
|
||||
{{ error }}<br />
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.reserved.label }} {{ form.reserved }}<br />
|
||||
{% for error in form.reserved.errors %}
|
||||
{{ error }}<br />
|
||||
{% endfor %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{{ form.csrf_token() }}
|
||||
{{ form.submit }}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">Current IP Pool</div>
|
||||
<div class="panel-body">
|
||||
{% for addr in alladdresses %}
|
||||
{{ addr }}<br />
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
9
flask/forest/templates/admin/admin_tasks.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<div class="col-md-12">
|
||||
<div class="panel panel-warning" id="prxadmincloud">
|
||||
<div class="panel-heading">Admin Panel</div>
|
||||
<div class="panel-body">
|
||||
{% include "admin/menu_cloud.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
39
flask/forest/templates/admin/charge.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Зареждане на сметка{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
{% include "admin/admin_tasks.html" %}
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">Зареждане на сметка</div>
|
||||
<div class="panel-body">
|
||||
<form method="POST" action="{{ url_for('admin.charge', user_pid=usr.pid) }}">
|
||||
<p>
|
||||
Current Value: {{ usr.wallet }}<br />
|
||||
{{ form.amount.label }} {{ form.amount }}<br />
|
||||
{% for error in form.amount.errors %}
|
||||
{{ error }}<br />
|
||||
{% endfor %}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{{ form.csrf_token() }}
|
||||
{{ form.submit }}
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
74
flask/forest/templates/admin/list_addresses.html
Normal file
|
@ -0,0 +1,74 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="row">
|
||||
{% include "admin/admin_tasks.html" %}
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-info" id="addresses">
|
||||
<div class="panel-heading">Addresses</div>
|
||||
<div class="panel-body"><p>
|
||||
{% include "admin/menu_deployments.html" %}
|
||||
<button class="btn btn-default" onclick="window.location.href='{{ url_for('admin.addr2pool') }}'"><span class="glyphicon glyphicon-plus" aria-hiddent="true"></span> Add IP to pool</button>
|
||||
|
||||
<div class="no-more-tables">
|
||||
<table class="table table-hover table-striped table-condensed cf">
|
||||
<thead>
|
||||
<tr>
|
||||
<!--<th>Region</th>-->
|
||||
<th>IP</th>
|
||||
<th>Assignee</th>
|
||||
<th>Server</th>
|
||||
<th>VLAN</th>
|
||||
<th>Deploy</th>
|
||||
<th>rDNS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for address in addresses %}
|
||||
<tr>
|
||||
{% if address.reserved == True %}<tr class="danger">{% else %}<tr>{% endif %}
|
||||
<!--<td data-title="Region">region1</td>-->
|
||||
<td data-title="IP">{{ address.ip }}</td>
|
||||
{% if address.user_id != None %}
|
||||
<td data-title="Assignee"><a href="{{ url_for('panel.dashboard', user_pid=address.user_id) }}">{{ address.owner.email }}</a></td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% if address.assignee != None %}
|
||||
<td data-title="Server">{{ address.assignee.deploy.server.name }}</td>
|
||||
<td data-title="VLAN">{{ address.assignee.vlan_id }}</td>
|
||||
<td data-title="Deploy">{{ address.assignee.deploy.machine_alias }}</td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
{% endif %}
|
||||
|
||||
{% if address.rdns != None %}
|
||||
<td data-title="rDNS">{{ address.rdns }}</td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
123
flask/forest/templates/admin/list_archive.html
Normal file
|
@ -0,0 +1,123 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
// Only run what comes next *after* the page has loaded
|
||||
addEventListener("DOMContentLoaded", function() {
|
||||
// Grab all of the elements with a class of command
|
||||
// (which all of the buttons we just created have)
|
||||
var commandButtons = document.querySelectorAll(".command");
|
||||
for (var i=0, l=commandButtons.length; i<l; i++) {
|
||||
var button = commandButtons[i];
|
||||
// For each button, listen for the "click" event
|
||||
button.addEventListener("click", function(e) {
|
||||
// When a click happens, stop the button
|
||||
// from submitting our form (if we have one)
|
||||
e.preventDefault();
|
||||
|
||||
if (window.confirm("Are you sure?")) {
|
||||
var clickedButton = e.target;
|
||||
var command = clickedButton.value;
|
||||
var vmid = clickedButton.getAttribute('vmid');
|
||||
|
||||
// Now we need to send the data to our server
|
||||
// without reloading the page - this is the domain of
|
||||
// AJAX (Asynchronous JavaScript And XML)
|
||||
// We will create a new request object
|
||||
// and set up a handler for the response
|
||||
var request = new XMLHttpRequest();
|
||||
request.onload = function() {
|
||||
// We could do more interesting things with the response
|
||||
// or, we could ignore it entirely
|
||||
//alert(request.responseText);
|
||||
};
|
||||
// We point the request at the appropriate command
|
||||
request.open("GET", "/vmanager/command/" + command + "/" + vmid, true);
|
||||
// and then we send it off
|
||||
request.send();
|
||||
alert("command " + command + " executed.");
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, true);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="row">
|
||||
{% include "admin/admin_tasks.html" %}
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-info" id="deployments">
|
||||
<div class="panel-heading">Deployments</div>
|
||||
<div class="panel-body"><p>
|
||||
{% include "admin/menu_deployments.html" %}
|
||||
<div class="no-more-tables">
|
||||
<table class="table table-hover table-striped table-condensed cf">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Owner</th>
|
||||
<th>Alias</th>
|
||||
<th>CPU</th>
|
||||
<th>Mem</th>
|
||||
<th>HDD</th>
|
||||
<th>Last Charged</th>
|
||||
<th>Days Left</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for deploy in deployments %}
|
||||
{% if deploy.deleted == True %}
|
||||
<tr class="active">
|
||||
{% else %}
|
||||
{% if deploy.enabled == False %}
|
||||
<tr class="danger">
|
||||
{% else %}
|
||||
{% if deploy.warning == True %}
|
||||
<tr class="warning">
|
||||
{% else %}
|
||||
<tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<td><a href="{{ url_for('panel.dashboard', user_pid=deploy.user_id) }}">{{ deploy.owner.email }}</a></td>
|
||||
<td>{{ deploy.machine_alias }}</font></td>
|
||||
<td>{{ deploy.machine_cpu }}</td>
|
||||
<td>{{ deploy.machine_mem }} MB</td>
|
||||
<td>{{ deploy.machine_hdd }} GB</td>
|
||||
{% if deploy.date_last_charge == None %}
|
||||
<td>Never</td>
|
||||
{% else %}
|
||||
<td>{{ moment(deploy.date_last_charge).format('lll') }} ({{ moment(deploy.date_last_charge).fromNow() }})</td>
|
||||
{% endif %}
|
||||
<td>{{ deploy.daysleft }}</td>
|
||||
{% if deploy.deleted == True %}
|
||||
<td>-deleted-</td>
|
||||
{% else %}
|
||||
<td><button class="btn btn-default btn-danger" onclick="location.reload();location.href='/vmanager/vmremove/{{ deploy.machine_id }}'"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete</button></td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
72
flask/forest/templates/admin/list_deployments.html
Normal file
|
@ -0,0 +1,72 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="row">
|
||||
{% include "admin/admin_tasks.html" %}
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-info" id="deployments">
|
||||
<div class="panel-heading">Deployments</div>
|
||||
<div class="panel-body"><p>
|
||||
|
||||
{% include "admin/menu_deployments.html" %}
|
||||
|
||||
<div class="no-more-tables">
|
||||
<table class="table table-hover table-striped table-condensed cf">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Server</th>
|
||||
<th>VLAN</th>
|
||||
<th>Alias</th>
|
||||
<th>CPU</th>
|
||||
<th>Mem</th>
|
||||
<th>HDD</th>
|
||||
<th>Last Charged</th>
|
||||
<th>Days Left</th>
|
||||
<th>Owner</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for deploy in deployments %}
|
||||
{% if deploy.enabled == False %}
|
||||
<tr class="danger">
|
||||
{% else %}
|
||||
{% if deploy.warning == True %}
|
||||
<tr class="warning">
|
||||
{% else %}
|
||||
<tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<td data-title="Server">{{ deploy.server.name }}</td>
|
||||
<td data-title="VLAN">{% for vlan in deploy.inv_pubvlans %}{{ vlan.vlan_id }}{% endfor %}</td>
|
||||
<td data-title="Alias"><a class="rrd" data-toggle="tooltip" title="Status: {{ status[deploy.machine_id] }} - Protected: {{ deploy.protected }} - ID: {{ deploy.machine_id }}"><b>{% if status[deploy.machine_id] == 'running' %}<font color="green">{% else %}{% if status[deploy.machine_id] == 'stopped' %}<font color="olive">{% else %}<font color="red">{% endif %}{% endif %}{{ deploy.machine_alias }}</font></b></a></td>
|
||||
<td data-title="CPU">{{ deploy.machine_cpu }}</td>
|
||||
<td data-title="Memory">{{ deploy.machine_mem }} MB</td>
|
||||
<td data-title="HDD">{{ deploy.machine_hdd }} GB</td>
|
||||
{% if deploy.date_last_charge == None %}
|
||||
<td data-title="Last Charged">Never</td>
|
||||
{% else %}
|
||||
<td data-title="Last Charged">{{ moment(deploy.date_last_charge).format('lll') }} ({{ moment(deploy.date_last_charge).fromNow() }})</td>
|
||||
{% endif %}
|
||||
<td data-title="Days Left">{{ deploy.daysleft }}</td>
|
||||
<td data-title="Owner"><a href="{{ url_for('panel.dashboard', user_pid=deploy.user_id) }}">{{ deploy.owner.email }}</a></td>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
55
flask/forest/templates/admin/list_domains.html
Normal file
|
@ -0,0 +1,55 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="row">
|
||||
{% include "admin/admin_tasks.html" %}
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-info" id="domains">
|
||||
<div class="panel-heading">Domains</div>
|
||||
<div class="panel-body"><p>
|
||||
<table class="table table-hover table-striped table-condensed cf">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Owner</th>
|
||||
<th>Name</th>
|
||||
<th>Expiry Date</th>
|
||||
<th>Days Left</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for domain in domains %}
|
||||
{% if domain.enabled == False %}
|
||||
<tr class="danger">
|
||||
{% else %}
|
||||
{% if domain.warning == True %}
|
||||
<tr class="warning">
|
||||
{% else %}
|
||||
<tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<td><a href="{{ url_for('panel.dashboard', user_pid=domain.user_id) }}">{{ domain.owner.email }}</a></td>
|
||||
<td><b><a href="http://{{ domain.fqdn }}">{{ domain.fqdn }}</a></b></td>
|
||||
<td>{{ domain.date_expire }}</td>
|
||||
<td>{{ domain.daysleft }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
72
flask/forest/templates/admin/list_orders.html
Normal file
|
@ -0,0 +1,72 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="row">
|
||||
{% include "admin/admin_tasks.html" %}
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-danger" id="orders">
|
||||
<div class="panel-heading">Orders</div>
|
||||
<div class="panel-body"><p>
|
||||
<div class="no-more-tables">
|
||||
<table class="table table-hover table-striped table-condensed cf">
|
||||
<thead>
|
||||
<tr>
|
||||
<!--<th>Region</th>-->
|
||||
<th>User</th>
|
||||
<th>Region</th>
|
||||
<th>Recipe</th>
|
||||
<th>param 1</th>
|
||||
<th>param 2</th>
|
||||
<th>param 3</th>
|
||||
<th>param 4</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for order in neworders %}
|
||||
<tr>
|
||||
<td data-title="User"><a href="{{ url_for('panel.dashboard', user_pid=order.user_id) }}">{{ order.owner.email }}</a></td>
|
||||
<td data-title="Region">{{ order.region.description }}</td>
|
||||
<td data-title="Recipe"><a href="#" title="{{ order.recipe.description }}">{{ order.recipe.templatefile }}</a></td>
|
||||
<td data-title="Parameter1">{{ order.parameter1 }}</td>
|
||||
<td data-title="Parameter2">{{ order.parameter2 }}</td>
|
||||
<td data-title="Parameter3">{{ order.parameter3 }}</td>
|
||||
<td data-title="Parameter4">{{ order.parameter4 }}</td>
|
||||
<td data-title="Status">{{ order.status }}</td>
|
||||
<td><button class="btn btn-default btn-success" onclick="window.open('{{ url_for('vmanager.vmcreate', orderid=order.pid) }}','_self');"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Confirm</button></td>
|
||||
{% endfor %}
|
||||
|
||||
{% for order in oldorders %}
|
||||
<tr>
|
||||
<td data-title="User"><a href="{{ url_for('panel.dashboard', user_pid=order.user_id) }}">{{ order.owner.email }}</a></td>
|
||||
<td data-title="Region">{{ order.region.description }}</td>
|
||||
<td data-title="Recipe"><a href="#" title="{{ order.recipe.description }}">{{ order.recipe.templatefile }}</a></td>
|
||||
<td data-title="Parameter1">{{ order.parameter1 }}</td>
|
||||
<td data-title="Parameter2">{{ order.parameter2 }}</td>
|
||||
<td data-title="Parameter3">{{ order.parameter3 }}</td>
|
||||
<td data-title="Parameter4">{{ order.parameter4 }}</td>
|
||||
<td data-title="Status">{{ order.status }}</td>
|
||||
<td></td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
53
flask/forest/templates/admin/list_servers.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="row">
|
||||
|
||||
{% include "admin/admin_tasks.html" %}
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">Servers</div>
|
||||
<div class="panel-body">
|
||||
{% include "admin/menu_deployments.html" %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped table-condensed cf">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>CPU</th>
|
||||
<th>MEM</th>
|
||||
<th>HDD</th>
|
||||
<th>Address</th>
|
||||
<th>Region</th>
|
||||
<th>Seller</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for server in servers %}
|
||||
<tr class="default">
|
||||
<td>{{ server.name }}</td>
|
||||
<td>{{ server.cpu }}</td>
|
||||
<td>{{ server.mem }}</td>
|
||||
<td>{{ server.hdd }}</td>
|
||||
<td>{{ server.address }}</td>
|
||||
<td>{{ server.region.name }}</td>
|
||||
<td><a href="{{ url_for('panel.dashboard', user_pid=server.owner.pid) }}">{{ server.owner.email }}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
59
flask/forest/templates/admin/list_services.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="row">
|
||||
{% include "admin/admin_tasks.html" %}
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-info" id="services">
|
||||
<div class="panel-heading">Services</div>
|
||||
<div class="panel-body"><p>
|
||||
<div class="no-more-tables">
|
||||
<table class="table table-hover table-striped table-condensed cf">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Owner</th>
|
||||
<th>Category</th>
|
||||
<th>Description</th>
|
||||
<th>Price</th>
|
||||
<th>Last Charged</th>
|
||||
<th>Days Left</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for service in services %}
|
||||
{% if service.enabled == False %}
|
||||
<tr class="danger">
|
||||
{% else %}
|
||||
{% if service.warning == True %}
|
||||
<tr class="warning">
|
||||
{% else %}
|
||||
<tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<td data-title="Owner"><a href="{{ url_for('panel.dashboard', user_pid=service.user_id) }}">{{ service.owner.email }}</a></td>
|
||||
<td data-title="Category">{{ service.category }}</td>
|
||||
<td data-title="Description">{{ service.description }}</td>
|
||||
<td data-title="Price">{{ service.price }}</td>
|
||||
<td data-title="Last Charged">{{ moment(service.date_last_charge).format('ll') }} ({{ moment(service.date_last_charge).fromNow() }})</td>
|
||||
<td data-title="Days Left">{{ service.daysleft }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
56
flask/forest/templates/admin/list_transactions.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="row">
|
||||
|
||||
{% include "admin/admin_tasks.html" %}
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">All Transactions</div>
|
||||
<div class="panel-body">
|
||||
<div class="no-more-tables">
|
||||
<table class="table table-hover table-striped table-condensed cf">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Description</th>
|
||||
<th>Amount</th>
|
||||
<th>Date</th>
|
||||
<th>User</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for transaction in transactions %}
|
||||
{% if transaction.value > 0 %}
|
||||
<tr class="default">
|
||||
<td data-title="ID">{{ transaction.pid }}</td>
|
||||
<td data-title="Description">{{ transaction.description }}</td>
|
||||
<td data-title="Amount">{{ transaction.value }} {{ transaction.currency }}</td>
|
||||
<td data-title="Date">{{ moment(transaction.date_created).format('lll') }}</td>
|
||||
<td data-title="User"><a href="{{ url_for('admin.transaction', user_pid=transaction.owner.pid) }}">{{ transaction.owner.email }}</a></td>
|
||||
{% else %}
|
||||
<tr class="default">
|
||||
<td data-title="ID">{{ transaction.pid }}</td>
|
||||
<td data-title="Description">{{ transaction.description }}</td>
|
||||
<td data-title="Amount">{{ transaction.value }} {{ transaction.currency }}</td>
|
||||
<td data-title="Date">{{ moment(transaction.date_created).format('lll') }}</td>
|
||||
<td data-title="User"><a href="{{ url_for('admin.transaction', user_pid=transaction.owner.pid) }}">{{ transaction.owner.email }}</a></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if transactions.has_prev %}<a href="{{ url_for('view', page=transactions.prev_num) }}"><< Previous</a>{% else %}<< Previous{% endif %} |
|
||||
{% if transactions.has_next %}<a href="{{ url_for('view', page=transactions.next_num) }}">Next >></a>{% else %}Next >>{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
49
flask/forest/templates/admin/list_users.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="row">
|
||||
{% include "admin/admin_tasks.html" %}
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-info" id="users">
|
||||
<div class="panel-heading">List Active Users</div>
|
||||
<div class="panel-body"><p>
|
||||
<div class="no-more-tables">
|
||||
<table class="table table-hover table-striped table-condensed cf">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>email</td>
|
||||
<td>last seen</td>
|
||||
<td>last ip</td>
|
||||
<td>wallet</td>
|
||||
<td>currency</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for usr in users %}
|
||||
<tr>
|
||||
<td data-title="Email"><font {% if usr.is_administrator() == True %}color="red"{% endif %}>{{ usr.email }}</td>
|
||||
<td data-title="Last Seen">{{ moment(usr.last_seen).format('lll') }}</td>
|
||||
<td data-title="Last IP"><a href="https://apps.db.ripe.net/search/query.html?searchtext={{ usr.last_ip }}" data-toggle="tooltip" title="RIPE Whois Search" target="_blank">{{ usr.last_ip }}</a></td>
|
||||
<td data-title="Wallet">{{ usr.wallet }}</td>
|
||||
<td data-title="Currency">{{ usr.currency }}</td>
|
||||
<td><a href="{{ url_for('admin.charge', user_pid=usr.pid) }}" data-toggle="tooltip" title="Add Funds"><span class="glyphicon glyphicon-plus"></span></a>
|
||||
<a href="{{ url_for('admin.transaction', user_pid=usr.pid) }}" data-toggle="tooltip" title="List Transactions"><span class="glyphicon glyphicon-credit-card"></span></a>
|
||||
<a href="{{ url_for('panel.dashboard', user_pid=usr.pid) }}" data-toggle="tooltip" title="Show Dashboard"><span class="glyphicon glyphicon-modal-window"></span></a>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if users.has_prev %}<a href="{{ url_for('view', page=users.prev_num) }}"><< Previous</a>{% else %}<< Previous{% endif %} |
|
||||
{% if users.has_next %}<a href="{{ url_for('view', page=users.next_num) }}">Next >></a>{% else %}Next >>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
6
flask/forest/templates/admin/menu_cloud.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<button class="btn btn-danger btn-md" onclick="window.open('{{ url_for('admin.list_orders') }}','_self')"><span class="glyphicon glyphicon-bell" aria-hidden="true"></span> Orders</button>
|
||||
<button class="btn btn-success btn-md" onclick="window.open('{{ url_for('admin.list_deployments') }}','_self')"><span class="glyphicon glyphicon-hdd" aria-hidden="true"></span> Deployments</button>
|
||||
<button class="btn btn-success btn-md" onclick="window.open('{{ url_for('admin.list_services') }}','_self')"><span class="glyphicon glyphicon-star" aria-hidden="true"></span> Services</button>
|
||||
<button class="btn btn-primary btn-md" onclick="window.open('{{ url_for('admin.list_users') }}','_self')"><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Users</button>
|
||||
<button class="btn btn-primary btn-md" onclick="window.open('{{ url_for('admin.list_transactions') }}','_self')"><span class="glyphicon glyphicon-btc" aria-hidden="true"></span> Transactions</button>
|
||||
|
4
flask/forest/templates/admin/menu_deployments.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<button class="btn btn-primary btn-md" onclick="window.open('{{ url_for('admin.list_addresses') }}','_self')"><span class="glyphicon glyphicon-tags" aria-hidden="true"></span> Addresses</button>
|
||||
<button class="btn btn-primary btn-md" onclick="window.open('{{ url_for('admin.list_servers') }}','_self')"><span class="glyphicon glyphicon-off" aria-hidden="true"></span> Servers</button>
|
||||
<button class="btn btn-primary btn-md" onclick="window.open('{{ url_for('admin.list_archive') }}','_self')"><span class="glyphicon glyphicon-folder-close" aria-hidden="true"></span> Archive</button>
|
||||
|
15
flask/forest/templates/auth/2fa.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "bootstrap/wtf.html" as wtf %}
|
||||
|
||||
{% block title %}2FA{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="page-header">
|
||||
<h1>Login</h1>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
{{ wtf.quick_form(form) }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
17
flask/forest/templates/auth/already_confirmed.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "bootstrap/wtf.html" as wtf %}
|
||||
|
||||
{% block title %}Вашият акаунт е вече потвърден.{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="page-header">
|
||||
<h3>Вашият акаунт е вече потвърден.</h3>
|
||||
<p>
|
||||
Моля напуснете тази страница :)
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url_for('vmanager.index') }}">Натиснете тук за изход</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
15
flask/forest/templates/auth/change_password.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "bootstrap/wtf.html" as wtf %}
|
||||
|
||||
{% block title %}Change Password{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="page-header">
|
||||
<h3>Change Your Password</h3>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
{{ wtf.quick_form(form) }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
6
flask/forest/templates/auth/email/adm_loginnotify.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<p>{{ user.email }} logged in.<br />
|
||||
<br />
|
||||
IP Address: {{ ipaddr }}<br />
|
||||
</p>
|
||||
<p>Regards,
|
||||
Proxadmin</p>
|
6
flask/forest/templates/auth/email/adm_loginnotify.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
User {{ user.email }} logged in.
|
||||
|
||||
IP Address: {{ ipaddr }}
|
||||
|
||||
Regards,
|
||||
Proxadmin
|
6
flask/forest/templates/auth/email/adm_regnotify.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
New user {{ user.email }} has been registered.
|
||||
|
||||
IP Address: {{ ipaddr }}
|
||||
|
||||
Regards,
|
||||
Proxadmin
|
6
flask/forest/templates/auth/email/adm_regnotify.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
New user {{ user.email }} has been registered.
|
||||
|
||||
IP Address: {{ ipaddr }}
|
||||
|
||||
Regards,
|
||||
Proxadmin
|
7
flask/forest/templates/auth/email/confirm.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<p>Dear Customer,</p>
|
||||
<p>To confirm your account please <a href="{{ url_for('auth.confirm', token=token, _external=True) }}">click here</a>.</p>
|
||||
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
|
||||
<p>{{ url_for('auth.confirm', token=token, _external=True) }}</p>
|
||||
<p>Sincerely,</p>
|
||||
<p>Datapoint.bg</p>
|
||||
<p><small>Note: replies to this email address are not monitored.</small></p>
|
13
flask/forest/templates/auth/email/confirm.txt
Normal file
|
@ -0,0 +1,13 @@
|
|||
Dear Customer,
|
||||
|
||||
Welcome to Datapoint!
|
||||
|
||||
To confirm your account please click on the following link:
|
||||
|
||||
{{ url_for('auth.confirm', token=token, _external=True) }}
|
||||
|
||||
Sincerely,
|
||||
|
||||
Datapoint.bg
|
||||
|
||||
Note: replies to this email address are not monitored.
|
5
flask/forest/templates/auth/email/reset_password.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
<p>Dear {{ user.username }},</p>
|
||||
<p>To reset your password <a href="{{ url_for('auth.password_reset', token=token, _external=True) }}">click here</a>.</p>
|
||||
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
|
||||
<p>If you have not requested a password reset simply ignore this message.</p>
|
||||
<p><small>Note: replies to this email address are not monitored.</small></p>
|