commit be76b6622f9fbb67e4efe622d4a9ac9d68a40e2d Author: deflax Date: Wed Mar 8 20:53:09 2017 +0200 initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7ac2761 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +Copyright (c) 2015-2016 deflax + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgement in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8d793d --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# proxmaster-admin +web panel for proxmaster built with Flask + +setup nginx vhosts: +example.com.conf: + +``` +server { + listen 80; + server_name panel.example.com; + root /var/www/html; + location / { + } +} +``` + +example.com-ssl.conf: +``` +server { + listen 443 ssl; + server_name EXAMPLE.com; + + ssl_certificate /etc/letsencrypt/live/EXAMPLE.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/EXAMPLE.com/privkey.pem; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_dhparam /etc/letsencrypt/dhparam.pem; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:50m; + ssl_stapling on; + ssl_stapling_verify on; + add_header Strict-Transport-Security max-age=15768000; + + + location / { + proxy_pass http://127.0.0.1:5000$request_uri; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + } + + location /novnc { + alias /home/USER/appserver/noVNC; + } +``` + +setup db backend: +1. apt-get install postgresql postgresql-contrib libpq-dev +2. sudo -i -u postgres psql +3. create user proxadmin with password 'mypassword'; +4. create database proxadmin owner proxadmin encoding 'utf-8'; + +setup panel +1. adduser USER +2. cd /home/USER +3. virtualenv -p python3 appserver +4. cd appserver +5. git clone git://github.com/kanaka/noVNC +6. git clone https://deflax@bitbucket.org/deflax/proxmaster-panel.git +7. source bin/activate +8. cd proxmaster-panel/ ; pip install -r requirements.txt +9. python3 manage.py db init ; python3 manage.py db migrate -m "init" ; python3 manage.py db upgrade ; python3 manage.py deploy + +start: +1. crontab -e +2. @reboot /usr/bin/screen -dmS proxadmin /home/proxadmin/appserver/proxmaster-panel/start.sh + + diff --git a/example_apache_vhost.conf b/example_apache_vhost.conf new file mode 100644 index 0000000..2cf5938 --- /dev/null +++ b/example_apache_vhost.conf @@ -0,0 +1,45 @@ + + + ServerAdmin support@example.com + ServerName www.example.com + ServerAlias example.com + + WSGIDaemonProcess proxadmin user=proxadmin group=proxadmin threads=5 + WSGIScriptAlias / /home/proxadmin/appserver/proxmaster-panel/start.wsgi + + + + Require all granted + + + + + WSGIProcessGroup proxadmin + WSGIApplicationGroup %{GLOBAL} + WSGIScriptReloading On + Require all granted + Order allow,deny + Allow from all + + + Alias /static /home/proxadmin/appserver/proxmaster-panel/app/static + + Require all granted + Order allow,deny + Allow from all + + + ErrorLog ${APACHE_LOG_DIR}/www.example.com-error.log + + # Possible values include: debug, info, notice, warn, error, crit, + # alert, emerg. + LogLevel warn + + CustomLog ${APACHE_LOG_DIR}/www.example.com-access.log combined + + Include /etc/letsencrypt/options-ssl-apache.conf + SSLCertificateFile /etc/letsencrypt/live/www.example.com/cert.pem + SSLCertificateKeyFile /etc/letsencrypt/live/www.example.com/privkey.pem + SSLCertificateChainFile /etc/letsencrypt/live/www.example.com/chain.pem + + diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..726027b --- /dev/null +++ b/manage.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +import os +import subprocess, shlex +from proxadmin import app, db +from flask_script import Manager, Shell, Command +from flask_migrate import Migrate, MigrateCommand + +def make_shell_context(): + return dict(app=app, + db=db, + User=User, + Role=Role, + Permission=Permission, + Deployment=Deployment) + +migrate = Migrate(app, db) +manager = Manager(app) +manager.add_command('shell', Shell(make_context=make_shell_context)) +manager.add_command('db', MigrateCommand) + +@manager.command +def deploy(): + """Run deployment tasks.""" + from flask_migrate import upgrade + from app.models import Role, User, Deployment, Product + + # migrate database to latest revision + upgrade() + + # create user roles + Role.insert_roles() + Product.insert_products() + +@manager.command +@manager.option('-r' '--restore_file', help='Restore from grid dump file') +def restore(restore_file): + """ recreate db from grid export with python3 manage.py restore /path/grid.tar.bz2 """ + print(str(restore_file)) + #TODO + from app.models import User + db.session.add(User(email=str(user), password=str(password), confirmed=True, confirmed_on=datetime.datetime.now())) + db.session.commit() + +def run_scheduler(): + command_line = 'python3 /home/proxadmin/appserver/proxmaster-panel/schedulerd.py' + args = shlex.split(command_line) + p = subprocess.Popen(args) + +@manager.command +def charge_deployments(): + from app.models import Product, Deployment, User + Deployment.charge() + +@manager.command +def charge_contracts(): + from app.models import Service, Contract, User + Contract.charge() + +@manager.command +def charge_domains(): + from app.models import Domain, User + Domain.charge() + +@manager.command +def runserver(): + print('Starting Scheduler...') + #run_scheduler() + + print('Starting Flask...') + app.run() + +if __name__ == '__main__': + manager.run() + + diff --git a/proxadmin/__init__.py b/proxadmin/__init__.py new file mode 100644 index 0000000..b0b351e --- /dev/null +++ b/proxadmin/__init__.py @@ -0,0 +1,103 @@ +from flask import Flask, g, render_template, request +from flask_bootstrap import Bootstrap +from flask_mail import Mail +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from flask_wtf.csrf import CSRFProtect, CSRFError +from flask_babel import Babel +from werkzeug.contrib.fixers import ProxyFix +from proxadmin import configure_app + +app = Flask(__name__) + +configure_app(app) + +app.wsgi_app = ProxyFix(app.wsgi_app) #trusting headers behind proxy + +db = SQLAlchemy(session_options = { "autoflush": False }) +lm = LoginManager() +lm.login_view = 'auth.login' +lm.login_message = 'Login Required.' +lm.session_protection = 'strong' +#lm.session_protection = 'basic' +mail = Mail() +bootstrap = Bootstrap() +#csrf = CSRFProtect() +babel = Babel() + +app = Flask(__name__) +app.config.from_object('config') + +#if app.debug: +# pass +# #toolbar = DebugToolbarExtension(app) +#else: +# import logging +# from logging.handlers import RotatingFileHandler +# file_handler = RotatingFileHandler('app/log/proxadmin.log', 'a', 1 * 1024 * 1024, 10) +# file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')) +# app.logger.setLevel(logging.INFO) +# file_handler.setLevel(logging.INFO) +# app.logger.addHandler(file_handler) +# app.logger.info('Proxadmin started.') + +bootstrap.init_app(app) +mail.init_app(app) +db.init_app(app) +lm.init_app(app) +babel.init_app(app) +#csrf.init_app(app) + +from .proxadmin import proxadmin as proxadmin_blueprint +app.register_blueprint(proxadmin_blueprint) + +from .auth import auth as auth_blueprint +app.register_blueprint(auth_blueprint, url_prefix='/auth') + +from .uinvoice import uinvoice as uinvoice_blueprint +app.register_blueprint(uinvoice_blueprint, url_prefix='/uinvoice') + +@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(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 __name__ == '__main__': + app.run() + diff --git a/proxadmin/auth/__init__.py b/proxadmin/auth/__init__.py new file mode 100644 index 0000000..3785107 --- /dev/null +++ b/proxadmin/auth/__init__.py @@ -0,0 +1,4 @@ +from flask import Blueprint +auth = Blueprint('auth', __name__) +from . import routes + diff --git a/proxadmin/auth/forms.py b/proxadmin/auth/forms.py new file mode 100644 index 0000000..2657fa2 --- /dev/null +++ b/proxadmin/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('Електронна Поща', [validators.DataRequired(), validators.Length(1,64), validators.Email()]) + password = PasswordField('Парола', [validators.DataRequired(), validators.Length(1,128)]) + remember_me = BooleanField('Запомни ме') + #recaptcha = RecaptchaField() + submit = SubmitField('Вход') + +class TwoFAForm(FlaskForm): + token = StringField('Token', [validators.DataRequired(), validators.Length(6, 6)]) + submit = SubmitField('Потвърди кода') + +class RegistrationForm(FlaskForm): + email = StringField('Електронна Поща', [validators.DataRequired(), validators.Length(6,35), validators.Email()]) + def validate_email(self, field): + if User.query.filter_by(email=field.data).first(): + raise ValidationError('Грешка. Опитайте пак с друг email адрес.') + password = PasswordField('Парола', [validators.DataRequired(), validators.EqualTo('confirm', message='Паролите трябва да съвпадат')]) + confirm = PasswordField('Повторете паролата', [validators.DataRequired()]) + accept_tos = BooleanField('Приемам Условията за Използване на услугата', [validators.DataRequired()]) + recaptcha = RecaptchaField() + submit = SubmitField('Регистрация') + +class ChangePasswordForm(FlaskForm): + old_password = PasswordField('Стара парола', [validators.DataRequired()]) + password = PasswordField('Нова Парола', [validators.DataRequired(), validators.EqualTo('confirm', message='Паролите трябва да съвпадат')]) + confirm = PasswordField('Повторете паролата') + submit = SubmitField('Обнови паролата') + +class PasswordResetRequestForm(FlaskForm): + email = EmailField('Електронна Поща', [validators.DataRequired(), validators.Length(1,64), validators.Email()]) + recaptcha = RecaptchaField() + submit = SubmitField('Възстановяване на парола', [validators.DataRequired()]) + +class PasswordResetForm(FlaskForm): + email = EmailField('Електронна Поща', [validators.DataRequired(), validators.Length(1,64), validators.Email()]) + password = PasswordField('Парола', [validators.DataRequired(), validators.EqualTo('confirm', message='Паролите трябва да съвпадат')]) + confirm = PasswordField('Повторете паролата', [validators.DataRequired()]) + submit = SubmitField('Промяна на паролата') + + def validate_email(self, field): + if User.query.filter_by(email=field.data).first() is None: + raise ValidationError('Грешка. Опитайте пак с друг email адрес.') + diff --git a/proxadmin/auth/routes.py b/proxadmin/auth/routes.py new file mode 100644 index 0000000..b0f9f13 --- /dev/null +++ b/proxadmin/auth/routes.py @@ -0,0 +1,220 @@ +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 +from ..email import send_email +from .forms import LoginForm, TwoFAForm, RegistrationForm, ChangePasswordForm,PasswordResetRequestForm, PasswordResetForm + +from io import BytesIO +import pyqrcode + +@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('proxadmin.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.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) + 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('proxadmin.dashboard')) + 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('proxadmin.dashboard')) + 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('proxadmin.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) + db.session.add(user) + 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('proxadmin.index')) + if current_user.confirm(token): + flash('Вашият акаунт е потвърден. Благодаря!') + else: + flash('Времето за потвърждение на вашият код изтече.') + return redirect(url_for('proxadmin.index')) + + +@auth.route('/confirm') +@login_required +def resend_confirmation(): + token = current_user.generate_confirmation_token() + send_email(current_user.email, 'Потвърдете_вашият_акаунт', + 'auth/email/confirm', user=current_user, token=token) + flash('Изпратен е нов код за потвърждение..') + return redirect(url_for('proxadmin.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('Вашата парола беше променена.') + return redirect(url_for('proxadmin.index')) + else: + flash('Грешна парола.') + 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('proxadmin.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('proxadmin.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('proxadmin.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('proxadmin.index')) + return render_template('auth/reset_password.html', form=form) + + diff --git a/proxadmin/decorators.py b/proxadmin/decorators.py new file mode 100644 index 0000000..73ea9c5 --- /dev/null +++ b/proxadmin/decorators.py @@ -0,0 +1,20 @@ +from functools import wraps +from flask import abort +from flask_login import current_user +from .models import Permission + + +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/proxadmin/email.py b/proxadmin/email.py new file mode 100644 index 0000000..3d13e6e --- /dev/null +++ b/proxadmin/email.py @@ -0,0 +1,20 @@ +from threading import Thread +from flask import current_app, render_template +from flask_mail import Message +from . import mail + + +def send_async_email(app, msg): + with app.app_context(): + mail.send(msg) + + +def send_email(to, subject, template, **kwargs): + app = current_app._get_current_object() + msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + ' ' + subject, 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/proxadmin/exceptions.py b/proxadmin/exceptions.py new file mode 100644 index 0000000..2851fa7 --- /dev/null +++ b/proxadmin/exceptions.py @@ -0,0 +1,2 @@ +class ValidationError(ValueError): + pass diff --git a/proxadmin/models.py b/proxadmin/models.py new file mode 100644 index 0000000..21d8bfc --- /dev/null +++ b/proxadmin/models.py @@ -0,0 +1,415 @@ +import hashlib +from werkzeug.security import generate_password_hash, check_password_hash +from itsdangerous import TimedJSONWebSignatureSerializer as Serializer + +from config import Config +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 base64 +import json +import requests +import onetimepass +from datetime import date, datetime, time, timedelta +from sortedcontainers import SortedDict + + +class Permission: + DEPLOY = 0x01 + ADMINISTER = 0x80 + + +class Role(db.Model): + __tablename__ = 'roles' + pid = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(64), 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': (0xff, 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' + + pid = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(64), unique=True, index=True) + role_id = db.Column(db.Integer, db.ForeignKey('roles.pid')) #FK + password_hash = db.Column(db.String(128)) + + confirmed = db.Column(db.Boolean, default=False) + member_since = db.Column(db.DateTime(), default=datetime.utcnow) + last_seen = db.Column(db.DateTime(), default=datetime.utcnow) + last_ip = db.Column(db.String(128)) + twofactor = db.Column(db.Boolean, default=False) #optional 2factor auth + otp_secret = db.Column(db.String(16)) + avatar_hash = db.Column(db.String(32)) + + name = db.Column(db.Unicode(256)) + address = db.Column(db.Unicode(256)) + city = db.Column(db.Unicode(64)) + postcode = db.Column(db.String(10)) + country = db.Column(db.String(64)) + phone = db.Column(db.String(64)) + org_responsible = db.Column(db.Unicode(128)) + org_bulstat = db.Column(db.String(16)) + + inv_deployments = db.relationship('Deployment', backref='owner', lazy='dynamic') + inv_contracts = db.relationship('Contract', backref='owner', lazy='dynamic') + inv_domains = db.relationship('Domain', backref='owner', lazy='dynamic') + + currency = db.Column(db.String(3), default='BGN') + + 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.email is not None and self.avatar_hash is 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') + + @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): + return self.can(Permission.ADMINISTER) + + 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)) + + +def contact_proxmaster(data, method, cubeid=0): + url = current_app.config['PROXMASTER_URL'] + data['apikey'] = current_app.config['APIKEY'] + data_json = json.dumps(data) + #print('--> {}'.format(data)) + + if method == 'vmcreate': + url = '{}/{}'.format(url, method) + else: + url = '{}/{}/{}'.format(url, method, cubeid) + + db_result = requests.post( url, data=data_json, headers={"content-type": "application/json"}, timeout=30 ) + + try: + proxjson = db_result.json() + #print('proxmaster query {}'.format(str(proxjson))) + return proxjson + except: + return None + + +#TEMPLATE CLASSES +class Product(db.Model): + __tablename__ = 'products' + pid = db.Column(db.Integer, primary_key=True) #PK + group = db.Column(db.Integer) + name = db.Column(db.String(64)) + image = db.Column(db.String(128)) + description = db.Column(db.String(128)) + cpu = db.Column(db.Integer) #default cpu + mem = db.Column(db.Integer) #default mem + hdd = db.Column(db.Integer) #default hdd + recipe = db.Column(db.String(128)) #defaut template name + enabled = db.Column(db.Boolean) + + @staticmethod + def insert_products(): + products = current_app.config['PRODUCTS'] + for p in products: + product = Product.query.filter_by(pid=p).first() + if product is None: + product = Product(name=p) + + #insert default values + product.group = products[p][0] + product.name = products[p][1] + product.image = products[p][2] + product.description = products[p][3] + product.cpu = products[p][4] + product.mem = products[p][5] + product.hdd = products[p][6] + product.recipe = products[p][7] + product.enabled = products[p][8] + db.session.add(product) + db.session.commit() + + @staticmethod + def get_products(): + result = Product.query.all() + products = {} + for product in result: + if product.enabled == True: + products[int(product.pid)] = { 'group': product.group, + 'img': '/static/images/' + product.image, + 'name': product.name, + 'description': product.description, + 'cpu': product.cpu, + 'mem': product.mem, + 'hdd': product.hdd, + 'recipe': product.recipe, + } + return products + +class Service(db.Model): + __tablename__ = 'services' + pid = db.Column(db.Integer, primary_key=True) #PK + name = db.Column(db.String(64)) + image = db.Column(db.String(128)) + description = db.Column(db.String(128)) + unitprice = db.Column(db.Float) + enabled = db.Column(db.Boolean) + + @staticmethod + def insert_services(): + services = current_app.config['SERVICES'] + for s in services: + service = Service.query.filter_by(pid=p).first() + if service is None: + service = Service(name=s) + + #insert default values + service.name = products[p][1] + service.image = products[p][2] + service.description = products[p][3] + service.unitprice = products[p][4] + service.enabled = products[p][5] + db.session.add(service) + db.session.commit() + + @staticmethod + def get_services(): + result = Service.query.all() + services = {} + for service in result: + if service.enabled == True: + services[int(service.pid)] = {'img': '/static/images/' + service.image, + 'name': service.name, + 'description': service.description, + 'unitprice': service.unitprice + } + return services + +class Order(db.Model): + __tablename__ = 'orders' + pid = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK + date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow) + units = db.Column(db.Integer, default=1) + unitvalue = db.Column(db.Float) + currency = db.Column(db.String, default='BGN') + paid = db.Column(db.Boolean, default=False) + +#class Invoice(db.Model) + +#INVENTORY CLASSES +class Deployment(db.Model): + __tablename__ = 'deployments' + pid = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK + product_id = db.Column(db.Integer, db.ForeignKey('products.pid')) #FK + date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow) + date_expire = db.Column(db.DateTime) + enabled = db.Column(db.Boolean) + + machine_id = db.Column(db.BigInteger) #cubeid + machine_alias = db.Column(db.String) #dns name for easy managing + machine_cpu = db.Column(db.Integer) + machine_mem = db.Column(db.Integer) + machine_hdd = db.Column(db.Integer) + credit = db.Column(db.Float) + + def charge(): + result = Deployment.query.all() + for deploy in result: + managed_user = User.query.get(deploy.user_id) + db.session.add(deploy) + if datetime.utcnow.date() > (deploy.date_expire - timedelta(days=10)): + if deploy.credit > 0: + print('{} Deployment {} will expire in 10 days at {}. Creating new order...'.format(managed_user.email, deploy.machine_alias, deploy.date_expire)) + current_product = Product.query.get(int(deploy.product_id)) + order = Order(user_id=managed_user.pid, unitvalue=deploy.credit, description='Cloud server {} - {}'.format(current_product.name, deploy.machine_alias)) + db.session.add(order) + deploy.credit = 0 + deploy.data_expire = datetime.utcnow.date() + timedelta(days=30) + else: + cpu_cost = deploy.machine_cpu * current_app.config['CPU_RATIO'] + mem_cost = ( deploy.machine_mem / 1024 ) * current_app.config['MEM_RATIO'] + hdd_cost = deploy.machine_hdd * current_app.config['HDD_RATIO'] + total = cpu_cost + mem_cost + hdd_cost + if deploy.enabled == True: + print('{}> Charging deployment #{} with {} ({} monthly)'.format(managed_user.email, deploy.pid, total)) + deploy.credit += total + db.session.commit() + return True + +class Contract(db.Model): + __tablename__ = 'contracts' + pid = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK + service_id = db.Column(db.Integer, db.ForeignKey('services.pid')) #FK + date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow) + date_expire = db.Column(db.DateTime) + enabled = db.Column(db.Boolean) + + description = db.Column(db.String) + units = db.Column(db.Integer) + discount = db.Column(db.Integer) #percent + credit = db.Column(db.Float) + + def charge(): + result = Contract.query.all() + for contract in result: + managed_user = User.query.get(contract.user_id) + db.session.add(contract) + if datetime.utcnow.date() > (contract.date_expire - timedelta(days=10)): + if contract.enabled == True: + print('{}> Contract {} will expire in 10 days at {}. Creating new order...'.format(managed_user.email, contract.pid, contract.date_expire)) + current_service = Service.query.get(int(contract.product_id)) + order = Order(user_id=managed_user.id, units=contract.units, unitvalue=(current_service.unitprice * contract.units), description=current_service.name) + db.session.add(order) + contract.data_expire = datetime.utcnow.date() + timedelta(days=30) + db.session.commit() + return True + +class Domain(db.Model): + __tablename__ = 'domains' + pid = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK + date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow) + date_expire = db.Column(db.DateTime) + enabled = db.Column(db.Boolean) + + fqdn = db.Column(db.String, unique=True) + auto_update = db.Column(db.Boolean) + credit = db.Column(db.Float) + + def charge(): + result = Domain.query.all() + for domain in result: + managed_user = User.query.get(domain.user_id) + db.session.add(domain) + if datetime.utcnow.date() > (domain.date_expire - timedelta(days=60)): + if domain.enabled == True: + print('{}> Domain {} will expire in 60 days at {}. Creating new order...'.format(managed_user.email, domain.fqdn, domain.date_expire)) + order = Order(user_id=managed_user.id, unitvalue=25, description=domain.fqdn) + db.session.add(order) + db.session.commit() + return True + diff --git a/proxadmin/proxadmin/__init__.py b/proxadmin/proxadmin/__init__.py new file mode 100644 index 0000000..8f5edd7 --- /dev/null +++ b/proxadmin/proxadmin/__init__.py @@ -0,0 +1,3 @@ +from flask import Blueprint +proxadmin = Blueprint('proxadmin', __name__) +from . import routes diff --git a/proxadmin/proxadmin/errors.py b/proxadmin/proxadmin/errors.py new file mode 100644 index 0000000..ac59ca9 --- /dev/null +++ b/proxadmin/proxadmin/errors.py @@ -0,0 +1,33 @@ +from flask import render_template, request, jsonify +from . import proxadmin + + +@proxadmin.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('403.html'), 403 + + +@proxadmin.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('404.html'), 404 + + +@proxadmin.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('500.html'), 500 + diff --git a/proxadmin/proxadmin/forms.py b/proxadmin/proxadmin/forms.py new file mode 100644 index 0000000..57b5d60 --- /dev/null +++ b/proxadmin/proxadmin/forms.py @@ -0,0 +1,24 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectField, DecimalField +from wtforms import validators, ValidationError +from wtforms.fields.html5 import EmailField + + +class DeployForm(FlaskForm): + servername = StringField('Име/Домейн:', [validators.Regexp(message='пример: 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})$')]) + region = SelectField('Регион:') + vmpassword = StringField('Парола:', [validators.DataRequired()]) + cpu = StringField('Процесорни ядра:') + mem = StringField('Памет:') + hdd = StringField('Дисково пространство:') + recipe = SelectField('Рецепта') + #ipv4 = SelectField('Брой публични IP адреса', choices=[('1', '1'),('2', '2' ), ('3', '3')]) + invite_key = StringField('Покана', [validators.DataRequired(), validators.Length(6,35)]) + + def validate_invite_key(self, field): + if field.data != 'inv1919': + raise ValidationError('Denied') + + submit = SubmitField('Deploy') + + diff --git a/proxadmin/proxadmin/routes.py b/proxadmin/proxadmin/routes.py new file mode 100644 index 0000000..6f691ac --- /dev/null +++ b/proxadmin/proxadmin/routes.py @@ -0,0 +1,245 @@ +from config import Config +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 proxadmin +from .forms import DeployForm +from .. import db +from ..email import send_email +from ..models import User, Role, Product, Deployment, contact_proxmaster, Contract, Domain +from ..decorators import admin_required, permission_required + +import base64 +import string +import random +from datetime import datetime, timedelta, date, time + +def randstr(n): + return ''.join(random.SystemRandom().choice(string.ascii_lowercase + string.digits) for _ in range(n)) + +#@proxadmin.before_app_request +#def before_request(): +# g.user = current_user +# print('current_user: %s, g.user: %s, leaving bef_req' % (current_user, g.user)) + +@proxadmin.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 + +#STATIC PAGES +@proxadmin.route("/", methods=['GET']) +def index(): + return render_template('proxadmin/index.html') + +@proxadmin.route("/chat", methods=['GET']) +def chat(): + return render_template('proxadmin/livechat.html') + +@proxadmin.route("/aboutus", methods=['GET']) +def about(): + return render_template('proxadmin/aboutus.html') + +@proxadmin.route("/terms", methods=['GET']) +def terms(): + return render_template('proxadmin/terms.html') + + +#APP STORE +@proxadmin.route('/market/', methods=['GET']) +@login_required +def market(group_id=0): + page = { 'title': 'Market' } + allproducts = Product.get_products() + allgroups = current_app.config['GROUPS'] + + if group_id == 0: + return render_template('proxadmin/market.html', groups=allgroups, products=allproducts) + + filtered_products = {} + for key, value in allproducts.items(): + if value['group'] == group_id: + filtered_products[key] = value + + if filtered_products == {}: + abort(404) + return render_template('proxadmin/marketgroup.html', groupname=allgroups[group_id], products=filtered_products) + + +@proxadmin.route('/deploy/', methods=['GET', 'POST']) +@login_required +def deploy(product_id=None): + #if current_user.wallet < 20: + # flash('Недостатъчно средства в сметката за тази операция') + # return redirect(url_for('uinvoice.addfunds')) + + if current_user.name is None: + flash('Моля обновете информацията за профила') + return redirect(url_for('uinvoice.profile')) + + page = { 'title': 'Deploy' } + try: + product = Product.get_products()[product_id] + except: + print('unknown product {}'.format(product_id)) + abort(404) + product_pic = '..' + product['img'] + product_name = product['name'] + product_description = product['description'] + + product_cpu = product['cpu'] + product_mem = product['mem'] + product_hdd = product['hdd'] + product_recipe = product['recipe'] + + hostname = 'deploy-{}.local'.format(randstr(6)) + + form = DeployForm(servername=hostname, cpu=product_cpu, mem=product_mem, hdd=product_hdd, recipe=product_recipe) + form.region.choices = current_app.config['REGIONS'] + form.recipe.choices = current_app.config['RECIPES'] + + if current_user.confirmed and form.validate_on_submit(): + client_id = current_user.pid + data = { 'clientid': str(client_id), + 'clientname': str(current_user.name), + 'clientemail': str(current_user.email), + 'hostname': str(form.servername.data), + 'vmpass': form.vmpassword.data, + 'region': form.region.data, + 'vps_type': 'kvm', + 'vps_recipe': form.recipe.data, + 'vps_cpu': form.cpu.data, + 'vps_mem': form.mem.data, + 'vps_hdd': form.hdd.data, + 'vps_ipv4': '1' } + + try: + query = contact_proxmaster(data, 'vmcreate') + except: + flash('Region unreachable! Please try again later...') + return redirect(url_for('proxadmin.index')) + + if query is not None: + cubeid = query['cube'] + deployment = Deployment(user_id=client_id, product_id=product_id, machine_alias=form.servername.data, machine_id=cubeid, machine_cpu=form.cpu.data, machine_mem=form.mem.data, machine_hdd=form.hdd.data, credit=0, date_expire=(datetime.utcnow() + timedelta(days=30)), enabled=True) + db.session.add(deployment) + db.session.commit() + + flash('Deploy requested.') + else: + flash('Deploy cancelled! Please try again later...') + + return redirect(url_for('proxadmin.index')) + + return render_template('proxadmin/deploy.html', page=page, form=form, product_id=product_id, product_pic=product_pic, product_name=product_name, product_description=product_description, product_recipe=product_recipe) + + +#COMMAND AND CONTROL +@proxadmin.route("/dashboard", methods=['GET', 'POST']) +@login_required +def dashboard(): + #if request.method == 'GET': + deployments = current_user.inv_deployments.order_by(Deployment.date_created.desc()).all() + + inv_deployments = [] + for invcls in deployments: + if invcls.enabled == True: + inv_deployments.extend([invcls.machine_id]) + #if not inv_deployments: + # return render_template('proxadmin/empty_dashboard.html') + + rrd = {} + statuses = {} + for cubeid in inv_deployments: + rrd[cubeid] = {} + try: + query = contact_proxmaster({}, 'vmrrd', cubeid) + except: + flash('Deploy #{} unreachable. Support is notified'.format(str(cubeid))) + send_email(current_app.config['MAIL_USERNAME'], 'Cube {} is unreachable'.format(cubeid), + 'proxadmin/email/adm_unreachable', user=current_user, cubeid=cubeid) + + graphs_list = ['net', 'cpu', 'mem', 'hdd'] + try: + for graph in graphs_list: + raw = query[graph]['image'].encode('raw_unicode_escape') + rrd[cubeid][graph] = base64.b64encode(raw).decode() + status = { cubeid : query['status'] } + statuses.update(status) + except Exception as e: + print(e) + flash('Deploy #{} unreachable. Support is notified'.format(str(cubeid))) + send_email(current_app.config['MAIL_USERNAME'], 'Cube {} is unreachable'.format(cubeid), + 'proxadmin/email/adm_unreachable', user=current_user, cubeid=cubeid ) + + contracts = current_user.inv_contracts.order_by(Contract.date_created.desc()).all() + #inv_contracts = [] + #for invcls in contracts: + # if invcls.enabled == True: + # inv_contracts.extend([invcls.template]) + + domains = current_user.inv_domains.order_by(Domain.date_created.desc()).all() + #inv_domains = [] + #for invcls in domains: + # if invcls.enabled == True: + # inv_domains.extend([invcls.fqdn]) + + #print(statuses) + + return render_template('proxadmin/dashboard.html', rrd=rrd, status=statuses, inv_deployments=deployments, inv_contracts=contracts, inv_domains=domains) + +@proxadmin.route('/vmvnc/') +@login_required +def vnc(vmid=0): + result = current_user.inv_deployments.order_by(Deployment.date_created.desc()).all() + inventory = [] + for invcls in result: + if invcls.enabled == True: + inventory.extend([invcls.machine_id]) + + #checks if current user owns this vmid + if not vmid in inventory: + print('WARNING: user does not own vmid: ' + str(vmid)) + #TODO: log ips + else: + data = {} + db_result = contact_proxmaster(data, 'vmvnc', vmid) + #return render_template('proxadmin/vnc.html', url=db_result['url']) + + return redirect(db_result['url']) + abort(404) + +valid_commands = ['vmstatus', 'vmstart', 'vmshutdown', 'vmstop'] + +@proxadmin.route('//') +@login_required +def command(cmd=None, vmid=0): + #checks whether this is a valid command + if not cmd in valid_commands: + print('WARNING: ' + cmd + ' is not a valid command!') + abort(404) + + #if cmd == 'vmstart' and current_user.wallet < 3.0: + # flash('Недостатъчно средства в сметката за тази операция') + # return redirect(url_for('uinvoice.addfunds')) + + result = current_user.inv_deployments.order_by(Deployment.date_created.desc()).all() + inventory = [] + for invcls in result: + if invcls.enabled == True: + inventory.extend([invcls.machine_id]) + + #checks if current user owns this vmid + if not vmid in inventory: + print('WARNING: user id:{} does not own cube id:{}'.format(current_user.pid, vmid)) + else: + data = {} + db_result = contact_proxmaster(data, cmd, vmid) + #print(db_result) + #TODO: log ips + abort(404) + + diff --git a/proxadmin/static/css/bootstrap-slider.css b/proxadmin/static/css/bootstrap-slider.css new file mode 100644 index 0000000..085291e --- /dev/null +++ b/proxadmin/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/proxadmin/static/css/navbar.css b/proxadmin/static/css/navbar.css new file mode 100644 index 0000000..4cc5e91 --- /dev/null +++ b/proxadmin/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: 200px; + 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/proxadmin/static/css/nouislider.css b/proxadmin/static/css/nouislider.css new file mode 100644 index 0000000..16c27d4 --- /dev/null +++ b/proxadmin/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/proxadmin/static/css/rangeslider.scss b/proxadmin/static/css/rangeslider.scss new file mode 100644 index 0000000..365faea --- /dev/null +++ b/proxadmin/static/css/rangeslider.scss @@ -0,0 +1,100 @@ +@import "compass/css3"; + +$rangeslider: ".rangeslider"; +$rangeslider--horizontal: ".rangeslider--horizontal"; +$rangeslider--vertical: ".rangeslider--vertical"; +$rangeslider--disabled: ".rangeslider--disabled"; +$rangeslider--active: ".rangeslider--active"; +$rangeslider__fill: ".rangeslider__fill"; +$rangeslider__handle: ".rangeslider__handle"; + +#{$rangeslider}, +#{$rangeslider__fill} { + display: block; + @include box-shadow(inset 0px 1px 3px rgba(0,0,0,0.3)); + @include border-radius(10px); +} + +#{$rangeslider} { + background: #e6e6e6; + position: relative; +} + +#{$rangeslider--horizontal} { + height: 20px; + width: 100%; +} + +#{$rangeslider--vertical} { + width: 20px; + min-height: 150px; + max-height: 100%; +} + +#{$rangeslider--disabled} { + @include opacity(.4); +} + +#{$rangeslider__fill} { + background: #00ff00; + position: absolute; + + #{$rangeslider--horizontal} & { + top: 0; + height: 100%; + } + + #{$rangeslider--vertical} & { + bottom: 0; + width: 100%; + } +} + +#{$rangeslider__handle} { + background: white; + border: 1px solid #ccc; + cursor: pointer; + display: inline-block; + width: 40px; + height: 40px; + position: absolute; + @include background-image(linear-gradient(rgba(white, 0), rgba(black, .10))); + @include box-shadow(0 0 8px rgba(black, .3)); + @include border-radius(50%); + + &:after { + content: ""; + display: block; + width: 18px; + height: 18px; + margin: auto; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + @include background-image(linear-gradient(rgba(black, .13), rgba(white, 0))); + @include border-radius(50%); + } + + &:active, + #{$rangeslider--active} & { + @include background-image(linear-gradient(rgba(black, .10), rgba(black, .12))); + } + + #{$rangeslider--horizontal} & { + top: -10px; + touch-action: pan-y; + -ms-touch-action: pan-y; + } + + #{$rangeslider--vertical} & { + left: -10px; + touch-action: pan-x; + -ms-touch-action: pan-x; + } +} + +input[type="range"]:focus + #{$rangeslider} #{$rangeslider__handle} { + @include box-shadow(0 0 8px rgba(#ff00ff, .9)); +} diff --git a/proxadmin/static/css/style.css b/proxadmin/static/css/style.css new file mode 100644 index 0000000..a021b04 --- /dev/null +++ b/proxadmin/static/css/style.css @@ -0,0 +1,152 @@ +body { + /* background: url('/static/images/purplebg.jpg') no-repeat center center fixed; */ + background-color: #cccccc; + padding-top: 0px; + -webkit-background-size: cover; + -moz-background-size: cover; + background-size: cover; + -o-background-size: cover; + font-size: 12pt; + font-weight: bold; +} + +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: 500px; +} + +.padding-left-32 { + padding-left: 32px; +} + +.padding-left-16 { + padding-left: 16px; +} + +.container-fluid { + position: relative; + max-width: 1170px; + min-width: 480px; +} + +.container-fluid-index { + position: relative; + color: #fff; +} + +.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%; +} + +.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; +} + diff --git a/proxadmin/static/css/style.css.old b/proxadmin/static/css/style.css.old new file mode 100644 index 0000000..4b9a01b --- /dev/null +++ b/proxadmin/static/css/style.css.old @@ -0,0 +1,121 @@ +body { + background: url('/static/images/bg.jpg') no-repeat center center fixed; + background: #000000; + background-color: #f8f8f8; + padding-top: 0px; + -webkit-background-size: cover; + -moz-background-size: cover; + background-size: cover; + -o-background-size: cover; + color: gray; +} + + +body { + color: gray; + font-size: 12pt; + font-weight: bold; +} + +h1 { + color: white; +} + + +.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: 500px; +} + +.padding-left-32 { + padding-left: 32px; +} + +.padding-left-16 { + padding-left: 16px; +} + +.container-fluid { + position: relative; + max-width: 1170px; + min-width: 480px; +} + +.roundavatar { +border-radius: 50%; +-moz-border-radius: 50%; +-webkit-border-radius: 50%; +} + +.navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus { + color: #fff; + border-color: #070; + background-color: #2b7845; +} + +.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; +} + diff --git a/proxadmin/static/images/1.jpg b/proxadmin/static/images/1.jpg new file mode 100644 index 0000000..22c13f2 Binary files /dev/null and b/proxadmin/static/images/1.jpg differ diff --git a/proxadmin/static/images/2.jpg b/proxadmin/static/images/2.jpg new file mode 100644 index 0000000..31fd6e3 Binary files /dev/null and b/proxadmin/static/images/2.jpg differ diff --git a/proxadmin/static/images/220x180.png b/proxadmin/static/images/220x180.png new file mode 100644 index 0000000..d1147ba Binary files /dev/null and b/proxadmin/static/images/220x180.png differ diff --git a/proxadmin/static/images/3.jpg b/proxadmin/static/images/3.jpg new file mode 100644 index 0000000..9ca84aa Binary files /dev/null and b/proxadmin/static/images/3.jpg differ diff --git a/proxadmin/static/images/4.jpg b/proxadmin/static/images/4.jpg new file mode 100644 index 0000000..9ff7c77 Binary files /dev/null and b/proxadmin/static/images/4.jpg differ diff --git a/proxadmin/static/images/5.jpg b/proxadmin/static/images/5.jpg new file mode 100644 index 0000000..4a2f54c Binary files /dev/null and b/proxadmin/static/images/5.jpg differ diff --git a/proxadmin/static/images/6.jpg b/proxadmin/static/images/6.jpg new file mode 100644 index 0000000..c2d9768 Binary files /dev/null and b/proxadmin/static/images/6.jpg differ diff --git a/proxadmin/static/images/Hosting2.png b/proxadmin/static/images/Hosting2.png new file mode 100644 index 0000000..c6d68f9 Binary files /dev/null and b/proxadmin/static/images/Hosting2.png differ diff --git a/proxadmin/static/images/VPS-Mission.png b/proxadmin/static/images/VPS-Mission.png new file mode 100644 index 0000000..ff3c09f Binary files /dev/null and b/proxadmin/static/images/VPS-Mission.png differ diff --git a/proxadmin/static/images/VPS-Security.png b/proxadmin/static/images/VPS-Security.png new file mode 100644 index 0000000..c557fcb Binary files /dev/null and b/proxadmin/static/images/VPS-Security.png differ diff --git a/proxadmin/static/images/VPS-Support.png b/proxadmin/static/images/VPS-Support.png new file mode 100644 index 0000000..f4429b8 Binary files /dev/null and b/proxadmin/static/images/VPS-Support.png differ diff --git a/proxadmin/static/images/VPS-equipment.png b/proxadmin/static/images/VPS-equipment.png new file mode 100644 index 0000000..114aebf Binary files /dev/null and b/proxadmin/static/images/VPS-equipment.png differ diff --git a/proxadmin/static/images/_bg.jpg b/proxadmin/static/images/_bg.jpg new file mode 100644 index 0000000..1c9bed5 Binary files /dev/null and b/proxadmin/static/images/_bg.jpg differ diff --git a/proxadmin/static/images/bg-linear.jpg b/proxadmin/static/images/bg-linear.jpg new file mode 100644 index 0000000..2282ad7 Binary files /dev/null and b/proxadmin/static/images/bg-linear.jpg differ diff --git a/proxadmin/static/images/bg.jpg b/proxadmin/static/images/bg.jpg new file mode 100644 index 0000000..1c9bed5 Binary files /dev/null and b/proxadmin/static/images/bg.jpg differ diff --git a/proxadmin/static/images/datapoint.png b/proxadmin/static/images/datapoint.png new file mode 100644 index 0000000..6c58a00 Binary files /dev/null and b/proxadmin/static/images/datapoint.png differ diff --git a/proxadmin/static/images/header-layer.jpg b/proxadmin/static/images/header-layer.jpg new file mode 100644 index 0000000..499d247 Binary files /dev/null and b/proxadmin/static/images/header-layer.jpg differ diff --git a/proxadmin/static/images/hex24.png b/proxadmin/static/images/hex24.png new file mode 100644 index 0000000..c21fdcd Binary files /dev/null and b/proxadmin/static/images/hex24.png differ diff --git a/proxadmin/static/images/hex32.png b/proxadmin/static/images/hex32.png new file mode 100644 index 0000000..1075279 Binary files /dev/null and b/proxadmin/static/images/hex32.png differ diff --git a/proxadmin/static/images/hex512.png b/proxadmin/static/images/hex512.png new file mode 100644 index 0000000..0cb65b8 Binary files /dev/null and b/proxadmin/static/images/hex512.png differ diff --git a/proxadmin/static/images/purplebg.jpg b/proxadmin/static/images/purplebg.jpg new file mode 100644 index 0000000..ffb5c68 Binary files /dev/null and b/proxadmin/static/images/purplebg.jpg differ diff --git a/proxadmin/static/images/purplebg1.jpg b/proxadmin/static/images/purplebg1.jpg new file mode 100644 index 0000000..f139038 Binary files /dev/null and b/proxadmin/static/images/purplebg1.jpg differ diff --git a/proxadmin/static/images/purplebg2.jpg b/proxadmin/static/images/purplebg2.jpg new file mode 100644 index 0000000..ffb5c68 Binary files /dev/null and b/proxadmin/static/images/purplebg2.jpg differ diff --git a/proxadmin/static/images/robot.png b/proxadmin/static/images/robot.png new file mode 100644 index 0000000..eebf640 Binary files /dev/null and b/proxadmin/static/images/robot.png differ diff --git a/proxadmin/static/images/texture-diagonal.png b/proxadmin/static/images/texture-diagonal.png new file mode 100644 index 0000000..f262725 Binary files /dev/null and b/proxadmin/static/images/texture-diagonal.png differ diff --git a/proxadmin/static/js/bootstrap-slider.js b/proxadmin/static/js/bootstrap-slider.js new file mode 100644 index 0000000..bb46283 --- /dev/null +++ b/proxadmin/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/proxadmin/static/js/jquery.js b/proxadmin/static/js/jquery.js new file mode 100644 index 0000000..4c5be4c --- /dev/null +++ b/proxadmin/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 navbar %} +{% include "nav.html" %} +{% endblock %} + +{% block content %} +
+ {% for message in get_flashed_messages() %} +
+ + {{ message }} +
+ {% endfor %} + + {% block page_content %}{% endblock %} + +{% endblock %} + +{% block footer %} +{% include "footer.html" %} +{% endblock %} + + diff --git a/proxadmin/templates/errors/403.html b/proxadmin/templates/errors/403.html new file mode 100644 index 0000000..0769eec --- /dev/null +++ b/proxadmin/templates/errors/403.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %}Forbidden{% endblock %} + +{% block page_content %} + +
+  ooo,    .---.
+ o`  o   /    |\________________
+o`   'oooo()  | ________   _   _)
+`oo   o` \    |/        | | | |
+  `ooo'   `---'         "-" |_|                           
+
+{% endblock %} diff --git a/proxadmin/templates/errors/404.html b/proxadmin/templates/errors/404.html new file mode 100644 index 0000000..e5c08e5 --- /dev/null +++ b/proxadmin/templates/errors/404.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% block title %}Page Not Found{% endblock %} + +{% block page_content %} + + +
+        \          SORRY            /
+         \                         /
+          \    This page does     /
+           ]   not exist yet.    [    ,'|
+           ]                     [   /  |
+           ]___               ___[ ,'   |
+           ]  ]\             /[  [ |:   |
+           ]  ] \           / [  [ |:   |
+           ]  ]  ]         [  [  [ |:   |
+           ]  ]  ]__     __[  [  [ |:   |
+           ]  ]  ] ]\ _ /[ [  [  [ |:   |
+           ]  ]  ] ] (#) [ [  [  [ :===='
+           ]  ]  ]_].nHn.[_[  [  [
+           ]  ]  ]  HHHHH. [  [  [
+           ]  ] /   `HH("N  \ [  [
+           ]__]/     HHH  "  \[__[
+           ]         NNN         [
+           ]         N/"         [
+           ]         N H         [
+          /          N            \
+         /           q,            \
+        /                           \
+
+{% endblock %} diff --git a/proxadmin/templates/errors/500.html b/proxadmin/templates/errors/500.html new file mode 100644 index 0000000..3f9a273 --- /dev/null +++ b/proxadmin/templates/errors/500.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block title %}Internal Server Error{% endblock %} + +{% block page_content %} + +
+        .--.       .--.
+    _  `    \     /    `  _
+     `\.===. \.^./ .===./`
+            \/`"`\/
+         ,  | 500 |  ,
+        / `\|;-.-'|/` \
+       /    |::\  |    \
+    .-' ,-'`|:::; |`'-, '-.
+        |   |::::\|   | 
+        |   |::::;|   |
+        |   \:::://   |
+        |    `.://'   |
+       .'             `.
+    _,'                 `,_
+
+ +{% endblock %} diff --git a/proxadmin/templates/errors/csrf_error.html b/proxadmin/templates/errors/csrf_error.html new file mode 100644 index 0000000..6ad0624 --- /dev/null +++ b/proxadmin/templates/errors/csrf_error.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %}CSRF Error{% endblock %} + +{% block page_content %} + +
+  ooo,    .---.
+ o`  o   /    |\________________
+o`   'oooo()  | ________   _   _)
+`oo   o` \    |/        | | | |
+  `ooo'   `---'         "-" |_|                           
+
+{% endblock %} diff --git a/proxadmin/templates/footer.html b/proxadmin/templates/footer.html new file mode 100644 index 0000000..7c98bf5 --- /dev/null +++ b/proxadmin/templates/footer.html @@ -0,0 +1,9 @@ +{% block page_footer %} +
+
+ +{% endblock %} diff --git a/proxadmin/templates/nav-full.html b/proxadmin/templates/nav-full.html new file mode 100644 index 0000000..461cacf --- /dev/null +++ b/proxadmin/templates/nav-full.html @@ -0,0 +1,70 @@ +{% block navbar %} + +{% endblock %} + + diff --git a/proxadmin/templates/nav.html b/proxadmin/templates/nav.html new file mode 100644 index 0000000..a95964e --- /dev/null +++ b/proxadmin/templates/nav.html @@ -0,0 +1,60 @@ +{% block navbar %} + +{% endblock %} + + diff --git a/proxadmin/templates/proxadmin/_misc.html b/proxadmin/templates/proxadmin/_misc.html new file mode 100644 index 0000000..13fd91f --- /dev/null +++ b/proxadmin/templates/proxadmin/_misc.html @@ -0,0 +1,161 @@ +{% extends "base.html" %} + +{% block page_content %} + + + + +
+
+
+ +
+
+ {% for key, value in current_user.inventory.items() %} +
+
{{ value['hostname'] }} ({{ key }})
+

+ + + + +

+ + +

+

+ + + + + + + +

+
NetworkCPU
+ + +

+

+ + + + + + + +

+
MemoryHard Disk
+

+
+
+ {% endfor %} +
+
+ +
+ + +
+ +{% endblock %} diff --git a/proxadmin/templates/proxadmin/aboutus.html b/proxadmin/templates/proxadmin/aboutus.html new file mode 100644 index 0000000..7290c72 --- /dev/null +++ b/proxadmin/templates/proxadmin/aboutus.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block page_content %} +

About us

+
+
+ +
+ +

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

+

Историята

+

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

+

Мястото

+

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

+

Екипът

+

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

+
+ +{% endblock %} diff --git a/proxadmin/templates/proxadmin/dashboard.html b/proxadmin/templates/proxadmin/dashboard.html new file mode 100644 index 0000000..dd5cc63 --- /dev/null +++ b/proxadmin/templates/proxadmin/dashboard.html @@ -0,0 +1,188 @@ +{% extends "base.html" %} + +{% block page_content %} + + + + + + + +
+
+
+ +{% block sidebar %} +{% include "uinvoice/_sidebar.html" %} +{% endblock %} + +
+
+
Services
+

+ {% for contract in inv_contracts %} + {{ contract.description }}
+ {{ contract.units }}
+ {% if not contract.discount == 0 %} + Discount %{{ contract.discount }}
+ Credit: {{ contract.credit }}

+ {% endif %} + {% endfor %} +

+

+
+
+ +
+
+
Domains
+

+ {% for domain in inv_domains %} + {{ fqdn }}
+ {% endfor %} +

+

+
+
+ + +
+
+
+
Deployments
+

+ {% for deploy in inv_deployments %} +

+
+
+
+
+ +
+
+ {% if status[deploy.machine_id] == 'running' %} + + + + {% else %} + + {% endif %} +

+ Grid ID# {{ deploy.machine_id }}
+ CPU: {{ deploy.machine_cpu }} cores
+ Memory: {{ deploy.machine_mem }} MB
+ Disk Space: {{ deploy.machine_hdd }} GB
+
+ Expiry date: {{ deploy.date_expire }}
+ Credit: {{ deploy.credit }} +
+ +
+ + + + + + + +
Network Bandwidth

+ + + + + + + + + +
CPU Load

+ + + + + + + + +
Memory Usage

+ + + + + + + + +
Disk Input/Output

+
+ +
+ Not yet implemented. +
+ +
+
+ {% endfor %} +
+
+ +
+
+ +
+ + +
+
+
+ +{% endblock %} + + diff --git a/proxadmin/templates/proxadmin/deploy.html b/proxadmin/templates/proxadmin/deploy.html new file mode 100644 index 0000000..21ba5bd --- /dev/null +++ b/proxadmin/templates/proxadmin/deploy.html @@ -0,0 +1,113 @@ +{% extends "base.html" %} + +{% block title %}Deploy{% endblock %} + +{% block page_content %} + + + + + + +
+
+
+ +
+
+
{{ product_name }}
+
+
+ {{ product_description }} +
+
+
+ +
+
+
Настройки
+
+
+

+ {{ form.servername.label }}
{{ form.servername(size=42) }}
+ {% for error in form.servername.errors %} + {{ error }}
+ {% endfor %} +

+ +
+ +

+ {{ form.region.label }}
{{ form.region }}
+ {% for error in form.region.errors %} + {{ error }}
+ {% endfor %} +

+ +
+ +

+ {{ form.vmpassword.label }}
{{ form.vmpassword }}
+ {% for error in form.vmpassword.errors %} + {{ error }}
+ {% endfor %} +

+ +
+ +

+

+
+ {{ form.cpu.label }} {{ form.cpu(class_="noUiSluder") }}
+
+ {{ form.mem.label }} {{ form.mem }}
+ {{ form.hdd.label }} {{ form.hdd }}
+ {{ form.recipe.label }} {{ form.recipe }}
+

+ +
+ + {{ form.invite_key.label }} {{ form.invite_key }}
+ {% for error in form.invite_key.errors %} + {{ error }}
+ {% endfor %} + + +

+
+ +

+ {{ form.csrf_token() }} + {{ form.submit }} +

+ + +
+ + + +
+
+ +{% endblock %} + diff --git a/proxadmin/templates/proxadmin/email/adm_unreachable.html b/proxadmin/templates/proxadmin/email/adm_unreachable.html new file mode 100644 index 0000000..b51466a --- /dev/null +++ b/proxadmin/templates/proxadmin/email/adm_unreachable.html @@ -0,0 +1,5 @@ +

{{ user.email }} encountered an error working with cube id: {{ cubeid }}
+
+

+

Regards, +Proxadmin

diff --git a/proxadmin/templates/proxadmin/email/adm_unreachable.txt b/proxadmin/templates/proxadmin/email/adm_unreachable.txt new file mode 100644 index 0000000..cfa0f93 --- /dev/null +++ b/proxadmin/templates/proxadmin/email/adm_unreachable.txt @@ -0,0 +1,4 @@ +User {{ user.email }} encountered an error working with cube id: {{ cubeid }} + +Regards, +Proxadmin diff --git a/proxadmin/templates/proxadmin/empty_dashboard.html b/proxadmin/templates/proxadmin/empty_dashboard.html new file mode 100644 index 0000000..073ceb4 --- /dev/null +++ b/proxadmin/templates/proxadmin/empty_dashboard.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block page_content %} +

Dashboard

+
+
+ Празно е :( Можете да посетите магазинчето +
+ +{% endblock %} diff --git a/proxadmin/templates/proxadmin/index.html b/proxadmin/templates/proxadmin/index.html new file mode 100644 index 0000000..2ddb59d --- /dev/null +++ b/proxadmin/templates/proxadmin/index.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} + +{% block page_content %} +
+
+
+ +
+ +
+
+
+
+ +

Оборудване

+

Благодарение на внедрените нови технологии ние предлагаме изчислителна мощ, надеждно съхранение на данни и гъвкаво разпределение на ресурсите.

+
+ +
+ +

Бърз приятелски support

+

Ще Ви помогнем във всички неприятни ситуации, по всяко време. Независимо от нивото на умения Ви. Ние сме винаги насреща за въпроси.

+
+
+ +
+
+ +

Мисия

+

Основната ни цел е безкомпромисно качество на услугите ни. Предлагаме цялостна поддръжка на вашите машини, включително тези които използвате във Вашият офис.

+
+ +
+ +

Сигурност и надеждност

+

Разчитаме на собствено сървърно помещение, обезпечено от най- големите български доставчици на интернет свързаност. Предлагаме пълен достъп и наблюдение на процесите в реално време.

+
+ +
+ + + +{% endblock %} + diff --git a/proxadmin/templates/proxadmin/livechat.html b/proxadmin/templates/proxadmin/livechat.html new file mode 100644 index 0000000..e6a4f6e --- /dev/null +++ b/proxadmin/templates/proxadmin/livechat.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block page_content %} + + +{% endblock %} + +{% block footer %} +{% endblock %} diff --git a/proxadmin/templates/proxadmin/market.html b/proxadmin/templates/proxadmin/market.html new file mode 100644 index 0000000..c491188 --- /dev/null +++ b/proxadmin/templates/proxadmin/market.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block page_content %} +

App Store

+
+
+ {% for key, value in products.items() %} +
+


{{ value['name'] }}

{{ value['description'] }}

+
+ {% if key|int % 3 == 0 %} +
+
+ {% endif %} + {% endfor %} +
+ +{% endblock %} diff --git a/proxadmin/templates/proxadmin/marketgroup.html b/proxadmin/templates/proxadmin/marketgroup.html new file mode 100644 index 0000000..4a415a0 --- /dev/null +++ b/proxadmin/templates/proxadmin/marketgroup.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block page_content %} +

{{ groupname }}

+
+
+ {% for key, value in products.items() %} +
+


{{ value['name'] }}

{{ value['description'] }}

+
+ {% if key|int % 3 == 0 %} +
+
+ {% endif %} + {% endfor %} +
+ +{% endblock %} diff --git a/proxadmin/templates/proxadmin/terms.html b/proxadmin/templates/proxadmin/terms.html new file mode 100644 index 0000000..9c8c1d3 --- /dev/null +++ b/proxadmin/templates/proxadmin/terms.html @@ -0,0 +1,75 @@ +{% extends "base.html" %} + +{% block page_content %} +

Правила и Условия

+
+
+ +
+
+ +

I. ОСНОВНИ ДЕФИНИЦИИ:

+

Чл.1 (1) Информация съгласно Закона за електронната търговия и Закона за защита на потребителите:

+

Новахостинг ЕООД, ЕИК 202555287, адрес на управление: гр. Пловдив, ул. Волга №52., наричана по- долу ДОСТАВЧИК.

+

(2) КЛИЕНТ- физическо или юридическо лице, което използва услугите на ДОСТАВЧИКА.

+

(3) СЪРВЪР – Компютърна конфигурация или изчислителна система за съхранение и изпълнение на потребителски приложения и потребителска информация.

+

(8)Политика за използване на бисквитки (cookies)

+

Cookies (бисквитки) – малки текстови файлове, чрез инсталирането на които, НОВАХОСТИНГ ЕООД осигурява най-добра възможност за ползване на уебсайта си, чрез съхраняване на информация за потребителите, възстановяване на информацията за потребителя, проследяване на действия, идентификация и други сходни дейности, например съхранение на информация за услугите, от които се интересува потребителя, с цел предоставяне на подходяща информация при посещението му на Уебсайта.

+

(9) ВИРТУАЛЕН СЪРВЪР – Свободно пространство на сървър на ДОСТАВЧИКА, на което Клиента има право да инсталира приложения и да помества информация, която да споделя с трети лица в Интернет.

+

 

+

+

II.ОБЩИ УСЛОВИЯ:

+

 

+

Чл.2 (1) Клиентът се съгласява с настоящите общи условия и се съгласява да ги спазва чрез едно от следните действия:

+

1. Избиране на отметка “ Съгласен съм с общите условия“

+

2. При закупуване на услуга от ДОСТАВЧИКА.

+

(3) Избирането на отметка ‘Съгласен съм’, от КЛИЕНТА се счита за електронно изявление по смисъла на Закона за електронния документ и електронния подпис (ЗЕДЕП), като със записването му на съответен носител в сървъра на ДОСТАВЧИКА, електронното изявление придобива качеството на електронен документ по смисъла на цитирания закон.

+

(4) ДОСТАВЧИКЪТ записва чрез общоприет стандарт за преобразуване, по технически начин, правещ възможно възпроизвеждането и съхраняването в специални файлове (лог-файлове, Log files) на своя сървър IP адреса на КЛИЕНТ, както и всяка друга информация, необходима за идентифициране на КЛИЕНТ и възпроизвеждане на електронното му изявление за приемане на Общите условия, в случай на възникване на правен спор.

+

(3) Договорът за предоставяне на хостинг услуги между ДОСТАВЧИКА и КЛИЕНТА се счита за сключен при едно от събитията упоменати в Чл.2,

+

(4) Доставчикът си запазва правото да допълва, променя и редактира настоящите ОБЩИ УСЛОВИЯ.

+

III. Основни положения

+

Чл.3 (1) Въз основа на настоящите Общи условия, ДОСТАВЧИКЪТ предоставя на КЛИЕНТА хостинг услуга, наричана по-долу за краткост ‘УСЛУГАТА’, представляваща правото на ползване на ресурсите на определена компютърна конфигурация – сървър, предостъпване на част от неговото дисково пространство, изчислителни ресурси и софтуер за управление чрез които КЛИЕНТА предоставя желана от него информация да бъде достъпна от други компютри през Интернет.

+

IV. Права и задължения на страните

+

Задължения на Доставчика:

+

Чл.4 (1) ДОСТАВЧИКЪТ се задължава да поддържа постоянно техническите параметри на услугите, закупени от КЛИЕНТА.

+

(2) ДОСТАВЧИКЪТ се задължава да предостави на КЛИЕНТА техническа възможност да променя паролите и кодовете си за достъп до системата, като се задължава да не ги предоставя на трети лица

+

(3) ДОСТАВЧИКЪТ има право да прави софтуерни и хардуерни промени по техническото оборудване, като предупреди КЛИЕНТА за евентуални технически проблеми и прекъсвания на услугата.

+

(4) ДОСТАВЧИКЪТ се задължава да пази данните и файловете на КЛИЕНТА и да не допуска да стават достояние на трети лица, дори и след прекратяване на взаимоотношенията между двете страни.

+

(5) При констатирани нарушения или неспазване на клаузите по настоящите ОБЩИ УСЛОВИЯ, ДОСТАВЧИКЪТ има право да спре услугите, закупени от КЛИЕНТА без предупреждение.

+

 

+

Права на ДОСТАВЧИКА:

+

Чл.5 (1) ДОСТАВЧИКЪТ не носи отговорност спрямо КЛИЕНТА в случаите, когато:

+

(2) УСЛУГАТА, закупена от клиента е с нарушени показатели, заради регулярни или инцидентни дейности по оборудването на ДОСТАВЧИКА, с цел подобряването качесвото на УСЛУГАТА.

+

(3) ДОСТАВЧИКЪТ има право да спре достъпа до услугата, използвана от клиента поради неплащане на задължение в срок от страна на КЛИЕНТА.

+

(4) e налице проблем със свързаността на ДОСТАВЧИКА с Интернет, поради локални или глобални проблеми с ресурси, извън мрежата на ДОСТАВЧИКА или неработоспособност на Интернет мрежата, преносната среда или оборудване между ДОСТАВЧИКА и КЛИЕНТА.

+

(5) КЛИЕНТЪТ не спазва изискванията и указанията на ДОСТАВЧИКА за нормалното функциониране на УСЛУГАТА.

+

(6) КЛИЕНТЪТ използва непозволен софтуер, който вреди на оборудването на ДОСТАВЧИКА и петни името му.

+

ПРАВА И ЗАДЪЛЖЕНИЯ НА КЛИЕНТА

+

 

+

Чл.5 (1) КЛИЕНТЪТ е длъжен да заплаща заявените за използване услуги в срок, определен от ДОСТАВЧИКА.

+

(2) КЛИЕНТЪТ се задължава да не нарушава законовите разпоредби на Република България при използването на УСЛУГАТА.

+

(3) Докато използва услугата, КИЛЕНТЪТ се задължава да не нарушава под никаква форма права и законни интереси на ДОСТАВЧИКА и/или трети лица.

+

(4) КЛИЕНТЪТ се задължава да не предприема каквито и да било действия чрез УСЛУГАТА, които са в нарушение на законите на Република България или която и да е друга страна по света,

+

(5) КЛИЕНТЪТ няма право да публикува текстове и съобщения, съдържащи заплаха за физическата цялост и телесния интегритет на индивида, накърняващи доброто име на другиго или призоваващи към насилствена промяна на конституционния ред, към извършване на престъпление, към насилие над личността или към разпалване на расова, национална, етническа или религиозна вражда.

+

(6) КЛИЕНТЪТ се задължава да не използва услугите, предоставяни от ДОСТАВЧИКА за доставяне на нежелани търговски съобщения – СПАМ. При констатиран такъв случай, ДОСТАВЧИКЪТ има право да спре предоставяната на КЛИЕНТА услуга без предупреждение.

+

(7) КЛИЕНТЪТ се задължава при използване на предоставяната от ДОСТАВЧИКА УСЛУГА да не зарежда, да не разполага на сървър на ДОСТАВЧИКА, да не изпраща или използва по какъвто и да било начин и да не прави достояние на трети лица информация, данни, текст, звук, файлове, софтуер, музика, фотографии, графики, видео или аудио материали, съобщения, както и всякакви други материали:

+
    +
  • противоречащи на българското законодателство, приложимите чужди закони, настоящите Общи условия, Интернет етиката или добрите нрави;
  • +
  • съдържащи заплаха за живота и телесната неприкосновеност на човека;
  • +
  • пропагандиращи дискриминация, основана на пол, раса, образователен ценз, възраст и религия; проповядващи фашистка, расистка или друга недемократична идеология;
  • +
  • с порнографско съдържание или каквото и да било друго съдържание, което застрашава нормалното психическо развитие на не навършилите пълнолетие лица или нарушава нормите на морала и добрите нрави;
  • +
  • съдържащи детска порнография, сексуално насилие, както хипервръзки към страници с подобно съдържание;
  • +
  • чието съдържание нарушава права или свободи на човека съгласно Конституцията и законите на Република България или международни актове, по които Република България е страна;
  • +
  • представляващи търговска, служебна или лична тайна или друга конфиденциална информация;
  • +
  • предмет на авторското право, освен в случаите на притежаване на това право или със съгласието на неговия носител;
  • +
  • нарушаващи каквито и да били имуществени или не имуществени права или законни интереси на трети лица, включително право на собственост, право на интелектуална собственост и други;
  • +
  • накърняващи доброто име на другиго и призоваващи към насилствена промяна на конституционно установения ред, към извършване на престъпление, към насилие над личността или към разпалване на расова, национална, eтническа или религиозна вражда;
  • +
  • съдържащи информация за чужди пароли или права за достъп без съгласието на техния титуляр, както и софтуер за достъп до такива пароли или права.
  • +
+

 

+

Забранено е разпространението, съхранението или излагането на данни, материали или информация, които нарушават законите на Република България и/или законите на други държави. Това включва, но не се изчерпва с: материали, защитени от законите за авторски права; материали, които са заплашителни или нецензурни; материали, които са предназначени за лица над 18 години („само за възрастни“); материали, защитени като фирмена тайна или с друг статут ограничаващ тяхното публично разпространение.

+

Потребителя се задължава да плати обезщетение и поеме вината при използване на услугите.

+

Използването на услугите за нарушаване на защитени материали и търговски марки е забранено. Това включва, но не се изчерпва с неоторизирано копиране на музика, книги, снимки и всякакви други защитени продукти. Използването на акаунта за продаване на фалшификати на регистрирани търговски марки ще доведе до незабавното изтриване на потребителския акаунт. Ако се установи, че акаунта на потребителя нарушава други запазени права то достъпа до защитените материали ще бъде преустановен. Всеки потребителски акаунт хванат в повторно нарушение ще бъде спрян и/или изтрит от сървърите на доставчика. Ако смятате, че Вашите запазени права са нарушени може да пишете до доставчика на адрес с необходимата информация.

+
+ +{% endblock %} diff --git a/proxadmin/templates/uinvoice/_sidebar.html b/proxadmin/templates/uinvoice/_sidebar.html new file mode 100644 index 0000000..d3ce7eb --- /dev/null +++ b/proxadmin/templates/uinvoice/_sidebar.html @@ -0,0 +1,10 @@ +
+
+
{{ current_user.name }}
+
+
+ 2-Factor: {{ current_user.twofactor }}
+
+
+
+ diff --git a/proxadmin/templates/uinvoice/charge.html b/proxadmin/templates/uinvoice/charge.html new file mode 100644 index 0000000..613444d --- /dev/null +++ b/proxadmin/templates/uinvoice/charge.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} + +{% block title %}Зареждане на сметка{% endblock %} + +{% block page_content %} + + +
+
+
+ +{% block sidebar %} +{% include "uinvoice/_sidebar.html" %} +{% endblock %} + +
+
+
Зареждане на сметка
+
+ +

+ {{ form.invoice_amount.label }} {{ form.invoice_amount }}
+ {% for error in form.invoice_amount.errors %} + {{ error }}
+ {% endfor %} +

+ +
+
+ +

+ {{ form.csrf_token() }} + {{ form.submit }} +

+ + +
+ + + +
+
+ +{% endblock %} + diff --git a/proxadmin/templates/uinvoice/documents.html b/proxadmin/templates/uinvoice/documents.html new file mode 100644 index 0000000..62d7840 --- /dev/null +++ b/proxadmin/templates/uinvoice/documents.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} + +{% block title %}Фактури{% endblock %} + +{% block page_content %} + + +
+
+
+ +{% block sidebar %} +{% include "uinvoice/_sidebar.html" %} +{% endblock %} + +
+
+
Издадени Фактури
+
+ +

+

+ + + + + + + + + + + + {% for invoice in documents %} + {% if invoice.paid %} + + + + + + + {% else %} + + + + + + {% endif %} + + + {% endfor %} +
Document IDDateAmount
{{ invoice.pid }}{{ invoice.date_created.strftime('%d %b %Y - %H:%m') }}{{ invoice.amount }}Preview
{{ invoice.pid }}{{ invoice.date_created.strftime('%d %b %Y - %H:%m') }}{{ invoice.amount }}Pay
+
+

+ +
+
+ + +
+ + + +
+
+ +{% endblock %} + diff --git a/proxadmin/templates/uinvoice/email/adm_payment.html b/proxadmin/templates/uinvoice/email/adm_payment.html new file mode 100644 index 0000000..abb836b --- /dev/null +++ b/proxadmin/templates/uinvoice/email/adm_payment.html @@ -0,0 +1,4 @@ +

{{ user.email }} paid {{ order.units }} x {{ order.unitvalue }} = {{ order.units * order.unitvalue }}
+

+

Regards, +Proxadmin

diff --git a/proxadmin/templates/uinvoice/email/adm_payment.txt b/proxadmin/templates/uinvoice/email/adm_payment.txt new file mode 100644 index 0000000..f7172b3 --- /dev/null +++ b/proxadmin/templates/uinvoice/email/adm_payment.txt @@ -0,0 +1,4 @@ +{{ user.email }} paid {{ order.units }} x {{ order.unitvalue }} = {{ order.units * order.unitvalue }} + +Regards, +Proxadmin diff --git a/proxadmin/templates/uinvoice/invoice.html b/proxadmin/templates/uinvoice/invoice.html new file mode 100644 index 0000000..dd3344c --- /dev/null +++ b/proxadmin/templates/uinvoice/invoice.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} + +{% block title %}Фактура No. #{{ document.pid }}{% endblock %} + +{% block page_content %} + + +
+
+
+ +{% block sidebar %} +{% include "uinvoice/_sidebar.html" %} +{% endblock %} + +
+
+
+
+ +

+ {{ document.invoice_date }}
+ {{ document.invoice_amount }}
+ print to pdf. +

+ + {% if not document.paid %} +

+ {{ form.processor.label }} {{ form.processor }}
+

+ +

+ {{ form.csrf_token() }} + {{ form.submit }} +

+ {% endif %} + + + +
+
+ + + +
+ + +
+
+ +{% endblock %} + diff --git a/proxadmin/templates/uinvoice/profile.html b/proxadmin/templates/uinvoice/profile.html new file mode 100644 index 0000000..aef2e69 --- /dev/null +++ b/proxadmin/templates/uinvoice/profile.html @@ -0,0 +1,123 @@ +{% extends "base.html" %} + +{% block title %}Edit Profile{% endblock %} + +{% block page_content %} + + +
+
+
+ +{% block sidebar %} +{% include "uinvoice/_sidebar.html" %} +{% endblock %} + +
+
+
Данни на профила
+
+ +

+ {{ form.name.label }}
{{ form.name(size=42) }}
+ {% for error in form.name.errors %} + {{ error }}
+ {% endfor %} +

+ +

+ {{ form.address.label }}
{{ form.address(size=42) }}
+ {% for error in form.address.errors %} + {{ error }}
+ {% endfor %} +

+ +

+ {{ form.city.label }}
{{ form.city(size=42) }}
+ {% for error in form.city.errors %} + {{ error }}
+ {% endfor %} +

+ +

+ {{ form.postcode.label }}
{{ form.postcode(size=24) }}
+ {% for error in form.postcode.errors %} + {{ error }}
+ {% endfor %} +

+ +

+ {{ form.country.label }}
{{ form.country }}
+ {% for error in form.country.errors %} + {{ error }}
+ {% endfor %} +

+ +

+ {{ form.phone.label }}
{{ form.phone(size=42) }}
+ {% for error in form.phone.errors %} + {{ error }}
+ {% endfor %} +

+ + +
+
+ +
+
Данни за юридическо лице
+
+ +

+ {{ form.org_responsible.label }}
{{ form.org_responsible(size=42) }}
+ {% for error in form.org_responsible.errors %} + {{ error }}
+ {% endfor %} +

+ +

+ {{ form.org_bulstat.label }}
{{ form.org_bulstat(size=42) }}
+ {% for error in form.org_bulstat.errors %} + {{ error }}
+ {% endfor %} +

+ + +

+
+ + +
+
Допълнителна защита на акаунта (2-Factor Authentication)
+
+ +

+

За да използвате тази функция изпълнете следните стъпки:
+ 1. Моля инсталирайте FreeOTP на вашият смартфон. iTunes | Google Play
+ 2. Сканирайте с помоща на приложението вашия QR код. Той е равносилен на допълнителна парола и не трябва да бъде показван или изгубван
+
+ 3. Маркирайте отметката и обновете профила. Не губете своя QR код.

+ {{ form.twofactor }} {{ form.twofactor.label }}
+ +

+ +
+
+ +

+ {{ form.csrf_token() }} + {{ form.submit }} +

+ + +
+ + + +
+
+ +{% endblock %} + diff --git a/proxadmin/uinvoice/__init__.py b/proxadmin/uinvoice/__init__.py new file mode 100644 index 0000000..cfe096b --- /dev/null +++ b/proxadmin/uinvoice/__init__.py @@ -0,0 +1,4 @@ +from flask import Blueprint +uinvoice = Blueprint('uinvoice', __name__) +from . import routes + diff --git a/proxadmin/uinvoice/forms.py b/proxadmin/uinvoice/forms.py new file mode 100644 index 0000000..87f328c --- /dev/null +++ b/proxadmin/uinvoice/forms.py @@ -0,0 +1,68 @@ +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('Лице за контакт:', [validators.DataRequired(), validators.Length(3, 60)]) + address = StringField('Адрес:', [validators.DataRequired(), validators.Length(2, 50)]) + city = StringField('Град:', [validators.DataRequired(), validators.Length(2,40)]) + + postcode = StringField('Пощенски Код:') + + clist = [] + for c in countries: + clist.append((c.alpha2, c.name)) + country = SelectField('Държава:', choices=clist, default='BG') + + phone = StringField('Телефон:') + org_responsible = StringField('Отговорно Лице:') + org_bulstat = StringField('БУЛСТАТ:') + twofactor = BooleanField('2-factor authentication') + submit = SubmitField('Обнови') + + +class EditProfileAdminForm(FlaskForm): + email = StringField('Електроннa поща (логин):', [validators.DataRequired(), validators.Length(1, 64), validators.Email()]) + confirmed = BooleanField('Активиран') + role = SelectField('Роля', 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_responsible = StringField('Отговорно Лице:') + org_bulstat = StringField('БУЛСТАТ:') + 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-а е вече регистриран.') + +class ChargeForm(FlaskForm): + invoice_amount = DecimalField('Стойност:', [validators.DataRequired(), validators.NumberRange(min=0, max=6)]) + submit = SubmitField('Зареди') + +class PaymentForm(FlaskForm): + plist = [('paypal', 'PayPal'), ('epay', 'ePay.bg'), ('bank', 'Bank Transfer')] + processor = SelectField('Финансов инструмент:', choices=plist) + submit = SubmitField('Плати') + diff --git a/proxadmin/uinvoice/routes.py b/proxadmin/uinvoice/routes.py new file mode 100644 index 0000000..88ebc9b --- /dev/null +++ b/proxadmin/uinvoice/routes.py @@ -0,0 +1,102 @@ +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 uinvoice +from .forms import EditProfileForm, EditProfileAdminForm, ChargeForm, PaymentForm + +from ..email import send_email +from .. import db +from ..models import User, Order + +#PROFILE +@uinvoice.route('/profile', methods=['GET', 'POST']) +@login_required +def profile(): + page = { 'title': 'Edit Profile' } + + currentmail = current_user.email + ouruser = User.query.filter_by(email=currentmail).first() + db.session.commit() + + #wallet = "%.2f" % round(ouruser.wallet, 3) + #print(wallet) + + 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_responsible = form.org_responsible.data + current_user.org_bulstat = form.org_bulstat.data + current_user.twofactor = form.twofactor.data + db.session.add(current_user) + db.session.commit() + flash('Info Updated!') + + form.twofactor.data = current_user.twofactor + 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_responsible.data = current_user.org_responsible + form.org_bulstat.data = current_user.org_bulstat + + return render_template('uinvoice/profile.html', page=page, form=form) + +#INVOICES +#@uinvoice.route('/charge', methods=['GET', 'POST']) +#@login_required +#def charge(): +# """ generate new invoice based on user request """ +# unpaid_invoices = Order.query.filter_by(user_id=current_user.pid).filter_by(paid=False).all() +# if unpaid_invoices != []: +# flash('You have unpaid invoices') +# return redirect(url_for('uinvoice.documents')) +# page = { 'title': 'Charge Funds' } +# form = ChargeForm() +# if form.validate_on_submit(): +# newinvoice = Order(amount=form.invoice_amount.data, user_id=current_user.pid) +# db.session.add(newinvoice) +# db.session.commit() +# return redirect(url_for('uinvoice.documents')) +# return render_template('uinvoice/charge.html', page=page, form=form) + +@uinvoice.route('/documents', methods=['GET']) +@login_required +def documents(): + page = { 'title': 'Order documents' } + invoices = Order.query.filter_by(user_id=current_user.pid).order_by(desc(Order.date_created)).all() + db.session.commit() + return render_template('uinvoice/documents.html', page=page, documents=invoices) + + +@uinvoice.route('/order/', methods=['GET', 'POST']) +@login_required +def order(document_id): + page = { 'title': 'Preview ' + str(document_id) } + order = Order.query.filter_by(pid=document_id).first() + db.session.commit() + #check if document_id is owned by you. + try: + if order.user_id != current_user.pid: + print('WARNING: user {} violates order {}'.format(current_user.pid, order.pid)) + abort(404) + except: + abort(404) + form = PaymentForm() + if form.validate_on_submit(): + #TODO: contact payment processor + send_email(current_app.config['MAIL_USERNAME'], current_user.email + ' plati ' + str(order.units * order.unitvalue) + ' v koshnicata.', 'uinvoice/email/adm_payment', user=current_user, order=order ) + order.paid = True + return redirect(url_for('uinvoice.documents')) + #except: + # abort(404) + + return render_template('uinvoice/invoice.html', page=page, form=form, document=invoice, document_id=document_id) + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fabf660 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +Flask +Flask-Login +Flask-Bootstrap +Flask-WTF +Flask-Script +Flask-Migrate +Flask-Mail +Flask-SQLAlchemy +Flask-Babel +psycopg2 +itsdangerous +requests +sortedcontainers +iso3166 +pyqrcode +onetimepass +schedule diff --git a/run.py b/run.py new file mode 100644 index 0000000..3a43937 --- /dev/null +++ b/run.py @@ -0,0 +1,4 @@ +from app import app + +if __name__ == '__main__': + app.run() diff --git a/schedulerd.py b/schedulerd.py new file mode 100644 index 0000000..1cb38d7 --- /dev/null +++ b/schedulerd.py @@ -0,0 +1,40 @@ +import schedule +import time +import subprocess, shlex + +from manage import app, db +from app.models import Deployment + +def job1_charge(): + #process deployment charges + command_line = 'python3 manage.py charge_deployments' + args = shlex.split(command_line) + p = subprocess.Popen(args) + +def job2_charge(): + #process deployment charges + command_line = 'python3 manage.py charge_contracts' + args = shlex.split(command_line) + p = subprocess.Popen(args) + +def job3_charge(): + #process deployment charges + command_line = 'python3 manage.py charge_domains' + args = shlex.split(command_line) + p = subprocess.Popen(args) + + +if __name__ == '__main__': + schedule.every().day.at("12:30").do(job1_charge) + schedule.every().day.at("09:30").do(job2_charge) + schedule.every().day.at("16:30").do(job3_charge) + + while True: + try: + #print('test') + schedule.run_pending() + except Exception as e: + print('scheduler error: {}'.format(str(e))) + break + time.sleep(1) + diff --git a/start.wsgi b/start.wsgi new file mode 100644 index 0000000..6c1582e --- /dev/null +++ b/start.wsgi @@ -0,0 +1,20 @@ +import sys +import os +import site +#import logging + +#add the site-packages of the chosen virtualenv +site.addsitedir('/home/proxadmin/appserver/lib/python3.5/site-packages') + +#activate virtualenv +#using libapache2-mod-wsgi ... +#activate_this = '/home/proxadmin/appserver/bin/activate_this.py' +#execfile(activate_this, dict(__file__=activate_this)) +#... or using libapache2-mod-wsgi-py3 +activate_this = '/home/proxadmin/appserver/bin/activate_this.py' +with open(activate_this) as file_: + exec(file_.read(), dict(__file__=activate_this)) + +sys.path.append('/home/proxadmin/appserver/proxmaster-panel') +from app import app as application +