diff --git a/flask/forest/__init__.py b/flask/forest/__init__.py index e3f93bf..32bf643 100644 --- a/flask/forest/__init__.py +++ b/flask/forest/__init__.py @@ -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/") -def staticfiles(filename): - return send_from_directory(app.config["STATIC_FOLDER"], filename) - - -@app.route("/media/") -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 """ - - upload new File -
-

-

- """ +if __name__ == '__main__': + app.run() diff --git a/flask/forest/admin/__init__.py b/flask/forest/admin/__init__.py new file mode 100644 index 0000000..f933ea1 --- /dev/null +++ b/flask/forest/admin/__init__.py @@ -0,0 +1,3 @@ +from flask import Blueprint +admin = Blueprint('admin', __name__) +from . import routes diff --git a/flask/forest/admin/forms.py b/flask/forest/admin/forms.py new file mode 100644 index 0000000..1d13e70 --- /dev/null +++ b/flask/forest/admin/forms.py @@ -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') + diff --git a/flask/forest/admin/routes.py b/flask/forest/admin/routes.py new file mode 100644 index 0000000..d12e167 --- /dev/null +++ b/flask/forest/admin/routes.py @@ -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/", 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/", 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/", 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/", 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) + diff --git a/flask/forest/auth/__init__.py b/flask/forest/auth/__init__.py new file mode 100644 index 0000000..3785107 --- /dev/null +++ b/flask/forest/auth/__init__.py @@ -0,0 +1,4 @@ +from flask import Blueprint +auth = Blueprint('auth', __name__) +from . import routes + diff --git a/flask/forest/auth/forms.py b/flask/forest/auth/forms.py new file mode 100644 index 0000000..2bcc6e3 --- /dev/null +++ b/flask/forest/auth/forms.py @@ -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 Terms of Service', [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.') + diff --git a/flask/forest/auth/routes.py b/flask/forest/auth/routes.py new file mode 100644 index 0000000..5d76713 --- /dev/null +++ b/flask/forest/auth/routes.py @@ -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/') +@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/', 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) + diff --git a/flask/forest/decorators.py b/flask/forest/decorators.py new file mode 100644 index 0000000..c3217c1 --- /dev/null +++ b/flask/forest/decorators.py @@ -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) + diff --git a/flask/forest/email.py b/flask/forest/email.py new file mode 100644 index 0000000..f0d7c49 --- /dev/null +++ b/flask/forest/email.py @@ -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 + diff --git a/flask/forest/exceptions.py b/flask/forest/exceptions.py new file mode 100644 index 0000000..2851fa7 --- /dev/null +++ b/flask/forest/exceptions.py @@ -0,0 +1,2 @@ +class ValidationError(ValueError): + pass diff --git a/flask/forest/init.py b/flask/forest/init.py new file mode 100644 index 0000000..e3f93bf --- /dev/null +++ b/flask/forest/init.py @@ -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/") +def staticfiles(filename): + return send_from_directory(app.config["STATIC_FOLDER"], filename) + + +@app.route("/media/") +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 """ + + upload new File +
+

+

+ """ diff --git a/flask/forest/models.py b/flask/forest/models.py new file mode 100644 index 0000000..24af7d0 --- /dev/null +++ b/flask/forest/models.py @@ -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 '' % 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 '' % 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) diff --git a/flask/forest/panel/__init__.py b/flask/forest/panel/__init__.py new file mode 100644 index 0000000..537a5f1 --- /dev/null +++ b/flask/forest/panel/__init__.py @@ -0,0 +1,3 @@ +from flask import Blueprint +panel = Blueprint('panel', __name__) +from . import routes diff --git a/flask/forest/panel/forms.py b/flask/forest/panel/forms.py new file mode 100644 index 0000000..ee6f96f --- /dev/null +++ b/flask/forest/panel/forms.py @@ -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') + diff --git a/flask/forest/panel/routes.py b/flask/forest/panel/routes.py new file mode 100644 index 0000000..6851e5b --- /dev/null +++ b/flask/forest/panel/routes.py @@ -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/", 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//", 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')) + diff --git a/flask/forest/settings/__init__.py b/flask/forest/settings/__init__.py new file mode 100644 index 0000000..709726b --- /dev/null +++ b/flask/forest/settings/__init__.py @@ -0,0 +1,3 @@ +from flask import Blueprint +settings = Blueprint('settings', __name__) +from . import routes diff --git a/flask/forest/settings/forms.py b/flask/forest/settings/forms.py new file mode 100644 index 0000000..c205426 --- /dev/null +++ b/flask/forest/settings/forms.py @@ -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-а е вече регистриран.') + + diff --git a/flask/forest/settings/routes.py b/flask/forest/settings/routes.py new file mode 100644 index 0000000..aa710e7 --- /dev/null +++ b/flask/forest/settings/routes.py @@ -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) + diff --git a/flask/forest/static/css/bootstrap-slider.css b/flask/forest/static/css/bootstrap-slider.css new file mode 100644 index 0000000..085291e --- /dev/null +++ b/flask/forest/static/css/bootstrap-slider.css @@ -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; +} diff --git a/flask/forest/static/css/login.css b/flask/forest/static/css/login.css new file mode 100644 index 0000000..d9a030c --- /dev/null +++ b/flask/forest/static/css/login.css @@ -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; +} diff --git a/flask/forest/static/css/login.css-dist b/flask/forest/static/css/login.css-dist new file mode 100644 index 0000000..e79db51 --- /dev/null +++ b/flask/forest/static/css/login.css-dist @@ -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; +} diff --git a/flask/forest/static/css/navbar.css b/flask/forest/static/css/navbar.css new file mode 100644 index 0000000..c39f09a --- /dev/null +++ b/flask/forest/static/css/navbar.css @@ -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; + } +} diff --git a/flask/forest/static/css/no-more-tables.css b/flask/forest/static/css/no-more-tables.css new file mode 100644 index 0000000..1cc0443 --- /dev/null +++ b/flask/forest/static/css/no-more-tables.css @@ -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); } + } diff --git a/flask/forest/static/css/nouislider.css b/flask/forest/static/css/nouislider.css new file mode 100644 index 0000000..16c27d4 --- /dev/null +++ b/flask/forest/static/css/nouislider.css @@ -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%; +} diff --git a/flask/forest/static/css/panel-transparent.css b/flask/forest/static/css/panel-transparent.css new file mode 100644 index 0000000..1b66021 --- /dev/null +++ b/flask/forest/static/css/panel-transparent.css @@ -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; +} diff --git a/flask/forest/static/css/range.css b/flask/forest/static/css/range.css new file mode 100644 index 0000000..de5b9eb --- /dev/null +++ b/flask/forest/static/css/range.css @@ -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; +} + diff --git a/flask/forest/static/css/simple-slideshow-styles.css b/flask/forest/static/css/simple-slideshow-styles.css new file mode 100644 index 0000000..ba4f664 --- /dev/null +++ b/flask/forest/static/css/simple-slideshow-styles.css @@ -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; +} diff --git a/flask/forest/static/css/style.css b/flask/forest/static/css/style.css new file mode 100644 index 0000000..09c438c --- /dev/null +++ b/flask/forest/static/css/style.css @@ -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; +} diff --git a/flask/forest/static/images/220x180.png b/flask/forest/static/images/220x180.png new file mode 100644 index 0000000..d1147ba Binary files /dev/null and b/flask/forest/static/images/220x180.png differ diff --git a/flask/forest/static/images/VPS-Mission.png b/flask/forest/static/images/VPS-Mission.png new file mode 100644 index 0000000..ff3c09f Binary files /dev/null and b/flask/forest/static/images/VPS-Mission.png differ diff --git a/flask/forest/static/images/VPS-Security.png b/flask/forest/static/images/VPS-Security.png new file mode 100644 index 0000000..c557fcb Binary files /dev/null and b/flask/forest/static/images/VPS-Security.png differ diff --git a/flask/forest/static/images/VPS-Support.png b/flask/forest/static/images/VPS-Support.png new file mode 100644 index 0000000..f4429b8 Binary files /dev/null and b/flask/forest/static/images/VPS-Support.png differ diff --git a/flask/forest/static/images/VPS-equipment.png b/flask/forest/static/images/VPS-equipment.png new file mode 100644 index 0000000..114aebf Binary files /dev/null and b/flask/forest/static/images/VPS-equipment.png differ diff --git a/flask/forest/static/images/_bg.jpg b/flask/forest/static/images/_bg.jpg new file mode 100644 index 0000000..1c9bed5 Binary files /dev/null and b/flask/forest/static/images/_bg.jpg differ diff --git a/flask/forest/static/images/arrows-alt_ffffff_64.png b/flask/forest/static/images/arrows-alt_ffffff_64.png new file mode 100644 index 0000000..e3f6779 Binary files /dev/null and b/flask/forest/static/images/arrows-alt_ffffff_64.png differ diff --git a/flask/forest/static/images/bg-linear.jpg b/flask/forest/static/images/bg-linear.jpg new file mode 100644 index 0000000..2282ad7 Binary files /dev/null and b/flask/forest/static/images/bg-linear.jpg differ diff --git a/flask/forest/static/images/bg.jpg b/flask/forest/static/images/bg.jpg new file mode 100644 index 0000000..1c9bed5 Binary files /dev/null and b/flask/forest/static/images/bg.jpg differ diff --git a/flask/forest/static/images/cloudsbg.jpeg b/flask/forest/static/images/cloudsbg.jpeg new file mode 100644 index 0000000..eeadeff Binary files /dev/null and b/flask/forest/static/images/cloudsbg.jpeg differ diff --git a/flask/forest/static/images/cloudsbg.jpeg_disabled b/flask/forest/static/images/cloudsbg.jpeg_disabled new file mode 100644 index 0000000..dd70580 Binary files /dev/null and b/flask/forest/static/images/cloudsbg.jpeg_disabled differ diff --git a/flask/forest/static/images/compress_ffffff_64.png b/flask/forest/static/images/compress_ffffff_64.png new file mode 100644 index 0000000..54cfc9b Binary files /dev/null and b/flask/forest/static/images/compress_ffffff_64.png differ diff --git a/flask/forest/static/images/createvm.gif b/flask/forest/static/images/createvm.gif new file mode 100644 index 0000000..8369875 Binary files /dev/null and b/flask/forest/static/images/createvm.gif differ diff --git a/flask/forest/static/images/datapoint-simple-logo.png b/flask/forest/static/images/datapoint-simple-logo.png new file mode 100644 index 0000000..fcd8303 Binary files /dev/null and b/flask/forest/static/images/datapoint-simple-logo.png differ diff --git a/flask/forest/static/images/datapoint.png b/flask/forest/static/images/datapoint.png new file mode 100644 index 0000000..6c58a00 Binary files /dev/null and b/flask/forest/static/images/datapoint.png differ diff --git a/flask/forest/static/images/fb.png b/flask/forest/static/images/fb.png new file mode 100644 index 0000000..edf4393 Binary files /dev/null and b/flask/forest/static/images/fb.png differ diff --git a/flask/forest/static/images/footer_cols_divider.png b/flask/forest/static/images/footer_cols_divider.png new file mode 100644 index 0000000..10ca2b3 Binary files /dev/null and b/flask/forest/static/images/footer_cols_divider.png differ diff --git a/flask/forest/static/images/footer_header_bg.png b/flask/forest/static/images/footer_header_bg.png new file mode 100644 index 0000000..d4564f2 Binary files /dev/null and b/flask/forest/static/images/footer_header_bg.png differ diff --git a/flask/forest/static/images/footer_newsletter_input.png b/flask/forest/static/images/footer_newsletter_input.png new file mode 100644 index 0000000..59b5db3 Binary files /dev/null and b/flask/forest/static/images/footer_newsletter_input.png differ diff --git a/flask/forest/static/images/footer_newsletter_input_btn.png b/flask/forest/static/images/footer_newsletter_input_btn.png new file mode 100644 index 0000000..b86711b Binary files /dev/null and b/flask/forest/static/images/footer_newsletter_input_btn.png differ diff --git a/flask/forest/static/images/footer_top_bg.png b/flask/forest/static/images/footer_top_bg.png new file mode 100644 index 0000000..f0ee10b Binary files /dev/null and b/flask/forest/static/images/footer_top_bg.png differ diff --git a/flask/forest/static/images/header-layer.jpg b/flask/forest/static/images/header-layer.jpg new file mode 100644 index 0000000..499d247 Binary files /dev/null and b/flask/forest/static/images/header-layer.jpg differ diff --git a/flask/forest/static/images/hex24.png b/flask/forest/static/images/hex24.png new file mode 100644 index 0000000..c21fdcd Binary files /dev/null and b/flask/forest/static/images/hex24.png differ diff --git a/flask/forest/static/images/hex32.png b/flask/forest/static/images/hex32.png new file mode 100644 index 0000000..1075279 Binary files /dev/null and b/flask/forest/static/images/hex32.png differ diff --git a/flask/forest/static/images/hex512.png b/flask/forest/static/images/hex512.png new file mode 100644 index 0000000..0cb65b8 Binary files /dev/null and b/flask/forest/static/images/hex512.png differ diff --git a/flask/forest/static/images/panel/icons8-administrative-tools-100.png b/flask/forest/static/images/panel/icons8-administrative-tools-100.png new file mode 100644 index 0000000..7c9d285 Binary files /dev/null and b/flask/forest/static/images/panel/icons8-administrative-tools-100.png differ diff --git a/flask/forest/static/images/panel/icons8-processor-40.png b/flask/forest/static/images/panel/icons8-processor-40.png new file mode 100644 index 0000000..edc8b03 Binary files /dev/null and b/flask/forest/static/images/panel/icons8-processor-40.png differ diff --git a/flask/forest/static/images/purplebg.jpg b/flask/forest/static/images/purplebg.jpg new file mode 100644 index 0000000..ffb5c68 Binary files /dev/null and b/flask/forest/static/images/purplebg.jpg differ diff --git a/flask/forest/static/images/purplebg1.jpg b/flask/forest/static/images/purplebg1.jpg new file mode 100644 index 0000000..f139038 Binary files /dev/null and b/flask/forest/static/images/purplebg1.jpg differ diff --git a/flask/forest/static/images/purplebg2.jpg b/flask/forest/static/images/purplebg2.jpg new file mode 100644 index 0000000..ffb5c68 Binary files /dev/null and b/flask/forest/static/images/purplebg2.jpg differ diff --git a/flask/forest/static/images/server.png b/flask/forest/static/images/server.png new file mode 100644 index 0000000..f687023 Binary files /dev/null and b/flask/forest/static/images/server.png differ diff --git a/flask/forest/static/images/srv.png b/flask/forest/static/images/srv.png new file mode 100644 index 0000000..9c6a744 Binary files /dev/null and b/flask/forest/static/images/srv.png differ diff --git a/flask/forest/static/images/texture-diagonal.png b/flask/forest/static/images/texture-diagonal.png new file mode 100644 index 0000000..f262725 Binary files /dev/null and b/flask/forest/static/images/texture-diagonal.png differ diff --git a/flask/forest/static/js/better-simple-slideshow.js b/flask/forest/static/js/better-simple-slideshow.js new file mode 100644 index 0000000..7fdfb9d --- /dev/null +++ b/flask/forest/static/js/better-simple-slideshow.js @@ -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); + }); +}; diff --git a/flask/forest/static/js/bootstrap-slider.js b/flask/forest/static/js/bootstrap-slider.js new file mode 100644 index 0000000..bb46283 --- /dev/null +++ b/flask/forest/static/js/bootstrap-slider.js @@ -0,0 +1,1807 @@ +/*! ======================================================= + VERSION 9.4.1 +========================================================= */ +"use strict"; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; + +/*! ========================================================= + * 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. + * + * ========================================================= */ + +/** + * Bridget makes jQuery widgets + * v1.0.1 + * MIT license + */ +var windowIsDefined = (typeof window === "undefined" ? "undefined" : _typeof(window)) === "object"; + +(function (factory) { + if (typeof define === "function" && define.amd) { + define(["jquery"], factory); + } else if ((typeof module === "undefined" ? "undefined" : _typeof(module)) === "object" && module.exports) { + var jQuery; + try { + jQuery = require("jquery"); + } catch (err) { + jQuery = null; + } + module.exports = factory(jQuery); + } else if (window) { + window.Slider = factory(window.jQuery); + } +})(function ($) { + // Constants + var NAMESPACE_MAIN = 'slider'; + var NAMESPACE_ALTERNATE = 'bootstrapSlider'; + + // Polyfill console methods + if (windowIsDefined && !window.console) { + window.console = {}; + } + if (windowIsDefined && !window.console.log) { + window.console.log = function () {}; + } + if (windowIsDefined && !window.console.warn) { + window.console.warn = function () {}; + } + + // Reference to Slider constructor + var Slider; + + (function ($) { + + 'use strict'; + + // -------------------------- utils -------------------------- // + + var slice = Array.prototype.slice; + + function noop() {} + + // -------------------------- definition -------------------------- // + + function defineBridget($) { + + // bail if no jQuery + if (!$) { + return; + } + + // -------------------------- addOptionMethod -------------------------- // + + /** + * adds option method -> $().plugin('option', {...}) + * @param {Function} PluginClass - constructor class + */ + function addOptionMethod(PluginClass) { + // don't overwrite original option method + if (PluginClass.prototype.option) { + return; + } + + // option setter + PluginClass.prototype.option = function (opts) { + // bail out if not an object + if (!$.isPlainObject(opts)) { + return; + } + this.options = $.extend(true, this.options, opts); + }; + } + + // -------------------------- plugin bridge -------------------------- // + + // helper function for logging errors + // $.error breaks jQuery chaining + var logError = typeof console === 'undefined' ? noop : function (message) { + console.error(message); + }; + + /** + * jQuery plugin bridge, access methods like $elem.plugin('method') + * @param {String} namespace - plugin name + * @param {Function} PluginClass - constructor class + */ + function bridge(namespace, PluginClass) { + // add to jQuery fn namespace + $.fn[namespace] = function (options) { + if (typeof options === 'string') { + // call plugin method when first argument is a string + // get arguments for method + var args = slice.call(arguments, 1); + + for (var i = 0, len = this.length; i < len; i++) { + var elem = this[i]; + var instance = $.data(elem, namespace); + if (!instance) { + logError("cannot call methods on " + namespace + " prior to initialization; " + "attempted to call '" + options + "'"); + continue; + } + if (!$.isFunction(instance[options]) || options.charAt(0) === '_') { + logError("no such method '" + options + "' for " + namespace + " instance"); + continue; + } + + // trigger method with arguments + var returnValue = instance[options].apply(instance, args); + + // break look and return first value if provided + if (returnValue !== undefined && returnValue !== instance) { + return returnValue; + } + } + // return this if no return value + return this; + } else { + var objects = this.map(function () { + var instance = $.data(this, namespace); + if (instance) { + // apply options & init + instance.option(options); + instance._init(); + } else { + // initialize new instance + instance = new PluginClass(this, options); + $.data(this, namespace, instance); + } + return $(this); + }); + + if (!objects || objects.length > 1) { + return objects; + } else { + return objects[0]; + } + } + }; + } + + // -------------------------- bridget -------------------------- // + + /** + * converts a Prototypical class into a proper jQuery plugin + * the class must have a ._init method + * @param {String} namespace - plugin name, used in $().pluginName + * @param {Function} PluginClass - constructor class + */ + $.bridget = function (namespace, PluginClass) { + addOptionMethod(PluginClass); + bridge(namespace, PluginClass); + }; + + return $.bridget; + } + + // get jquery from browser global + defineBridget($); + })($); + + /************************************************* + BOOTSTRAP-SLIDER SOURCE CODE + **************************************************/ + + (function ($) { + + var ErrorMsgs = { + formatInvalidInputErrorMsg: function formatInvalidInputErrorMsg(input) { + return "Invalid input value '" + input + "' passed in"; + }, + callingContextNotSliderInstance: "Calling context element does not have instance of Slider bound to it. Check your code to make sure the JQuery object returned from the call to the slider() initializer is calling the method" + }; + + var SliderScale = { + linear: { + toValue: function toValue(percentage) { + var rawValue = percentage / 100 * (this.options.max - this.options.min); + var shouldAdjustWithBase = true; + if (this.options.ticks_positions.length > 0) { + var minv, + maxv, + minp, + maxp = 0; + for (var i = 1; i < this.options.ticks_positions.length; i++) { + if (percentage <= this.options.ticks_positions[i]) { + minv = this.options.ticks[i - 1]; + minp = this.options.ticks_positions[i - 1]; + maxv = this.options.ticks[i]; + maxp = this.options.ticks_positions[i]; + + break; + } + } + var partialPercentage = (percentage - minp) / (maxp - minp); + rawValue = minv + partialPercentage * (maxv - minv); + shouldAdjustWithBase = false; + } + + var adjustment = shouldAdjustWithBase ? this.options.min : 0; + var value = adjustment + Math.round(rawValue / this.options.step) * this.options.step; + if (value < this.options.min) { + return this.options.min; + } else if (value > this.options.max) { + return this.options.max; + } else { + return value; + } + }, + toPercentage: function toPercentage(value) { + if (this.options.max === this.options.min) { + return 0; + } + + if (this.options.ticks_positions.length > 0) { + var minv, + maxv, + minp, + maxp = 0; + for (var i = 0; i < this.options.ticks.length; i++) { + if (value <= this.options.ticks[i]) { + minv = i > 0 ? this.options.ticks[i - 1] : 0; + minp = i > 0 ? this.options.ticks_positions[i - 1] : 0; + maxv = this.options.ticks[i]; + maxp = this.options.ticks_positions[i]; + + break; + } + } + if (i > 0) { + var partialPercentage = (value - minv) / (maxv - minv); + return minp + partialPercentage * (maxp - minp); + } + } + + return 100 * (value - this.options.min) / (this.options.max - this.options.min); + } + }, + + logarithmic: { + /* Based on http://stackoverflow.com/questions/846221/logarithmic-slider */ + toValue: function toValue(percentage) { + var min = this.options.min === 0 ? 0 : Math.log(this.options.min); + var max = Math.log(this.options.max); + var value = Math.exp(min + (max - min) * percentage / 100); + value = this.options.min + Math.round((value - this.options.min) / this.options.step) * this.options.step; + /* Rounding to the nearest step could exceed the min or + * max, so clip to those values. */ + if (value < this.options.min) { + return this.options.min; + } else if (value > this.options.max) { + return this.options.max; + } else { + return value; + } + }, + toPercentage: function toPercentage(value) { + if (this.options.max === this.options.min) { + return 0; + } else { + var max = Math.log(this.options.max); + var min = this.options.min === 0 ? 0 : Math.log(this.options.min); + var v = value === 0 ? 0 : Math.log(value); + return 100 * (v - min) / (max - min); + } + } + } + }; + + /************************************************* + CONSTRUCTOR + **************************************************/ + Slider = function Slider(element, options) { + createNewSlider.call(this, element, options); + return this; + }; + + function createNewSlider(element, options) { + + /* + The internal state object is used to store data about the current 'state' of slider. + This includes values such as the `value`, `enabled`, etc... + */ + this._state = { + value: null, + enabled: null, + offset: null, + size: null, + percentage: null, + inDrag: false, + over: false + }; + + // The objects used to store the reference to the tick methods if ticks_tooltip is on + this.ticksCallbackMap = {}; + this.handleCallbackMap = {}; + + if (typeof element === "string") { + this.element = document.querySelector(element); + } else if (element instanceof HTMLElement) { + this.element = element; + } + + /************************************************* + Process Options + **************************************************/ + options = options ? options : {}; + var optionTypes = Object.keys(this.defaultOptions); + + for (var i = 0; i < optionTypes.length; i++) { + var optName = optionTypes[i]; + + // First check if an option was passed in via the constructor + var val = options[optName]; + // If no data attrib, then check data atrributes + val = typeof val !== 'undefined' ? val : getDataAttrib(this.element, optName); + // Finally, if nothing was specified, use the defaults + val = val !== null ? val : this.defaultOptions[optName]; + + // Set all options on the instance of the Slider + if (!this.options) { + this.options = {}; + } + this.options[optName] = val; + } + + /* + Validate `tooltip_position` against 'orientation` + - if `tooltip_position` is incompatible with orientation, swith it to a default compatible with specified `orientation` + -- default for "vertical" -> "right" + -- default for "horizontal" -> "left" + */ + if (this.options.orientation === "vertical" && (this.options.tooltip_position === "top" || this.options.tooltip_position === "bottom")) { + + this.options.tooltip_position = "right"; + } else if (this.options.orientation === "horizontal" && (this.options.tooltip_position === "left" || this.options.tooltip_position === "right")) { + + this.options.tooltip_position = "top"; + } + + function getDataAttrib(element, optName) { + var dataName = "data-slider-" + optName.replace(/_/g, '-'); + var dataValString = element.getAttribute(dataName); + + try { + return JSON.parse(dataValString); + } catch (err) { + return dataValString; + } + } + + /************************************************* + Create Markup + **************************************************/ + + var origWidth = this.element.style.width; + var updateSlider = false; + var parent = this.element.parentNode; + var sliderTrackSelection; + var sliderTrackLow, sliderTrackHigh; + var sliderMinHandle; + var sliderMaxHandle; + + if (this.sliderElem) { + updateSlider = true; + } else { + /* Create elements needed for slider */ + this.sliderElem = document.createElement("div"); + this.sliderElem.className = "slider"; + + /* Create slider track elements */ + var sliderTrack = document.createElement("div"); + sliderTrack.className = "slider-track"; + + sliderTrackLow = document.createElement("div"); + sliderTrackLow.className = "slider-track-low"; + + sliderTrackSelection = document.createElement("div"); + sliderTrackSelection.className = "slider-selection"; + + sliderTrackHigh = document.createElement("div"); + sliderTrackHigh.className = "slider-track-high"; + + sliderMinHandle = document.createElement("div"); + sliderMinHandle.className = "slider-handle min-slider-handle"; + sliderMinHandle.setAttribute('role', 'slider'); + sliderMinHandle.setAttribute('aria-valuemin', this.options.min); + sliderMinHandle.setAttribute('aria-valuemax', this.options.max); + + sliderMaxHandle = document.createElement("div"); + sliderMaxHandle.className = "slider-handle max-slider-handle"; + sliderMaxHandle.setAttribute('role', 'slider'); + sliderMaxHandle.setAttribute('aria-valuemin', this.options.min); + sliderMaxHandle.setAttribute('aria-valuemax', this.options.max); + + sliderTrack.appendChild(sliderTrackLow); + sliderTrack.appendChild(sliderTrackSelection); + sliderTrack.appendChild(sliderTrackHigh); + + /* Create highlight range elements */ + this.rangeHighlightElements = []; + if (Array.isArray(this.options.rangeHighlights) && this.options.rangeHighlights.length > 0) { + for (var j = 0; j < this.options.rangeHighlights.length; j++) { + + var rangeHighlightElement = document.createElement("div"); + rangeHighlightElement.className = "slider-rangeHighlight slider-selection"; + + this.rangeHighlightElements.push(rangeHighlightElement); + sliderTrack.appendChild(rangeHighlightElement); + } + } + + /* Add aria-labelledby to handle's */ + var isLabelledbyArray = Array.isArray(this.options.labelledby); + if (isLabelledbyArray && this.options.labelledby[0]) { + sliderMinHandle.setAttribute('aria-labelledby', this.options.labelledby[0]); + } + if (isLabelledbyArray && this.options.labelledby[1]) { + sliderMaxHandle.setAttribute('aria-labelledby', this.options.labelledby[1]); + } + if (!isLabelledbyArray && this.options.labelledby) { + sliderMinHandle.setAttribute('aria-labelledby', this.options.labelledby); + sliderMaxHandle.setAttribute('aria-labelledby', this.options.labelledby); + } + + /* Create ticks */ + this.ticks = []; + if (Array.isArray(this.options.ticks) && this.options.ticks.length > 0) { + this.ticksContainer = document.createElement('div'); + this.ticksContainer.className = 'slider-tick-container'; + + for (i = 0; i < this.options.ticks.length; i++) { + var tick = document.createElement('div'); + tick.className = 'slider-tick'; + if (this.options.ticks_tooltip) { + var tickListenerReference = this._addTickListener(); + var enterCallback = tickListenerReference.addMouseEnter(this, tick, i); + var leaveCallback = tickListenerReference.addMouseLeave(this, tick); + + this.ticksCallbackMap[i] = { + mouseEnter: enterCallback, + mouseLeave: leaveCallback + }; + } + this.ticks.push(tick); + this.ticksContainer.appendChild(tick); + } + + sliderTrackSelection.className += " tick-slider-selection"; + } + + this.tickLabels = []; + if (Array.isArray(this.options.ticks_labels) && this.options.ticks_labels.length > 0) { + this.tickLabelContainer = document.createElement('div'); + this.tickLabelContainer.className = 'slider-tick-label-container'; + + for (i = 0; i < this.options.ticks_labels.length; i++) { + var label = document.createElement('div'); + var noTickPositionsSpecified = this.options.ticks_positions.length === 0; + var tickLabelsIndex = this.options.reversed && noTickPositionsSpecified ? this.options.ticks_labels.length - (i + 1) : i; + label.className = 'slider-tick-label'; + label.innerHTML = this.options.ticks_labels[tickLabelsIndex]; + + this.tickLabels.push(label); + this.tickLabelContainer.appendChild(label); + } + } + + var createAndAppendTooltipSubElements = function createAndAppendTooltipSubElements(tooltipElem) { + var arrow = document.createElement("div"); + arrow.className = "tooltip-arrow"; + + var inner = document.createElement("div"); + inner.className = "tooltip-inner"; + + tooltipElem.appendChild(arrow); + tooltipElem.appendChild(inner); + }; + + /* Create tooltip elements */ + var sliderTooltip = document.createElement("div"); + sliderTooltip.className = "tooltip tooltip-main"; + sliderTooltip.setAttribute('role', 'presentation'); + createAndAppendTooltipSubElements(sliderTooltip); + + var sliderTooltipMin = document.createElement("div"); + sliderTooltipMin.className = "tooltip tooltip-min"; + sliderTooltipMin.setAttribute('role', 'presentation'); + createAndAppendTooltipSubElements(sliderTooltipMin); + + var sliderTooltipMax = document.createElement("div"); + sliderTooltipMax.className = "tooltip tooltip-max"; + sliderTooltipMax.setAttribute('role', 'presentation'); + createAndAppendTooltipSubElements(sliderTooltipMax); + + /* Append components to sliderElem */ + this.sliderElem.appendChild(sliderTrack); + this.sliderElem.appendChild(sliderTooltip); + this.sliderElem.appendChild(sliderTooltipMin); + this.sliderElem.appendChild(sliderTooltipMax); + + if (this.tickLabelContainer) { + this.sliderElem.appendChild(this.tickLabelContainer); + } + if (this.ticksContainer) { + this.sliderElem.appendChild(this.ticksContainer); + } + + this.sliderElem.appendChild(sliderMinHandle); + this.sliderElem.appendChild(sliderMaxHandle); + + /* Append slider element to parent container, right before the original element */ + parent.insertBefore(this.sliderElem, this.element); + + /* Hide original element */ + this.element.style.display = "none"; + } + /* If JQuery exists, cache JQ references */ + if ($) { + this.$element = $(this.element); + this.$sliderElem = $(this.sliderElem); + } + + /************************************************* + Setup + **************************************************/ + this.eventToCallbackMap = {}; + this.sliderElem.id = this.options.id; + + this.touchCapable = 'ontouchstart' in window || window.DocumentTouch && document instanceof window.DocumentTouch; + + this.touchX = 0; + this.touchY = 0; + + this.tooltip = this.sliderElem.querySelector('.tooltip-main'); + this.tooltipInner = this.tooltip.querySelector('.tooltip-inner'); + + this.tooltip_min = this.sliderElem.querySelector('.tooltip-min'); + this.tooltipInner_min = this.tooltip_min.querySelector('.tooltip-inner'); + + this.tooltip_max = this.sliderElem.querySelector('.tooltip-max'); + this.tooltipInner_max = this.tooltip_max.querySelector('.tooltip-inner'); + + if (SliderScale[this.options.scale]) { + this.options.scale = SliderScale[this.options.scale]; + } + + if (updateSlider === true) { + // Reset classes + this._removeClass(this.sliderElem, 'slider-horizontal'); + this._removeClass(this.sliderElem, 'slider-vertical'); + this._removeClass(this.tooltip, 'hide'); + this._removeClass(this.tooltip_min, 'hide'); + this._removeClass(this.tooltip_max, 'hide'); + + // Undo existing inline styles for track + ["left", "top", "width", "height"].forEach(function (prop) { + this._removeProperty(this.trackLow, prop); + this._removeProperty(this.trackSelection, prop); + this._removeProperty(this.trackHigh, prop); + }, this); + + // Undo inline styles on handles + [this.handle1, this.handle2].forEach(function (handle) { + this._removeProperty(handle, 'left'); + this._removeProperty(handle, 'top'); + }, this); + + // Undo inline styles and classes on tooltips + [this.tooltip, this.tooltip_min, this.tooltip_max].forEach(function (tooltip) { + this._removeProperty(tooltip, 'left'); + this._removeProperty(tooltip, 'top'); + this._removeProperty(tooltip, 'margin-left'); + this._removeProperty(tooltip, 'margin-top'); + + this._removeClass(tooltip, 'right'); + this._removeClass(tooltip, 'top'); + }, this); + } + + if (this.options.orientation === 'vertical') { + this._addClass(this.sliderElem, 'slider-vertical'); + this.stylePos = 'top'; + this.mousePos = 'pageY'; + this.sizePos = 'offsetHeight'; + } else { + this._addClass(this.sliderElem, 'slider-horizontal'); + this.sliderElem.style.width = origWidth; + this.options.orientation = 'horizontal'; + this.stylePos = 'left'; + this.mousePos = 'pageX'; + this.sizePos = 'offsetWidth'; + } + this._setTooltipPosition(); + /* In case ticks are specified, overwrite the min and max bounds */ + if (Array.isArray(this.options.ticks) && this.options.ticks.length > 0) { + this.options.max = Math.max.apply(Math, this.options.ticks); + this.options.min = Math.min.apply(Math, this.options.ticks); + } + + if (Array.isArray(this.options.value)) { + this.options.range = true; + this._state.value = this.options.value; + } else if (this.options.range) { + // User wants a range, but value is not an array + this._state.value = [this.options.value, this.options.max]; + } else { + this._state.value = this.options.value; + } + + this.trackLow = sliderTrackLow || this.trackLow; + this.trackSelection = sliderTrackSelection || this.trackSelection; + this.trackHigh = sliderTrackHigh || this.trackHigh; + + if (this.options.selection === 'none') { + this._addClass(this.trackLow, 'hide'); + this._addClass(this.trackSelection, 'hide'); + this._addClass(this.trackHigh, 'hide'); + } + + this.handle1 = sliderMinHandle || this.handle1; + this.handle2 = sliderMaxHandle || this.handle2; + + if (updateSlider === true) { + // Reset classes + this._removeClass(this.handle1, 'round triangle'); + this._removeClass(this.handle2, 'round triangle hide'); + + for (i = 0; i < this.ticks.length; i++) { + this._removeClass(this.ticks[i], 'round triangle hide'); + } + } + + var availableHandleModifiers = ['round', 'triangle', 'custom']; + var isValidHandleType = availableHandleModifiers.indexOf(this.options.handle) !== -1; + if (isValidHandleType) { + this._addClass(this.handle1, this.options.handle); + this._addClass(this.handle2, this.options.handle); + + for (i = 0; i < this.ticks.length; i++) { + this._addClass(this.ticks[i], this.options.handle); + } + } + + this._state.offset = this._offset(this.sliderElem); + this._state.size = this.sliderElem[this.sizePos]; + this.setValue(this._state.value); + + /****************************************** + Bind Event Listeners + ******************************************/ + + // Bind keyboard handlers + this.handle1Keydown = this._keydown.bind(this, 0); + this.handle1.addEventListener("keydown", this.handle1Keydown, false); + + this.handle2Keydown = this._keydown.bind(this, 1); + this.handle2.addEventListener("keydown", this.handle2Keydown, false); + + this.mousedown = this._mousedown.bind(this); + this.touchstart = this._touchstart.bind(this); + this.touchmove = this._touchmove.bind(this); + + if (this.touchCapable) { + // Bind touch handlers + this.sliderElem.addEventListener("touchstart", this.touchstart, false); + this.sliderElem.addEventListener("touchmove", this.touchmove, false); + } + this.sliderElem.addEventListener("mousedown", this.mousedown, false); + + // Bind window handlers + this.resize = this._resize.bind(this); + window.addEventListener("resize", this.resize, false); + + // Bind tooltip-related handlers + if (this.options.tooltip === 'hide') { + this._addClass(this.tooltip, 'hide'); + this._addClass(this.tooltip_min, 'hide'); + this._addClass(this.tooltip_max, 'hide'); + } else if (this.options.tooltip === 'always') { + this._showTooltip(); + this._alwaysShowTooltip = true; + } else { + this.showTooltip = this._showTooltip.bind(this); + this.hideTooltip = this._hideTooltip.bind(this); + + if (this.options.ticks_tooltip) { + var callbackHandle = this._addTickListener(); + //create handle1 listeners and store references in map + var mouseEnter = callbackHandle.addMouseEnter(this, this.handle1); + var mouseLeave = callbackHandle.addMouseLeave(this, this.handle1); + this.handleCallbackMap.handle1 = { + mouseEnter: mouseEnter, + mouseLeave: mouseLeave + }; + //create handle2 listeners and store references in map + mouseEnter = callbackHandle.addMouseEnter(this, this.handle2); + mouseLeave = callbackHandle.addMouseLeave(this, this.handle2); + this.handleCallbackMap.handle2 = { + mouseEnter: mouseEnter, + mouseLeave: mouseLeave + }; + } else { + this.sliderElem.addEventListener("mouseenter", this.showTooltip, false); + this.sliderElem.addEventListener("mouseleave", this.hideTooltip, false); + } + + this.handle1.addEventListener("focus", this.showTooltip, false); + this.handle1.addEventListener("blur", this.hideTooltip, false); + + this.handle2.addEventListener("focus", this.showTooltip, false); + this.handle2.addEventListener("blur", this.hideTooltip, false); + } + + if (this.options.enabled) { + this.enable(); + } else { + this.disable(); + } + } + + /************************************************* + INSTANCE PROPERTIES/METHODS + - Any methods bound to the prototype are considered + part of the plugin's `public` interface + **************************************************/ + Slider.prototype = { + _init: function _init() {}, // NOTE: Must exist to support bridget + + constructor: Slider, + + defaultOptions: { + id: "", + min: 0, + max: 10, + step: 1, + precision: 0, + orientation: 'horizontal', + value: 5, + range: false, + selection: 'before', + tooltip: 'show', + tooltip_split: false, + handle: 'round', + reversed: false, + enabled: true, + formatter: function formatter(val) { + if (Array.isArray(val)) { + return val[0] + " : " + val[1]; + } else { + return val; + } + }, + natural_arrow_keys: false, + ticks: [], + ticks_positions: [], + ticks_labels: [], + ticks_snap_bounds: 0, + ticks_tooltip: false, + scale: 'linear', + focus: false, + tooltip_position: null, + labelledby: null, + rangeHighlights: [] + }, + + getElement: function getElement() { + return this.sliderElem; + }, + + getValue: function getValue() { + if (this.options.range) { + return this._state.value; + } else { + return this._state.value[0]; + } + }, + + setValue: function setValue(val, triggerSlideEvent, triggerChangeEvent) { + if (!val) { + val = 0; + } + var oldValue = this.getValue(); + this._state.value = this._validateInputValue(val); + var applyPrecision = this._applyPrecision.bind(this); + + if (this.options.range) { + this._state.value[0] = applyPrecision(this._state.value[0]); + this._state.value[1] = applyPrecision(this._state.value[1]); + + this._state.value[0] = Math.max(this.options.min, Math.min(this.options.max, this._state.value[0])); + this._state.value[1] = Math.max(this.options.min, Math.min(this.options.max, this._state.value[1])); + } else { + this._state.value = applyPrecision(this._state.value); + this._state.value = [Math.max(this.options.min, Math.min(this.options.max, this._state.value))]; + this._addClass(this.handle2, 'hide'); + if (this.options.selection === 'after') { + this._state.value[1] = this.options.max; + } else { + this._state.value[1] = this.options.min; + } + } + + if (this.options.max > this.options.min) { + this._state.percentage = [this._toPercentage(this._state.value[0]), this._toPercentage(this._state.value[1]), this.options.step * 100 / (this.options.max - this.options.min)]; + } else { + this._state.percentage = [0, 0, 100]; + } + + this._layout(); + var newValue = this.options.range ? this._state.value : this._state.value[0]; + + this._setDataVal(newValue); + if (triggerSlideEvent === true) { + this._trigger('slide', newValue); + } + if (oldValue !== newValue && triggerChangeEvent === true) { + this._trigger('change', { + oldValue: oldValue, + newValue: newValue + }); + } + + return this; + }, + + destroy: function destroy() { + // Remove event handlers on slider elements + this._removeSliderEventHandlers(); + + // Remove the slider from the DOM + this.sliderElem.parentNode.removeChild(this.sliderElem); + /* Show original element */ + this.element.style.display = ""; + + // Clear out custom event bindings + this._cleanUpEventCallbacksMap(); + + // Remove data values + this.element.removeAttribute("data"); + + // Remove JQuery handlers/data + if ($) { + this._unbindJQueryEventHandlers(); + this.$element.removeData('slider'); + } + }, + + disable: function disable() { + this._state.enabled = false; + this.handle1.removeAttribute("tabindex"); + this.handle2.removeAttribute("tabindex"); + this._addClass(this.sliderElem, 'slider-disabled'); + this._trigger('slideDisabled'); + + return this; + }, + + enable: function enable() { + this._state.enabled = true; + this.handle1.setAttribute("tabindex", 0); + this.handle2.setAttribute("tabindex", 0); + this._removeClass(this.sliderElem, 'slider-disabled'); + this._trigger('slideEnabled'); + + return this; + }, + + toggle: function toggle() { + if (this._state.enabled) { + this.disable(); + } else { + this.enable(); + } + return this; + }, + + isEnabled: function isEnabled() { + return this._state.enabled; + }, + + on: function on(evt, callback) { + this._bindNonQueryEventHandler(evt, callback); + return this; + }, + + off: function off(evt, callback) { + if ($) { + this.$element.off(evt, callback); + this.$sliderElem.off(evt, callback); + } else { + this._unbindNonQueryEventHandler(evt, callback); + } + }, + + getAttribute: function getAttribute(attribute) { + if (attribute) { + return this.options[attribute]; + } else { + return this.options; + } + }, + + setAttribute: function setAttribute(attribute, value) { + this.options[attribute] = value; + return this; + }, + + refresh: function refresh() { + this._removeSliderEventHandlers(); + createNewSlider.call(this, this.element, this.options); + if ($) { + // Bind new instance of slider to the element + $.data(this.element, 'slider', this); + } + return this; + }, + + relayout: function relayout() { + this._resize(); + this._layout(); + return this; + }, + + /******************************+ + HELPERS + - Any method that is not part of the public interface. + - Place it underneath this comment block and write its signature like so: + _fnName : function() {...} + ********************************/ + _removeSliderEventHandlers: function _removeSliderEventHandlers() { + // Remove keydown event listeners + this.handle1.removeEventListener("keydown", this.handle1Keydown, false); + this.handle2.removeEventListener("keydown", this.handle2Keydown, false); + + //remove the listeners from the ticks and handles if they had their own listeners + if (this.options.ticks_tooltip) { + var ticks = this.ticksContainer.getElementsByClassName('slider-tick'); + for (var i = 0; i < ticks.length; i++) { + ticks[i].removeEventListener('mouseenter', this.ticksCallbackMap[i].mouseEnter, false); + ticks[i].removeEventListener('mouseleave', this.ticksCallbackMap[i].mouseLeave, false); + } + this.handle1.removeEventListener('mouseenter', this.handleCallbackMap.handle1.mouseEnter, false); + this.handle2.removeEventListener('mouseenter', this.handleCallbackMap.handle2.mouseEnter, false); + this.handle1.removeEventListener('mouseleave', this.handleCallbackMap.handle1.mouseLeave, false); + this.handle2.removeEventListener('mouseleave', this.handleCallbackMap.handle2.mouseLeave, false); + } + + this.handleCallbackMap = null; + this.ticksCallbackMap = null; + + if (this.showTooltip) { + this.handle1.removeEventListener("focus", this.showTooltip, false); + this.handle2.removeEventListener("focus", this.showTooltip, false); + } + if (this.hideTooltip) { + this.handle1.removeEventListener("blur", this.hideTooltip, false); + this.handle2.removeEventListener("blur", this.hideTooltip, false); + } + + // Remove event listeners from sliderElem + if (this.showTooltip) { + this.sliderElem.removeEventListener("mouseenter", this.showTooltip, false); + } + if (this.hideTooltip) { + this.sliderElem.removeEventListener("mouseleave", this.hideTooltip, false); + } + this.sliderElem.removeEventListener("touchstart", this.touchstart, false); + this.sliderElem.removeEventListener("touchmove", this.touchmove, false); + this.sliderElem.removeEventListener("mousedown", this.mousedown, false); + + // Remove window event listener + window.removeEventListener("resize", this.resize, false); + }, + _bindNonQueryEventHandler: function _bindNonQueryEventHandler(evt, callback) { + if (this.eventToCallbackMap[evt] === undefined) { + this.eventToCallbackMap[evt] = []; + } + this.eventToCallbackMap[evt].push(callback); + }, + _unbindNonQueryEventHandler: function _unbindNonQueryEventHandler(evt, callback) { + var callbacks = this.eventToCallbackMap[evt]; + if (callbacks !== undefined) { + for (var i = 0; i < callbacks.length; i++) { + if (callbacks[i] === callback) { + callbacks.splice(i, 1); + break; + } + } + } + }, + _cleanUpEventCallbacksMap: function _cleanUpEventCallbacksMap() { + var eventNames = Object.keys(this.eventToCallbackMap); + for (var i = 0; i < eventNames.length; i++) { + var eventName = eventNames[i]; + delete this.eventToCallbackMap[eventName]; + } + }, + _showTooltip: function _showTooltip() { + if (this.options.tooltip_split === false) { + this._addClass(this.tooltip, 'in'); + this.tooltip_min.style.display = 'none'; + this.tooltip_max.style.display = 'none'; + } else { + this._addClass(this.tooltip_min, 'in'); + this._addClass(this.tooltip_max, 'in'); + this.tooltip.style.display = 'none'; + } + this._state.over = true; + }, + _hideTooltip: function _hideTooltip() { + if (this._state.inDrag === false && this.alwaysShowTooltip !== true) { + this._removeClass(this.tooltip, 'in'); + this._removeClass(this.tooltip_min, 'in'); + this._removeClass(this.tooltip_max, 'in'); + } + this._state.over = false; + }, + _setToolTipOnMouseOver: function _setToolTipOnMouseOver(tempState) { + var formattedTooltipVal = this.options.formatter(!tempState ? this._state.value[0] : tempState.value[0]); + var positionPercentages = !tempState ? getPositionPercentages(this._state, this.options.reversed) : getPositionPercentages(tempState, this.options.reversed); + this._setText(this.tooltipInner, formattedTooltipVal); + + this.tooltip.style[this.stylePos] = positionPercentages[0] + '%'; + if (this.options.orientation === 'vertical') { + this._css(this.tooltip, 'margin-top', -this.tooltip.offsetHeight / 2 + 'px'); + } else { + this._css(this.tooltip, 'margin-left', -this.tooltip.offsetWidth / 2 + 'px'); + } + + function getPositionPercentages(state, reversed) { + if (reversed) { + return [100 - state.percentage[0], this.options.range ? 100 - state.percentage[1] : state.percentage[1]]; + } + return [state.percentage[0], state.percentage[1]]; + } + }, + _addTickListener: function _addTickListener() { + return { + addMouseEnter: function addMouseEnter(reference, tick, index) { + var enter = function enter() { + var tempState = reference._state; + var idString = index >= 0 ? index : this.attributes['aria-valuenow'].value; + var hoverIndex = parseInt(idString, 10); + tempState.value[0] = hoverIndex; + tempState.percentage[0] = reference.options.ticks_positions[hoverIndex]; + reference._setToolTipOnMouseOver(tempState); + reference._showTooltip(); + }; + tick.addEventListener("mouseenter", enter, false); + return enter; + }, + addMouseLeave: function addMouseLeave(reference, tick) { + var leave = function leave() { + reference._hideTooltip(); + }; + tick.addEventListener("mouseleave", leave, false); + return leave; + } + }; + }, + _layout: function _layout() { + var positionPercentages; + + if (this.options.reversed) { + positionPercentages = [100 - this._state.percentage[0], this.options.range ? 100 - this._state.percentage[1] : this._state.percentage[1]]; + } else { + positionPercentages = [this._state.percentage[0], this._state.percentage[1]]; + } + + this.handle1.style[this.stylePos] = positionPercentages[0] + '%'; + this.handle1.setAttribute('aria-valuenow', this._state.value[0]); + + this.handle2.style[this.stylePos] = positionPercentages[1] + '%'; + this.handle2.setAttribute('aria-valuenow', this._state.value[1]); + + /* Position highlight range elements */ + if (this.rangeHighlightElements.length > 0 && Array.isArray(this.options.rangeHighlights) && this.options.rangeHighlights.length > 0) { + for (var _i = 0; _i < this.options.rangeHighlights.length; _i++) { + var startPercent = this._toPercentage(this.options.rangeHighlights[_i].start); + var endPercent = this._toPercentage(this.options.rangeHighlights[_i].end); + + if (this.options.reversed) { + var sp = 100 - endPercent; + endPercent = 100 - startPercent; + startPercent = sp; + } + + var currentRange = this._createHighlightRange(startPercent, endPercent); + + if (currentRange) { + if (this.options.orientation === 'vertical') { + this.rangeHighlightElements[_i].style.top = currentRange.start + "%"; + this.rangeHighlightElements[_i].style.height = currentRange.size + "%"; + } else { + this.rangeHighlightElements[_i].style.left = currentRange.start + "%"; + this.rangeHighlightElements[_i].style.width = currentRange.size + "%"; + } + } else { + this.rangeHighlightElements[_i].style.display = "none"; + } + } + } + + /* Position ticks and labels */ + if (Array.isArray(this.options.ticks) && this.options.ticks.length > 0) { + + var styleSize = this.options.orientation === 'vertical' ? 'height' : 'width'; + var styleMargin = this.options.orientation === 'vertical' ? 'marginTop' : 'marginLeft'; + var labelSize = this._state.size / (this.options.ticks.length - 1); + + if (this.tickLabelContainer) { + var extraMargin = 0; + if (this.options.ticks_positions.length === 0) { + if (this.options.orientation !== 'vertical') { + this.tickLabelContainer.style[styleMargin] = -labelSize / 2 + 'px'; + } + + extraMargin = this.tickLabelContainer.offsetHeight; + } else { + /* Chidren are position absolute, calculate height by finding the max offsetHeight of a child */ + for (i = 0; i < this.tickLabelContainer.childNodes.length; i++) { + if (this.tickLabelContainer.childNodes[i].offsetHeight > extraMargin) { + extraMargin = this.tickLabelContainer.childNodes[i].offsetHeight; + } + } + } + if (this.options.orientation === 'horizontal') { + this.sliderElem.style.marginBottom = extraMargin + 'px'; + } + } + for (var i = 0; i < this.options.ticks.length; i++) { + + var percentage = this.options.ticks_positions[i] || this._toPercentage(this.options.ticks[i]); + + if (this.options.reversed) { + percentage = 100 - percentage; + } + + this.ticks[i].style[this.stylePos] = percentage + '%'; + + /* Set class labels to denote whether ticks are in the selection */ + this._removeClass(this.ticks[i], 'in-selection'); + if (!this.options.range) { + if (this.options.selection === 'after' && percentage >= positionPercentages[0]) { + this._addClass(this.ticks[i], 'in-selection'); + } else if (this.options.selection === 'before' && percentage <= positionPercentages[0]) { + this._addClass(this.ticks[i], 'in-selection'); + } + } else if (percentage >= positionPercentages[0] && percentage <= positionPercentages[1]) { + this._addClass(this.ticks[i], 'in-selection'); + } + + if (this.tickLabels[i]) { + this.tickLabels[i].style[styleSize] = labelSize + 'px'; + + if (this.options.orientation !== 'vertical' && this.options.ticks_positions[i] !== undefined) { + this.tickLabels[i].style.position = 'absolute'; + this.tickLabels[i].style[this.stylePos] = percentage + '%'; + this.tickLabels[i].style[styleMargin] = -labelSize / 2 + 'px'; + } else if (this.options.orientation === 'vertical') { + this.tickLabels[i].style['marginLeft'] = this.sliderElem.offsetWidth + 'px'; + this.tickLabelContainer.style['marginTop'] = this.sliderElem.offsetWidth / 2 * -1 + 'px'; + } + } + } + } + + var formattedTooltipVal; + + if (this.options.range) { + formattedTooltipVal = this.options.formatter(this._state.value); + this._setText(this.tooltipInner, formattedTooltipVal); + this.tooltip.style[this.stylePos] = (positionPercentages[1] + positionPercentages[0]) / 2 + '%'; + + if (this.options.orientation === 'vertical') { + this._css(this.tooltip, 'margin-top', -this.tooltip.offsetHeight / 2 + 'px'); + } else { + this._css(this.tooltip, 'margin-left', -this.tooltip.offsetWidth / 2 + 'px'); + } + + if (this.options.orientation === 'vertical') { + this._css(this.tooltip, 'margin-top', -this.tooltip.offsetHeight / 2 + 'px'); + } else { + this._css(this.tooltip, 'margin-left', -this.tooltip.offsetWidth / 2 + 'px'); + } + + var innerTooltipMinText = this.options.formatter(this._state.value[0]); + this._setText(this.tooltipInner_min, innerTooltipMinText); + + var innerTooltipMaxText = this.options.formatter(this._state.value[1]); + this._setText(this.tooltipInner_max, innerTooltipMaxText); + + this.tooltip_min.style[this.stylePos] = positionPercentages[0] + '%'; + + if (this.options.orientation === 'vertical') { + this._css(this.tooltip_min, 'margin-top', -this.tooltip_min.offsetHeight / 2 + 'px'); + } else { + this._css(this.tooltip_min, 'margin-left', -this.tooltip_min.offsetWidth / 2 + 'px'); + } + + this.tooltip_max.style[this.stylePos] = positionPercentages[1] + '%'; + + if (this.options.orientation === 'vertical') { + this._css(this.tooltip_max, 'margin-top', -this.tooltip_max.offsetHeight / 2 + 'px'); + } else { + this._css(this.tooltip_max, 'margin-left', -this.tooltip_max.offsetWidth / 2 + 'px'); + } + } else { + formattedTooltipVal = this.options.formatter(this._state.value[0]); + this._setText(this.tooltipInner, formattedTooltipVal); + + this.tooltip.style[this.stylePos] = positionPercentages[0] + '%'; + if (this.options.orientation === 'vertical') { + this._css(this.tooltip, 'margin-top', -this.tooltip.offsetHeight / 2 + 'px'); + } else { + this._css(this.tooltip, 'margin-left', -this.tooltip.offsetWidth / 2 + 'px'); + } + } + + if (this.options.orientation === 'vertical') { + this.trackLow.style.top = '0'; + this.trackLow.style.height = Math.min(positionPercentages[0], positionPercentages[1]) + '%'; + + this.trackSelection.style.top = Math.min(positionPercentages[0], positionPercentages[1]) + '%'; + this.trackSelection.style.height = Math.abs(positionPercentages[0] - positionPercentages[1]) + '%'; + + this.trackHigh.style.bottom = '0'; + this.trackHigh.style.height = 100 - Math.min(positionPercentages[0], positionPercentages[1]) - Math.abs(positionPercentages[0] - positionPercentages[1]) + '%'; + } else { + this.trackLow.style.left = '0'; + this.trackLow.style.width = Math.min(positionPercentages[0], positionPercentages[1]) + '%'; + + this.trackSelection.style.left = Math.min(positionPercentages[0], positionPercentages[1]) + '%'; + this.trackSelection.style.width = Math.abs(positionPercentages[0] - positionPercentages[1]) + '%'; + + this.trackHigh.style.right = '0'; + this.trackHigh.style.width = 100 - Math.min(positionPercentages[0], positionPercentages[1]) - Math.abs(positionPercentages[0] - positionPercentages[1]) + '%'; + + var offset_min = this.tooltip_min.getBoundingClientRect(); + var offset_max = this.tooltip_max.getBoundingClientRect(); + + if (this.options.tooltip_position === 'bottom') { + if (offset_min.right > offset_max.left) { + this._removeClass(this.tooltip_max, 'bottom'); + this._addClass(this.tooltip_max, 'top'); + this.tooltip_max.style.top = ''; + this.tooltip_max.style.bottom = 22 + 'px'; + } else { + this._removeClass(this.tooltip_max, 'top'); + this._addClass(this.tooltip_max, 'bottom'); + this.tooltip_max.style.top = this.tooltip_min.style.top; + this.tooltip_max.style.bottom = ''; + } + } else { + if (offset_min.right > offset_max.left) { + this._removeClass(this.tooltip_max, 'top'); + this._addClass(this.tooltip_max, 'bottom'); + this.tooltip_max.style.top = 18 + 'px'; + } else { + this._removeClass(this.tooltip_max, 'bottom'); + this._addClass(this.tooltip_max, 'top'); + this.tooltip_max.style.top = this.tooltip_min.style.top; + } + } + } + }, + _createHighlightRange: function _createHighlightRange(start, end) { + if (this._isHighlightRange(start, end)) { + if (start > end) { + return { 'start': end, 'size': start - end }; + } + return { 'start': start, 'size': end - start }; + } + return null; + }, + _isHighlightRange: function _isHighlightRange(start, end) { + if (0 <= start && start <= 100 && 0 <= end && end <= 100) { + return true; + } else { + return false; + } + }, + _resize: function _resize(ev) { + /*jshint unused:false*/ + this._state.offset = this._offset(this.sliderElem); + this._state.size = this.sliderElem[this.sizePos]; + this._layout(); + }, + _removeProperty: function _removeProperty(element, prop) { + if (element.style.removeProperty) { + element.style.removeProperty(prop); + } else { + element.style.removeAttribute(prop); + } + }, + _mousedown: function _mousedown(ev) { + if (!this._state.enabled) { + return false; + } + + this._state.offset = this._offset(this.sliderElem); + this._state.size = this.sliderElem[this.sizePos]; + + var percentage = this._getPercentage(ev); + + if (this.options.range) { + var diff1 = Math.abs(this._state.percentage[0] - percentage); + var diff2 = Math.abs(this._state.percentage[1] - percentage); + this._state.dragged = diff1 < diff2 ? 0 : 1; + this._adjustPercentageForRangeSliders(percentage); + } else { + this._state.dragged = 0; + } + + this._state.percentage[this._state.dragged] = percentage; + this._layout(); + + if (this.touchCapable) { + document.removeEventListener("touchmove", this.mousemove, false); + document.removeEventListener("touchend", this.mouseup, false); + } + + if (this.mousemove) { + document.removeEventListener("mousemove", this.mousemove, false); + } + if (this.mouseup) { + document.removeEventListener("mouseup", this.mouseup, false); + } + + this.mousemove = this._mousemove.bind(this); + this.mouseup = this._mouseup.bind(this); + + if (this.touchCapable) { + // Touch: Bind touch events: + document.addEventListener("touchmove", this.mousemove, false); + document.addEventListener("touchend", this.mouseup, false); + } + // Bind mouse events: + document.addEventListener("mousemove", this.mousemove, false); + document.addEventListener("mouseup", this.mouseup, false); + + this._state.inDrag = true; + var newValue = this._calculateValue(); + + this._trigger('slideStart', newValue); + + this._setDataVal(newValue); + this.setValue(newValue, false, true); + + this._pauseEvent(ev); + + if (this.options.focus) { + this._triggerFocusOnHandle(this._state.dragged); + } + + return true; + }, + _touchstart: function _touchstart(ev) { + if (ev.changedTouches === undefined) { + this._mousedown(ev); + return; + } + + var touch = ev.changedTouches[0]; + this.touchX = touch.pageX; + this.touchY = touch.pageY; + }, + _triggerFocusOnHandle: function _triggerFocusOnHandle(handleIdx) { + if (handleIdx === 0) { + this.handle1.focus(); + } + if (handleIdx === 1) { + this.handle2.focus(); + } + }, + _keydown: function _keydown(handleIdx, ev) { + if (!this._state.enabled) { + return false; + } + + var dir; + switch (ev.keyCode) { + case 37: // left + case 40: + // down + dir = -1; + break; + case 39: // right + case 38: + // up + dir = 1; + break; + } + if (!dir) { + return; + } + + // use natural arrow keys instead of from min to max + if (this.options.natural_arrow_keys) { + var ifVerticalAndNotReversed = this.options.orientation === 'vertical' && !this.options.reversed; + var ifHorizontalAndReversed = this.options.orientation === 'horizontal' && this.options.reversed; + + if (ifVerticalAndNotReversed || ifHorizontalAndReversed) { + dir = -dir; + } + } + + var val = this._state.value[handleIdx] + dir * this.options.step; + if (this.options.range) { + val = [!handleIdx ? val : this._state.value[0], handleIdx ? val : this._state.value[1]]; + } + + this._trigger('slideStart', val); + this._setDataVal(val); + this.setValue(val, true, true); + + this._setDataVal(val); + this._trigger('slideStop', val); + this._layout(); + + this._pauseEvent(ev); + + return false; + }, + _pauseEvent: function _pauseEvent(ev) { + if (ev.stopPropagation) { + ev.stopPropagation(); + } + if (ev.preventDefault) { + ev.preventDefault(); + } + ev.cancelBubble = true; + ev.returnValue = false; + }, + _mousemove: function _mousemove(ev) { + if (!this._state.enabled) { + return false; + } + + var percentage = this._getPercentage(ev); + this._adjustPercentageForRangeSliders(percentage); + this._state.percentage[this._state.dragged] = percentage; + this._layout(); + + var val = this._calculateValue(true); + this.setValue(val, true, true); + + return false; + }, + _touchmove: function _touchmove(ev) { + if (ev.changedTouches === undefined) { + return; + } + + var touch = ev.changedTouches[0]; + + var xDiff = touch.pageX - this.touchX; + var yDiff = touch.pageY - this.touchY; + + if (!this._state.inDrag) { + // Vertical Slider + if (this.options.orientation === 'vertical' && xDiff <= 5 && xDiff >= -5 && (yDiff >= 15 || yDiff <= -15)) { + this._mousedown(ev); + } + // Horizontal slider. + else if (yDiff <= 5 && yDiff >= -5 && (xDiff >= 15 || xDiff <= -15)) { + this._mousedown(ev); + } + } + }, + _adjustPercentageForRangeSliders: function _adjustPercentageForRangeSliders(percentage) { + if (this.options.range) { + var precision = this._getNumDigitsAfterDecimalPlace(percentage); + precision = precision ? precision - 1 : 0; + var percentageWithAdjustedPrecision = this._applyToFixedAndParseFloat(percentage, precision); + if (this._state.dragged === 0 && this._applyToFixedAndParseFloat(this._state.percentage[1], precision) < percentageWithAdjustedPrecision) { + this._state.percentage[0] = this._state.percentage[1]; + this._state.dragged = 1; + } else if (this._state.dragged === 1 && this._applyToFixedAndParseFloat(this._state.percentage[0], precision) > percentageWithAdjustedPrecision) { + this._state.percentage[1] = this._state.percentage[0]; + this._state.dragged = 0; + } + } + }, + _mouseup: function _mouseup() { + if (!this._state.enabled) { + return false; + } + if (this.touchCapable) { + // Touch: Unbind touch event handlers: + document.removeEventListener("touchmove", this.mousemove, false); + document.removeEventListener("touchend", this.mouseup, false); + } + // Unbind mouse event handlers: + document.removeEventListener("mousemove", this.mousemove, false); + document.removeEventListener("mouseup", this.mouseup, false); + + this._state.inDrag = false; + if (this._state.over === false) { + this._hideTooltip(); + } + var val = this._calculateValue(true); + + this._layout(); + this._setDataVal(val); + this._trigger('slideStop', val); + + return false; + }, + _calculateValue: function _calculateValue(snapToClosestTick) { + var val; + if (this.options.range) { + val = [this.options.min, this.options.max]; + if (this._state.percentage[0] !== 0) { + val[0] = this._toValue(this._state.percentage[0]); + val[0] = this._applyPrecision(val[0]); + } + if (this._state.percentage[1] !== 100) { + val[1] = this._toValue(this._state.percentage[1]); + val[1] = this._applyPrecision(val[1]); + } + } else { + val = this._toValue(this._state.percentage[0]); + val = parseFloat(val); + val = this._applyPrecision(val); + } + + if (snapToClosestTick) { + var min = [val, Infinity]; + for (var i = 0; i < this.options.ticks.length; i++) { + var diff = Math.abs(this.options.ticks[i] - val); + if (diff <= min[1]) { + min = [this.options.ticks[i], diff]; + } + } + if (min[1] <= this.options.ticks_snap_bounds) { + return min[0]; + } + } + + return val; + }, + _applyPrecision: function _applyPrecision(val) { + var precision = this.options.precision || this._getNumDigitsAfterDecimalPlace(this.options.step); + return this._applyToFixedAndParseFloat(val, precision); + }, + _getNumDigitsAfterDecimalPlace: function _getNumDigitsAfterDecimalPlace(num) { + var match = ('' + num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/); + if (!match) { + return 0; + } + return Math.max(0, (match[1] ? match[1].length : 0) - (match[2] ? +match[2] : 0)); + }, + _applyToFixedAndParseFloat: function _applyToFixedAndParseFloat(num, toFixedInput) { + var truncatedNum = num.toFixed(toFixedInput); + return parseFloat(truncatedNum); + }, + /* + Credits to Mike Samuel for the following method! + Source: http://stackoverflow.com/questions/10454518/javascript-how-to-retrieve-the-number-of-decimals-of-a-string-number + */ + _getPercentage: function _getPercentage(ev) { + if (this.touchCapable && (ev.type === 'touchstart' || ev.type === 'touchmove')) { + ev = ev.touches[0]; + } + + var eventPosition = ev[this.mousePos]; + var sliderOffset = this._state.offset[this.stylePos]; + var distanceToSlide = eventPosition - sliderOffset; + // Calculate what percent of the length the slider handle has slid + var percentage = distanceToSlide / this._state.size * 100; + percentage = Math.round(percentage / this._state.percentage[2]) * this._state.percentage[2]; + if (this.options.reversed) { + percentage = 100 - percentage; + } + + // Make sure the percent is within the bounds of the slider. + // 0% corresponds to the 'min' value of the slide + // 100% corresponds to the 'max' value of the slide + return Math.max(0, Math.min(100, percentage)); + }, + _validateInputValue: function _validateInputValue(val) { + if (!isNaN(+val)) { + return +val; + } else if (Array.isArray(val)) { + this._validateArray(val); + return val; + } else { + throw new Error(ErrorMsgs.formatInvalidInputErrorMsg(val)); + } + }, + _validateArray: function _validateArray(val) { + for (var i = 0; i < val.length; i++) { + var input = val[i]; + if (typeof input !== 'number') { + throw new Error(ErrorMsgs.formatInvalidInputErrorMsg(input)); + } + } + }, + _setDataVal: function _setDataVal(val) { + this.element.setAttribute('data-value', val); + this.element.setAttribute('value', val); + this.element.value = val; + }, + _trigger: function _trigger(evt, val) { + val = val || val === 0 ? val : undefined; + + var callbackFnArray = this.eventToCallbackMap[evt]; + if (callbackFnArray && callbackFnArray.length) { + for (var i = 0; i < callbackFnArray.length; i++) { + var callbackFn = callbackFnArray[i]; + callbackFn(val); + } + } + + /* If JQuery exists, trigger JQuery events */ + if ($) { + this._triggerJQueryEvent(evt, val); + } + }, + _triggerJQueryEvent: function _triggerJQueryEvent(evt, val) { + var eventData = { + type: evt, + value: val + }; + this.$element.trigger(eventData); + this.$sliderElem.trigger(eventData); + }, + _unbindJQueryEventHandlers: function _unbindJQueryEventHandlers() { + this.$element.off(); + this.$sliderElem.off(); + }, + _setText: function _setText(element, text) { + if (typeof element.textContent !== "undefined") { + element.textContent = text; + } else if (typeof element.innerText !== "undefined") { + element.innerText = text; + } + }, + _removeClass: function _removeClass(element, classString) { + var classes = classString.split(" "); + var newClasses = element.className; + + for (var i = 0; i < classes.length; i++) { + var classTag = classes[i]; + var regex = new RegExp("(?:\\s|^)" + classTag + "(?:\\s|$)"); + newClasses = newClasses.replace(regex, " "); + } + + element.className = newClasses.trim(); + }, + _addClass: function _addClass(element, classString) { + var classes = classString.split(" "); + var newClasses = element.className; + + for (var i = 0; i < classes.length; i++) { + var classTag = classes[i]; + var regex = new RegExp("(?:\\s|^)" + classTag + "(?:\\s|$)"); + var ifClassExists = regex.test(newClasses); + + if (!ifClassExists) { + newClasses += " " + classTag; + } + } + + element.className = newClasses.trim(); + }, + _offsetLeft: function _offsetLeft(obj) { + return obj.getBoundingClientRect().left; + }, + _offsetTop: function _offsetTop(obj) { + var offsetTop = obj.offsetTop; + while ((obj = obj.offsetParent) && !isNaN(obj.offsetTop)) { + offsetTop += obj.offsetTop; + if (obj.tagName !== 'BODY') { + offsetTop -= obj.scrollTop; + } + } + return offsetTop; + }, + _offset: function _offset(obj) { + return { + left: this._offsetLeft(obj), + top: this._offsetTop(obj) + }; + }, + _css: function _css(elementRef, styleName, value) { + if ($) { + $.style(elementRef, styleName, value); + } else { + var style = styleName.replace(/^-ms-/, "ms-").replace(/-([\da-z])/gi, function (all, letter) { + return letter.toUpperCase(); + }); + elementRef.style[style] = value; + } + }, + _toValue: function _toValue(percentage) { + return this.options.scale.toValue.apply(this, [percentage]); + }, + _toPercentage: function _toPercentage(value) { + return this.options.scale.toPercentage.apply(this, [value]); + }, + _setTooltipPosition: function _setTooltipPosition() { + var tooltips = [this.tooltip, this.tooltip_min, this.tooltip_max]; + if (this.options.orientation === 'vertical') { + var tooltipPos = this.options.tooltip_position || 'right'; + var oppositeSide = tooltipPos === 'left' ? 'right' : 'left'; + tooltips.forEach(function (tooltip) { + this._addClass(tooltip, tooltipPos); + tooltip.style[oppositeSide] = '100%'; + }.bind(this)); + } else if (this.options.tooltip_position === 'bottom') { + tooltips.forEach(function (tooltip) { + this._addClass(tooltip, 'bottom'); + tooltip.style.top = 22 + 'px'; + }.bind(this)); + } else { + tooltips.forEach(function (tooltip) { + this._addClass(tooltip, 'top'); + tooltip.style.top = -this.tooltip.outerHeight - 14 + 'px'; + }.bind(this)); + } + } + }; + + /********************************* + Attach to global namespace + *********************************/ + if ($) { + (function () { + var autoRegisterNamespace = void 0; + + if (!$.fn.slider) { + $.bridget(NAMESPACE_MAIN, Slider); + autoRegisterNamespace = NAMESPACE_MAIN; + } else { + if (windowIsDefined) { + window.console.warn("bootstrap-slider.js - WARNING: $.fn.slider namespace is already bound. Use the $.fn.bootstrapSlider namespace instead."); + } + autoRegisterNamespace = NAMESPACE_ALTERNATE; + } + $.bridget(NAMESPACE_ALTERNATE, Slider); + + // Auto-Register data-provide="slider" Elements + $(function () { + $("input[data-provide=slider]")[autoRegisterNamespace](); + }); + })(); + } + })($); + + return Slider; +}); diff --git a/flask/forest/static/js/clouds.js b/flask/forest/static/js/clouds.js new file mode 100644 index 0000000..f7de3d1 --- /dev/null +++ b/flask/forest/static/js/clouds.js @@ -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(); + diff --git a/flask/forest/static/js/jquery.js b/flask/forest/static/js/jquery.js new file mode 100644 index 0000000..4c5be4c --- /dev/null +++ b/flask/forest/static/js/jquery.js @@ -0,0 +1,4 @@ +/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R), +a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)), +void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" +{% endblock %} + +{% block page_content %} +
+ {% include "admin/admin_tasks.html" %} + +
+
+
Deployments
+

+ {% include "admin/menu_deployments.html" %} +

+ + + + + + + + + + + + + + + {% for deploy in deployments %} + {% if deploy.deleted == True %} + + {% else %} + {% if deploy.enabled == False %} + + {% else %} + {% if deploy.warning == True %} + + {% else %} + + {% endif %} + {% endif %} + {% endif %} + + + + + + {% if deploy.date_last_charge == None %} + + {% else %} + + {% endif %} + + {% if deploy.deleted == True %} + + {% else %} + + {% endif %} + {% endfor %} + +
OwnerAliasCPUMemHDDLast ChargedDays Left
{{ deploy.owner.email }}{{ deploy.machine_alias }}{{ deploy.machine_cpu }}{{ deploy.machine_mem }} MB{{ deploy.machine_hdd }} GBNever{{ moment(deploy.date_last_charge).format('lll') }} ({{ moment(deploy.date_last_charge).fromNow() }}){{ deploy.daysleft }}-deleted-
+
+
+
+ +
+ + +
+
+
+ +{% endblock %} + + diff --git a/flask/forest/templates/admin/list_deployments.html b/flask/forest/templates/admin/list_deployments.html new file mode 100644 index 0000000..6940181 --- /dev/null +++ b/flask/forest/templates/admin/list_deployments.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} + +{% block styles %} +{{ super() }} + +{% endblock %} + +{% block page_content %} +
+ {% include "admin/admin_tasks.html" %} + +
+
+
Deployments
+

+ + {% include "admin/menu_deployments.html" %} + +

+ + + + + + + + + + + + + + + + {% for deploy in deployments %} + {% if deploy.enabled == False %} + + {% else %} + {% if deploy.warning == True %} + + {% else %} + + {% endif %} + {% endif %} + + + + + + + {% if deploy.date_last_charge == None %} + + {% else %} + + {% endif %} + + + {% endfor %} + +
ServerVLANAliasCPUMemHDDLast ChargedDays LeftOwner
{{ deploy.server.name }}{% for vlan in deploy.inv_pubvlans %}{{ vlan.vlan_id }}{% endfor %}{% if status[deploy.machine_id] == 'running' %}{% else %}{% if status[deploy.machine_id] == 'stopped' %}{% else %}{% endif %}{% endif %}{{ deploy.machine_alias }}{{ deploy.machine_cpu }}{{ deploy.machine_mem }} MB{{ deploy.machine_hdd }} GBNever{{ moment(deploy.date_last_charge).format('lll') }} ({{ moment(deploy.date_last_charge).fromNow() }}){{ deploy.daysleft }}{{ deploy.owner.email }}
+
+
+
+ + +
+
+ +
+ +{% endblock %} + diff --git a/flask/forest/templates/admin/list_domains.html b/flask/forest/templates/admin/list_domains.html new file mode 100644 index 0000000..f39dbf9 --- /dev/null +++ b/flask/forest/templates/admin/list_domains.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} + +{% block styles %} +{{ super() }} + +{% endblock %} + +{% block page_content %} +
+ {% include "admin/admin_tasks.html" %} + +
+
+
Domains
+

+ + + + + + + + + + + {% for domain in domains %} + {% if domain.enabled == False %} + + {% else %} + {% if domain.warning == True %} + + {% else %} + + {% endif %} + {% endif %} + + + + + + {% endfor %} + +
OwnerNameExpiry DateDays Left
{{ domain.owner.email }}{{ domain.fqdn }}{{ domain.date_expire }}{{ domain.daysleft }}
+

+
+
+ +
+ +
+
+
+ +{% endblock %} + diff --git a/flask/forest/templates/admin/list_orders.html b/flask/forest/templates/admin/list_orders.html new file mode 100644 index 0000000..460ff91 --- /dev/null +++ b/flask/forest/templates/admin/list_orders.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} + +{% block styles %} +{{ super() }} +{% endblock %} + +{% block page_content %} +
+ {% include "admin/admin_tasks.html" %} + +
+
+
Orders
+

+

+ + + + + + + + + + + + + + + + {% for order in neworders %} + + + + + + + + + + + {% endfor %} + + {% for order in oldorders %} + + + + + + + + + + + {% endfor %} + + +
UserRegionRecipeparam 1param 2param 3param 4Status
{{ order.owner.email }}{{ order.region.description }}{{ order.recipe.templatefile }}{{ order.parameter1 }}{{ order.parameter2 }}{{ order.parameter3 }}{{ order.parameter4 }}{{ order.status }}
{{ order.owner.email }}{{ order.region.description }}{{ order.recipe.templatefile }}{{ order.parameter1 }}{{ order.parameter2 }}{{ order.parameter3 }}{{ order.parameter4 }}{{ order.status }}
+
+
+
+ + +
+ + +
+
+
+ +{% endblock %} + diff --git a/flask/forest/templates/admin/list_servers.html b/flask/forest/templates/admin/list_servers.html new file mode 100644 index 0000000..abc442b --- /dev/null +++ b/flask/forest/templates/admin/list_servers.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} + +{% block page_content %} +
+ + {% include "admin/admin_tasks.html" %} + +
+
+
Servers
+
+ {% include "admin/menu_deployments.html" %} +
+ + + + + + + + + + + + + + + {% for server in servers %} + + + + + + + + + + + {% endfor %} +
NameCPUMEMHDDAddressRegionSeller
{{ server.name }}{{ server.cpu }}{{ server.mem }}{{ server.hdd }}{{ server.address }}{{ server.region.name }}{{ server.owner.email }}
+
+ +
+
+
+
+ + + + + +{% endblock %} + diff --git a/flask/forest/templates/admin/list_services.html b/flask/forest/templates/admin/list_services.html new file mode 100644 index 0000000..f0557c3 --- /dev/null +++ b/flask/forest/templates/admin/list_services.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} + +{% block styles %} +{{ super() }} + +{% endblock %} + +{% block page_content %} +
+ {% include "admin/admin_tasks.html" %} + +
+
+
Services
+

+

+ + + + + + + + + + + + + {% for service in services %} + {% if service.enabled == False %} + + {% else %} + {% if service.warning == True %} + + {% else %} + + {% endif %} + {% endif %} + + + + + + + + {% endfor %} + +
OwnerCategoryDescriptionPriceLast ChargedDays Left
{{ service.owner.email }}{{ service.category }}{{ service.description }}{{ service.price }}{{ moment(service.date_last_charge).format('ll') }} ({{ moment(service.date_last_charge).fromNow() }}){{ service.daysleft }}
+
+
+
+ + +
+
+
+ +{% endblock %} + diff --git a/flask/forest/templates/admin/list_transactions.html b/flask/forest/templates/admin/list_transactions.html new file mode 100644 index 0000000..933a3ad --- /dev/null +++ b/flask/forest/templates/admin/list_transactions.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} + +{% block page_content %} +
+ + {% include "admin/admin_tasks.html" %} + +
+
+
All Transactions
+
+
+ + + + + + + + + + + + + {% for transaction in transactions %} + {% if transaction.value > 0 %} + + + + + + + {% else %} + + + + + + + {% endif %} + + + {% endfor %} +
IDDescriptionAmountDateUser
{{ transaction.pid }}{{ transaction.description }}{{ transaction.value }} {{ transaction.currency }}{{ moment(transaction.date_created).format('lll') }}{{ transaction.owner.email }}
{{ transaction.pid }}{{ transaction.description }}{{ transaction.value }} {{ transaction.currency }}{{ moment(transaction.date_created).format('lll') }}{{ transaction.owner.email }}
+ {% if transactions.has_prev %}<< Previous{% else %}<< Previous{% endif %} | + {% if transactions.has_next %}Next >>{% else %}Next >>{% endif %} +
+ +
+
+ + +
+ +{% endblock %} + diff --git a/flask/forest/templates/admin/list_users.html b/flask/forest/templates/admin/list_users.html new file mode 100644 index 0000000..72989c8 --- /dev/null +++ b/flask/forest/templates/admin/list_users.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} + +{% block page_content %} +
+ {% include "admin/admin_tasks.html" %} + +
+
+
List Active Users
+

+

+ + + + + + + + + + + + + {% for usr in users %} + + + + + + + + {% endfor %} + +
emaillast seenlast ipwalletcurrency
{{ usr.email }}{{ moment(usr.last_seen).format('lll') }}{{ usr.last_ip }}{{ usr.wallet }}{{ usr.currency }} + + +
+ {% if users.has_prev %}<< Previous{% else %}<< Previous{% endif %} | + {% if users.has_next %}Next >>{% else %}Next >>{% endif %} +
+
+
+ +
+ +
+
+ +{% endblock %} diff --git a/flask/forest/templates/admin/menu_cloud.html b/flask/forest/templates/admin/menu_cloud.html new file mode 100644 index 0000000..2f4422e --- /dev/null +++ b/flask/forest/templates/admin/menu_cloud.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/flask/forest/templates/admin/menu_deployments.html b/flask/forest/templates/admin/menu_deployments.html new file mode 100644 index 0000000..aa765cb --- /dev/null +++ b/flask/forest/templates/admin/menu_deployments.html @@ -0,0 +1,4 @@ + + + + diff --git a/flask/forest/templates/auth/2fa.html b/flask/forest/templates/auth/2fa.html new file mode 100644 index 0000000..5fb5058 --- /dev/null +++ b/flask/forest/templates/auth/2fa.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} + +{% block title %}2FA{% endblock %} + +{% block page_content %} + + +
+ {{ wtf.quick_form(form) }} +
+ +{% endblock %} diff --git a/flask/forest/templates/auth/already_confirmed.html b/flask/forest/templates/auth/already_confirmed.html new file mode 100644 index 0000000..1f7b742 --- /dev/null +++ b/flask/forest/templates/auth/already_confirmed.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} + +{% block title %}Вашият акаунт е вече потвърден.{% endblock %} + +{% block page_content %} + + +{% endblock %} diff --git a/flask/forest/templates/auth/change_password.html b/flask/forest/templates/auth/change_password.html new file mode 100644 index 0000000..617cc75 --- /dev/null +++ b/flask/forest/templates/auth/change_password.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} + +{% block title %}Change Password{% endblock %} + +{% block page_content %} + + +
+ {{ wtf.quick_form(form) }} +
+ +{% endblock %} diff --git a/flask/forest/templates/auth/email/adm_loginnotify.html b/flask/forest/templates/auth/email/adm_loginnotify.html new file mode 100644 index 0000000..a075577 --- /dev/null +++ b/flask/forest/templates/auth/email/adm_loginnotify.html @@ -0,0 +1,6 @@ +

{{ user.email }} logged in.
+
+IP Address: {{ ipaddr }}
+

+

Regards, +Proxadmin

diff --git a/flask/forest/templates/auth/email/adm_loginnotify.txt b/flask/forest/templates/auth/email/adm_loginnotify.txt new file mode 100644 index 0000000..b615312 --- /dev/null +++ b/flask/forest/templates/auth/email/adm_loginnotify.txt @@ -0,0 +1,6 @@ +User {{ user.email }} logged in. + +IP Address: {{ ipaddr }} + +Regards, +Proxadmin diff --git a/flask/forest/templates/auth/email/adm_regnotify.html b/flask/forest/templates/auth/email/adm_regnotify.html new file mode 100644 index 0000000..edd081f --- /dev/null +++ b/flask/forest/templates/auth/email/adm_regnotify.html @@ -0,0 +1,6 @@ +New user {{ user.email }} has been registered. + +IP Address: {{ ipaddr }} + +Regards, +Proxadmin diff --git a/flask/forest/templates/auth/email/adm_regnotify.txt b/flask/forest/templates/auth/email/adm_regnotify.txt new file mode 100644 index 0000000..edd081f --- /dev/null +++ b/flask/forest/templates/auth/email/adm_regnotify.txt @@ -0,0 +1,6 @@ +New user {{ user.email }} has been registered. + +IP Address: {{ ipaddr }} + +Regards, +Proxadmin diff --git a/flask/forest/templates/auth/email/confirm.html b/flask/forest/templates/auth/email/confirm.html new file mode 100644 index 0000000..756412a --- /dev/null +++ b/flask/forest/templates/auth/email/confirm.html @@ -0,0 +1,7 @@ +

Dear Customer,

+

To confirm your account please click here.

+

Alternatively, you can paste the following link in your browser's address bar:

+

{{ url_for('auth.confirm', token=token, _external=True) }}

+

Sincerely,

+

Datapoint.bg

+

Note: replies to this email address are not monitored.

diff --git a/flask/forest/templates/auth/email/confirm.txt b/flask/forest/templates/auth/email/confirm.txt new file mode 100644 index 0000000..c8578ea --- /dev/null +++ b/flask/forest/templates/auth/email/confirm.txt @@ -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. diff --git a/flask/forest/templates/auth/email/reset_password.html b/flask/forest/templates/auth/email/reset_password.html new file mode 100644 index 0000000..a531879 --- /dev/null +++ b/flask/forest/templates/auth/email/reset_password.html @@ -0,0 +1,5 @@ +

Dear {{ user.username }},

+

To reset your password click here.

+

Alternatively, you can paste the following link in your browser's address bar:

+

If you have not requested a password reset simply ignore this message.

+

Note: replies to this email address are not monitored.

diff --git a/flask/forest/templates/auth/email/reset_password.txt b/flask/forest/templates/auth/email/reset_password.txt new file mode 100644 index 0000000..d50cf1c --- /dev/null +++ b/flask/forest/templates/auth/email/reset_password.txt @@ -0,0 +1,9 @@ +Dear {{ user.username }}, + +To reset your password click on the following link: + +{{ url_for('auth.password_reset', token=token, _external=True) }} + +If you have not requested a password reset simply ignore this message. + +Note: replies to this email address are not monitored. diff --git a/flask/forest/templates/auth/login.html b/flask/forest/templates/auth/login.html new file mode 100644 index 0000000..86265ce --- /dev/null +++ b/flask/forest/templates/auth/login.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} + +{% block title %}Login{% endblock %} + +{% block page_content %} + + +
+ {{ wtf.quick_form(form) }} +
+

Forgot your password? Click here to reset it.

+

New user? Click here to register.

+
+ +{% endblock %} diff --git a/flask/forest/templates/auth/register.html b/flask/forest/templates/auth/register.html new file mode 100644 index 0000000..f2dd1b4 --- /dev/null +++ b/flask/forest/templates/auth/register.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} + +{% block title %}Register{% endblock %} + +{% block page_content %} + + + +
+ {{ wtf.quick_form(form) }} +
+
+ +{% endblock %} diff --git a/flask/forest/templates/auth/reset_password.html b/flask/forest/templates/auth/reset_password.html new file mode 100644 index 0000000..d8a1d06 --- /dev/null +++ b/flask/forest/templates/auth/reset_password.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} + +{% block title %}Password Reset{% endblock %} + +{% block page_content %} + + +
+ {{ wtf.quick_form(form) }} +
+ +{% endblock %} + diff --git a/flask/forest/templates/auth/unconfirmed.html b/flask/forest/templates/auth/unconfirmed.html new file mode 100644 index 0000000..fb28648 --- /dev/null +++ b/flask/forest/templates/auth/unconfirmed.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} + +{% block title %}Confirm your account{% endblock %} + +{% block page_content %} + + +{% endblock %} diff --git a/flask/forest/templates/base.html b/flask/forest/templates/base.html new file mode 100644 index 0000000..6f05f27 --- /dev/null +++ b/flask/forest/templates/base.html @@ -0,0 +1,54 @@ +{% extends "bootstrap/base.html" %} + +{% import "bootstrap/wtf.html" as wtf %} +{% import "bootstrap/fixes.html" as fixes %} +{% import "bootstrap/utils.html" as util %} + +{% block title %}Cloud Builder - Datapoint.bg{% endblock %} + +{% block head %} +{{ super() }} + + +{% endblock %} + +{% block styles %} +{{ super() }} + + + + + + +{% endblock %} + +{% block scripts %} +{{ super() }} +{{ moment.lang("bg") }} +{{ moment.include_moment() }} +{% endblock %} + +{% block navbar %} +{% include "nav.html" %} +{% endblock %} + +{% block content %} + {% block outside_container %}{% endblock %} +
+
+ {% for message in get_flashed_messages() %} +
+ + {{ message }} +
+ {% endfor %} + + {% block page_content %}{% endblock %} +
+
+ +{% block footer %} +{% endblock %} + +{% endblock %} + diff --git a/flask/forest/templates/email/adm_logger.html b/flask/forest/templates/email/adm_logger.html new file mode 100644 index 0000000..bc6908b --- /dev/null +++ b/flask/forest/templates/email/adm_logger.html @@ -0,0 +1,8 @@ +

User: {{ user.email }} notified.
+

+

+Action taken: {{ content }}
+

+

Regards,
+Datapoint.bg +

diff --git a/flask/forest/templates/email/adm_logger.txt b/flask/forest/templates/email/adm_logger.txt new file mode 100644 index 0000000..9017b59 --- /dev/null +++ b/flask/forest/templates/email/adm_logger.txt @@ -0,0 +1,6 @@ +User {{ user.email }} notified. + +Action taken: {{ content }} + +Regards, +Proxadmin diff --git a/flask/forest/templates/email/client_logger.html b/flask/forest/templates/email/client_logger.html new file mode 100644 index 0000000..c1891f4 --- /dev/null +++ b/flask/forest/templates/email/client_logger.html @@ -0,0 +1,14 @@ +

Dear client,
+

+ +

{{ content }} +

+ +

Regards,
+Datapoint.bg
+
+52 Volga str.
+4002 Plovdiv, Bulgaria
+tel: +359 323 982 95
+office@datapoint.bg
+

diff --git a/flask/forest/templates/email/client_logger.txt b/flask/forest/templates/email/client_logger.txt new file mode 100644 index 0000000..ae71831 --- /dev/null +++ b/flask/forest/templates/email/client_logger.txt @@ -0,0 +1,11 @@ +Dear client, + +{{ content }} + +Regards, +Datapoint.bg + +52 Volga str. +4002 Plovdiv, Bulgaria +tel: +359 323 982 95 +office@datapoint.bg diff --git a/flask/forest/templates/errors/403.html b/flask/forest/templates/errors/403.html new file mode 100644 index 0000000..0b99580 --- /dev/null +++ b/flask/forest/templates/errors/403.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block title %}Forbidden{% endblock %} + +{% block page_content %} + + +
+     .-------.
+    / .-----. \
+   / /       \ \
+   | |       | |
+  _| |_______| |_
+.' |_|       |_| '.
+'._____ ___ _____.'
+|     .'___'.     |
+'.__.'.'   '.'.__.'
+'.__  | 403 |  __.'
+|   '.'.___.'.'   |
+'.____'.___.'____.'
+'._______________.'
+
+
+{% endblock %} diff --git a/flask/forest/templates/errors/404.html b/flask/forest/templates/errors/404.html new file mode 100644 index 0000000..c0872c8 --- /dev/null +++ b/flask/forest/templates/errors/404.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% block title %}Page Not Found{% endblock %} + +{% block page_content %} + + +
+                     %%%%
+                    %%%%-(
+                  _%%%%%_/                        \ ' /
+                _%%%%%%%%                        - (_) -
+              _%%%%%%%/ \%                        / , \
+             %%%%%%%%%\\ \_
+               %%%%%%   \ \\
+                   )    /\_/
+                 /(___. \
+                 '----' (
+                /       )
+    ---....____/        (_____ __ _ ___ ___ __ _ _ _____ _ _ ___
+              /         )---...___ =-= = -_= -=_= _-=_-_ -=- =-_
+            ,'          (         ```--.._= -_= -_= _-=- -_= _=-
+         ,-'            )                 ``--._=-_ =-=_-= _-= _
+         '-._    '-..___(                       ``-._=_-=_- =_-=
+             ``---....__)                            `-._-=_-_=-
+                   )|)|                                  `-._=-_
+        gnv       '-'-.\_                                    `-.
+
+
+{% endblock %} diff --git a/flask/forest/templates/errors/500.html b/flask/forest/templates/errors/500.html new file mode 100644 index 0000000..b6299e4 --- /dev/null +++ b/flask/forest/templates/errors/500.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block title %}Internal Server Error{% endblock %} + +{% block page_content %} + + +
+        .--.       .--.
+    _  `    \     /    `  _
+     `\.===. \.^./ .===./`
+            \/`"`\/
+         ,  | 500 |  ,
+        / `\|;-.-'|/` \
+       /    |::\  |    \
+    .-' ,-'`|:::; |`'-, '-.
+        |   |::::\|   | 
+        |   |::::;|   |
+        |   \:::://   |
+        |    `.://'   |
+       .'             `.
+    _,'                 `,_
+
+
+{% endblock %} diff --git a/flask/forest/templates/errors/503.html b/flask/forest/templates/errors/503.html new file mode 100644 index 0000000..9f842ba --- /dev/null +++ b/flask/forest/templates/errors/503.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block title %}Forbidden{% endblock %} + +{% block page_content %} + + +
+         ,-.
+        / \  `.  __..-,O
+       :   \ --''_..-'.'
+       |    . .-' `. '.
+       :     .     .`.'
+        \     `.  /  ..
+         \      `.   ' .
+          `,       `.   \
+         ,|,`.        `-.\
+        '.||  ``-...__..-`
+         |  |
+         |__|
+         /||\
+        //||\\
+       // || \\
+    __//__||__\\__
+   '--------------' SSt
+
+
+ diff --git a/flask/forest/templates/errors/csrf_error.html b/flask/forest/templates/errors/csrf_error.html new file mode 100644 index 0000000..9fa91b8 --- /dev/null +++ b/flask/forest/templates/errors/csrf_error.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block title %}CSRF Error{% endblock %} + +{% block page_content %} + + +
+ ad8888888888ba
+dP'         `"8b,
+8  ,aaa,       "Y888a     ,aaaa,     ,aaa,  ,aa,
+8  8' `8           "8baaaad""""baaaad""""baad""8b
+8  8   8              """"      """"      ""    8b
+8  8, ,8         ,aaaaaaaaaaaaaaaaaaaaaaaaddddd88P
+8  `"""'       ,d8""
+Yb,         ,ad8"     nv
+ "Y8888888888P"                       
+
+
+{% endblock %} diff --git a/flask/forest/templates/main/aboutus.html b/flask/forest/templates/main/aboutus.html new file mode 100644 index 0000000..fc6511c --- /dev/null +++ b/flask/forest/templates/main/aboutus.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} + +{% block styles %} +{{ super() }} + + +{% endblock %} + + +{% block page_content %} +

About us

+
+
+ +
+ +
+         ,-.
+        / \  `.  __..-,O
+       :   \ --''_..-'.'
+       |    . .-' `. '.
+       :     .     .`.'
+        \     `.  /  ..
+         \      `.   ' .
+          `,       `.   \
+         ,|,`.        `-.\
+        '.||  ``-...__..-`
+         |  |
+         |__|
+         /||\
+        //||\\
+       // || \\
+    __//__||__\\__
+   '--------------' SSt
+
+ + +

datapoint.bg е проект на Новахостинг ЕООД който създадохме, за да предоставим на потребителите ново поколение изчислителни услуги. Стремим се да помагаме на неуверените и неопитни потребители в първата им среща с тях, като сме на линия за съвети отновно одимите услуги и инструменти, за да не влагате излишни средства в ресурси.

+

Историята

+

Този сайт е естествено продължение на няколко лични проекта на двама ентусиасти, на едно пространство, използвано по много предназначения и наричано от нас просто “избата“. Там е мястото, където се затваряхме, за да свършим малко работа бързо и без никой да знае къде сме. От там са тръгнали много идеи, за да оживеят в сървърното помещение, което изградихме първоначално за собствена употреба. После пораснахме и дойдоха идеите, а след тях и клиентите ни. Както споменах естественото продължение на нещата беше да затворим цялостно процеса на работа и усъвършенствахме сървърното помещение, за да е пригодно за крайни клиенти. Докато се усетим имахме готово чисто ново сървърно помещение- мечта за всеки системен администратор. С мощни сървъри, гъвкава мрежова инфраструктура, резервирана свързаност с най- големите български интернет доставчици, резервирано захранване, климатични системи, температурен контрол, цялостен мониторинг и ново поколение охранителна система.

+

Мястото

+

За да сме мотивирани, не просто работим. Вършим задачите си с удоволствие в най- приятния офис – малкият двор на “избата“. Това място за нас е свещенно – то ни зарежда и мотивира, там си почиваме, провеждаме работните си срещи и обсъждаме работата си.

+

Екипът

+

Като модерна и устремена към успех компания, в нашата структура няма строго определено йерархично ниво. Вскички сме ХОРА, равни пред идеите си.

+
+ +{% endblock %} diff --git a/flask/forest/templates/main/domaincheck.html b/flask/forest/templates/main/domaincheck.html new file mode 100644 index 0000000..23b7dc4 --- /dev/null +++ b/flask/forest/templates/main/domaincheck.html @@ -0,0 +1,62 @@ +

+ +

+ + + + + . + + + + + +
diff --git a/flask/forest/templates/main/footer_colored.html b/flask/forest/templates/main/footer_colored.html new file mode 100644 index 0000000..4d85176 --- /dev/null +++ b/flask/forest/templates/main/footer_colored.html @@ -0,0 +1,7 @@ +