Browse Source

That's ugly

pull/2/head
squeaky otter 5 years ago
parent
commit
0cebb67f58
Signed by: dashie GPG Key ID: C2D57B325840B755
  1. 7
      .gitignore
  2. 114
      app.py
  3. 35
      config.py.sample
  4. 0
      controllers/__init__.py
  5. 43
      controllers/admin.py
  6. 36
      controllers/api.py
  7. 33
      controllers/main.py
  8. 61
      controllers/sound.py
  9. 61
      controllers/users.py
  10. 15
      crons.py
  11. 46
      dbseed.py
  12. 71
      forms.py
  13. 1
      migrations/README
  14. 45
      migrations/alembic.ini
  15. 88
      migrations/env.py
  16. 22
      migrations/script.py.mako
  17. 91
      migrations/versions/00_795db5a5e99b_.py
  18. 49
      migrations/versions/01_da3273ca0f0f_.py
  19. 152
      models.py
  20. 78
      reel2bits.py
  21. 55
      requirements.txt
  22. 587
      static/css/bootstrap-theme.css
  23. 1
      static/css/bootstrap-theme.css.map
  24. 6
      static/css/bootstrap-theme.min.css
  25. 1
      static/css/bootstrap-theme.min.css.map
  26. 6760
      static/css/bootstrap.css
  27. 1
      static/css/bootstrap.css.map
  28. 6
      static/css/bootstrap.min.css
  29. 1
      static/css/bootstrap.min.css.map
  30. BIN
      static/css/chosen-sprite.png
  31. BIN
      static/css/chosen-sprite@2x.png
  32. 450
      static/css/chosen.css
  33. 3
      static/css/chosen.min.css
  34. 239
      static/css/font-awesome.css
  35. 4
      static/css/font-awesome.min.css
  36. 15
      static/css/ie10-viewport-bug-workaround.css
  37. BIN
      static/css/images/ui-anim_basic_16x16.gif
  38. BIN
      static/css/images/ui-bg_diagonals-thick_18_b81900_40x40.png
  39. BIN
      static/css/images/ui-bg_diagonals-thick_20_666666_40x40.png
  40. BIN
      static/css/images/ui-bg_flat_0_888888_40x100.png
  41. BIN
      static/css/images/ui-bg_flat_0_aaaaaa_40x100.png
  42. BIN
      static/css/images/ui-bg_flat_10_000000_40x100.png
  43. BIN
      static/css/images/ui-bg_flat_75_ffffff_40x100.png
  44. BIN
      static/css/images/ui-bg_glass_100_f6f6f6_1x400.png
  45. BIN
      static/css/images/ui-bg_glass_100_fdf5ce_1x400.png
  46. BIN
      static/css/images/ui-bg_glass_25_e1f0f5_1x400.png
  47. BIN
      static/css/images/ui-bg_glass_55_444444_1x400.png
  48. BIN
      static/css/images/ui-bg_glass_65_ffffff_1x400.png
  49. BIN
      static/css/images/ui-bg_glass_75_dadada_1x400.png
  50. BIN
      static/css/images/ui-bg_gloss-wave_35_f6a828_500x100.png
  51. BIN
      static/css/images/ui-bg_highlight-soft_100_eeeeee_1x100.png
  52. BIN
      static/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png
  53. BIN
      static/css/images/ui-bg_highlight-soft_75_ffe45c_1x100.png
  54. BIN
      static/css/images/ui-bg_inset-soft_95_fef1ec_1x100.png
  55. BIN
      static/css/images/ui-icons_222222_256x240.png
  56. BIN
      static/css/images/ui-icons_228ef1_256x240.png
  57. BIN
      static/css/images/ui-icons_309bbf_256x240.png
  58. BIN
      static/css/images/ui-icons_454545_256x240.png
  59. BIN
      static/css/images/ui-icons_bf3030_256x240.png
  60. BIN
      static/css/images/ui-icons_ef8c08_256x240.png
  61. BIN
      static/css/images/ui-icons_ffd27a_256x240.png
  62. BIN
      static/css/images/ui-icons_ffffff_256x240.png
  63. 1225
      static/css/jquery-ui.css
  64. 7
      static/css/jquery-ui.min.css
  65. 833
      static/css/jquery-ui.structure.css
  66. 5
      static/css/jquery-ui.structure.min.css
  67. 410
      static/css/jquery-ui.theme.css
  68. 5
      static/css/jquery-ui.theme.min.css
  69. 62
      static/css/pygments.css
  70. 74
      static/css/style.css
  71. 180
      static/css/timeline.css
  72. 18
      static/datatables/Contributing.md
  73. 53
      static/datatables/Readme.md
  74. 762
      static/datatables/examples/advanced_init/column_render.html
  75. 755
      static/datatables/examples/advanced_init/complex_header.html
  76. 746
      static/datatables/examples/advanced_init/defaults.html
  77. 755
      static/datatables/examples/advanced_init/dom_multiple_elements.html
  78. 756
      static/datatables/examples/advanced_init/dom_toolbar.html
  79. 761
      static/datatables/examples/advanced_init/dt_events.html
  80. 745
      static/datatables/examples/advanced_init/events_live.html
  81. 743
      static/datatables/examples/advanced_init/footer_callback.html
  82. 745
      static/datatables/examples/advanced_init/html5-data-attributes.html
  83. 744
      static/datatables/examples/advanced_init/html5-data-options.html
  84. 73
      static/datatables/examples/advanced_init/index.html
  85. 742
      static/datatables/examples/advanced_init/language_file.html
  86. 744
      static/datatables/examples/advanced_init/length_menu.html
  87. 771
      static/datatables/examples/advanced_init/object_dom_read.html
  88. 755
      static/datatables/examples/advanced_init/row_callback.html
  89. 807
      static/datatables/examples/advanced_init/row_grouping.html
  90. 764
      static/datatables/examples/advanced_init/sort_direction_control.html
  91. 310
      static/datatables/examples/ajax/custom_data_flat.html
  92. 296
      static/datatables/examples/ajax/custom_data_property.html
  93. 460
      static/datatables/examples/ajax/data/arrays.txt
  94. 460
      static/datatables/examples/ajax/data/arrays_custom_prop.txt
  95. 688
      static/datatables/examples/ajax/data/arrays_subobjects.txt
  96. 460
      static/datatables/examples/ajax/data/objects.txt
  97. 688
      static/datatables/examples/ajax/data/objects_deep.txt
  98. 458
      static/datatables/examples/ajax/data/objects_root_array.txt
  99. 745
      static/datatables/examples/ajax/data/objects_subarrays.txt
  100. 631
      static/datatables/examples/ajax/data/orthogonal.txt

7
.gitignore

@ -0,0 +1,7 @@
*.pyc
config.py
TODO.org
.idea
*.db
uploads/pictures/*
tmp/cty.xml

114
app.py

@ -0,0 +1,114 @@
# encoding: utf-8
import logging
import os
import subprocess
from logging.handlers import RotatingFileHandler
from flask import Flask, render_template, g, send_from_directory, jsonify
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_migrate import Migrate
from flask_security import Security
from flask_uploads import configure_uploads, UploadSet, IMAGES
from controllers.admin import bp_admin
from controllers.main import bp_main
from controllers.users import bp_users
from forms import ExtendedRegisterForm
from models import db, user_datastore
from utils import dt_utc_to_user_tz, InvalidUsage, show_date_no_offset, is_admin, gcfg
__VERSION__ = "0.0.1"
def create_app(cfg=None):
# App Configuration
if cfg is None:
cfg = {}
app = Flask(__name__)
app.config.from_pyfile("config.py")
app.config.update(cfg)
Bootstrap(app)
app.jinja_env.add_extension('jinja2.ext.with_')
app.jinja_env.add_extension('jinja2.ext.do')
app.jinja_env.globals.update(is_admin=is_admin)
app.jinja_env.globals.update(gcfg=gcfg)
# Logging
if not app.debug:
formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')
file_handler = RotatingFileHandler("%s/errors_app.log" % os.getcwd(), 'a', 1000000, 1)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
app.logger.addHandler(file_handler)
mail = Mail(app)
migrate = Migrate(app, db)
db.init_app(app)
# Setup Flask-Security
security = Security(app, user_datastore,
register_form=ExtendedRegisterForm)
git_version = ""
gitpath = os.path.join(os.getcwd(), ".git")
if os.path.isdir(gitpath):
git_version = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'])
if git_version:
git_version = git_version.strip().decode('UTF-8')
@app.before_request
def before_request():
g.cfg = {
'REEL2BITS_VERSION_VER': __VERSION__,
'REEL2BITS_VERSION_GIT': git_version,
'REEL2BITS_VERSION': "{0} ({1})".format(__VERSION__, git_version),
}
@app.errorhandler(InvalidUsage)
def handle_invalid_usage(error):
response = jsonify(error.to_dict())
response.status_code = error.status_code
return response
pictures = UploadSet('pictures', IMAGES)
configure_uploads(app, pictures)
app.register_blueprint(bp_main)
app.register_blueprint(bp_users)
app.register_blueprint(bp_admin)
# Used in development
@app.route('/uploads/<path:stuff>', methods=['GET'])
def get_uploads_stuff(stuff):
print("Get {0} from {1}".format(stuff, app.config['UPLOADS_DEFAULT_DEST']))
return send_from_directory(app.config['UPLOADS_DEFAULT_DEST'], stuff, as_attachment=False)
@app.errorhandler(404)
def page_not_found(msg):
pcfg = {"title": "Whoops, something failed.",
"error": 404, "message": "Page not found", "e": msg}
return render_template('error_page.jinja2', pcfg=pcfg), 404
@app.errorhandler(403)
def err_forbidden(msg):
pcfg = {"title": "Whoops, something failed.",
"error": 403, "message": "Access forbidden", "e": msg}
return render_template('error_page.jinja2', pcfg=pcfg), 403
@app.errorhandler(410)
def err_gone(msg):
pcfg = {"title": "Whoops, something failed.",
"error": 410, "message": "Gone", "e": msg}
return render_template('error_page.jinja2', pcfg=pcfg), 410
if not app.debug:
@app.errorhandler(500)
def err_failed(msg):
pcfg = {"title": "Whoops, something failed.", "error": 500, "message": "Something is broken", "e": msg}
return render_template('error_page.jinja2', pcfg=pcfg), 500
return app

35
config.py.sample

@ -0,0 +1,35 @@
DEBUG = True
SECRET_KEY = 'aglaglawx2r31ht1g0m1vcq'
# SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://dashie@localhost/reel2bits'
SQLALCHEMY_DATABASE_URI = 'postgresql+pg8000://dashie@localhost/reel2bits'
# SQLALCHEMY_DATABASE_URI = 'sqlite:///reel2bits.db'
SQLALCHEMY_ECHO = False
SQLALCHEMY_TRACK_MODIFICATIONS = False
SECURITY_CONFIRMABLE = False
SECURITY_REGISTERABLE = False # deactivate registration
SECURITY_RECOVERABLE = True
SECURITY_TRACKABLE = False
SECURITY_CHANGEABLE = True
SECURITY_PASSWORD_HASH = 'bcrypt'
SECURITY_PASSWORD_SALT = 'ottersrulestheworld'
# SECURITY_URL_PREFIX = '/sec'
SECURITY_SEND_REGISTER_EMAIL = False
SECURITY_SEND_PASSWORD_CHANGE_EMAIL = False
SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL = False
BOOTSTRAP_USE_MINIFIED = True
BOOTSTRAP_SERVE_LOCAL = True
BOOTSTRAP_CDN_FORCE_SSL = True
BOOTSTRAP_QUERYSTRING_REVVING = True
DEBUG_TB_PROFILER_ENABLED = True
DEBUG_TB_INTERCEPT_REDIRECTS = False
# Paginations
UPLOADED_SND_DEST = "/home/reel2bits/uploads/snd"
UPLOADS_DEFAULT_DEST = "/home/reel2bits/uploads"
TEMP_DOWNLOAD_FOLDER = "/home/reel2bits/tmp"

0
controllers/__init__.py

43
controllers/admin.py

@ -0,0 +1,43 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_security import login_required
from forms import ConfigForm
from models import db, Logging, Config
from utils import is_admin
bp_admin = Blueprint('bp_admin', __name__)
@bp_admin.route('/admin/logs', methods=['GET'])
@login_required
def logs():
if not is_admin():
return redirect(url_for('bp_home.home'))
pcfg = {"title": "Application Logs"}
_logs = Logging.query.order_by(Logging.timestamp.desc()).limit(100).all()
return render_template('admin/logs.jinja2', pcfg=pcfg, logs=_logs)
@bp_admin.route('/admin/config', methods=['GET', 'POST'])
@login_required
def config():
if not is_admin():
return redirect(url_for('bp_main.home'))
pcfg = {"title": "Application Config"}
_config = Config.query.one()
if not _config:
flash("Config not found", 'error')
return redirect(url_for("bp_main.home"))
form = ConfigForm(request.form, _config)
if form.validate_on_submit():
_config.app_name = form.app_name.data
db.session.commit()
flash("Configuration updated", "info")
return redirect(url_for('bp_admin.config'))
return render_template('admin/config.jinja2', pcfg=pcfg, form=form)

36
controllers/api.py

@ -0,0 +1,36 @@
from flask import Blueprint, redirect, url_for, abort, flash
from flask_security import login_required, current_user
from models import db, Apitoken
from utils import generate_uniques_apitoken
bp_api = Blueprint('bp_api', __name__)
@bp_api.route('/api/token/new')
@login_required
def apitoken_new():
apitoken = generate_uniques_apitoken()
if not apitoken:
return abort(500)
a = Apitoken()
a.user_id = current_user.id
a.token = apitoken["token"]
a.secret = apitoken["secret"]
db.session.add(a)
db.session.commit()
return redirect(url_for('bp_users.user_profile'))
@bp_api.route('/api/token/<string:apit>/del')
@login_required
def apitoken_del(apit):
apitoken = Apitoken.query.filter(Apitoken.id == apit).first()
if not apitoken:
flash("API Token not found", "error")
return redirect(url_for("bp_main.home"))
db.session.delete(apitoken)
db.session.commit()
return redirect(url_for('bp_users.user_profile'))

33
controllers/main.py

@ -0,0 +1,33 @@
from flask import Blueprint, render_template, flash, redirect, url_for
from flask_security import current_user
from sqlalchemy import func
from models import db, User, Config
bp_main = Blueprint('bp_main', __name__)
# Show public logbooks
@bp_main.route('/')
def home():
_config = Config.query.one()
if not _config:
flash("Config not found", 'error')
return redirect(url_for("bp_main.home"))
pcfg = {"title": "Home", "app_name": _config.app_name}
users = User.query.all()
return render_template('home.jinja2', pcfg=pcfg, users=users)
@bp_main.route('/about')
def about():
_config = Config.query.one()
if not _config:
flash("Config not found", 'error')
return redirect(url_for("bp_main.home"))
pcfg = {"title": _config.app_name}
return render_template('about.jinja2', pcfg=pcfg)

61
controllers/sound.py

@ -0,0 +1,61 @@
import pytz
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_security import login_required, current_user
from sqlalchemy import func
from forms import UserProfileForm
from models import db, User, UserLogging
bp_users = Blueprint('bp_users', __name__)
@bp_users.route('/user/logs', methods=['GET'])
@login_required
def logs():
level = request.args.get('level')
pcfg = {"title": "User Logs"}
if level:
_logs = UserLogging.query.filter(UserLogging.level == level.upper(),
UserLogging.user_id == current_user.id
).order_by(UserLogging.timestamp.desc()).limit(100).all()
else:
_logs = UserLogging.query.filter(UserLogging.user_id == current_user.id
).order_by(UserLogging.timestamp.desc()).limit(100).all()
return render_template('users/user_logs.jinja2', pcfg=pcfg, logs=_logs)
@bp_users.route('/user', methods=['GET'])
@login_required
def profile():
pcfg = {"title": "My Profile"}
user = User.query.filter(User.id == current_user.id).first()
if not user:
flash("User not found", 'error')
return redirect(url_for("bp_main.home"))
return render_template('users/profile.jinja2', pcfg=pcfg, user=user)
@bp_users.route('/user/edit', methods=['GET', 'POST'])
@login_required
def edit():
pcfg = {"title": "Edit my profile"}
user = User.query.filter(User.id == current_user.id).first()
if not user:
flash("User not found", 'error')
return redirect(url_for("bp_main.home"))
form = UserProfileForm(request.form, user)
form.timezone.choices = [[str(i), str(i)] for i in pytz.all_timezones]
if form.validate_on_submit():
user.lastname = form.lastname.data
user.firstname = form.firstname.data
user.timezone = form.timezone.data
db.session.commit()
return redirect(url_for('bp_users.profile'))
return render_template('users/edit.jinja2', pcfg=pcfg, form=form, user=user)

61
controllers/users.py

@ -0,0 +1,61 @@
import pytz
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_security import login_required, current_user
from sqlalchemy import func
from forms import UserProfileForm
from models import db, User, UserLogging
bp_users = Blueprint('bp_users', __name__)
@bp_users.route('/user/logs', methods=['GET'])
@login_required
def logs():
level = request.args.get('level')
pcfg = {"title": "User Logs"}
if level:
_logs = UserLogging.query.filter(UserLogging.level == level.upper(),
UserLogging.user_id == current_user.id
).order_by(UserLogging.timestamp.desc()).limit(100).all()
else:
_logs = UserLogging.query.filter(UserLogging.user_id == current_user.id
).order_by(UserLogging.timestamp.desc()).limit(100).all()
return render_template('users/user_logs.jinja2', pcfg=pcfg, logs=_logs)
@bp_users.route('/user', methods=['GET'])
@login_required
def profile():
pcfg = {"title": "My Profile"}
user = User.query.filter(User.id == current_user.id).first()
if not user:
flash("User not found", 'error')
return redirect(url_for("bp_main.home"))
return render_template('users/profile.jinja2', pcfg=pcfg, user=user)
@bp_users.route('/user/edit', methods=['GET', 'POST'])
@login_required
def edit():
pcfg = {"title": "Edit my profile"}
user = User.query.filter(User.id == current_user.id).first()
if not user:
flash("User not found", 'error')
return redirect(url_for("bp_main.home"))
form = UserProfileForm(request.form, user)
form.timezone.choices = [[str(i), str(i)] for i in pytz.all_timezones]
if form.validate_on_submit():
user.lastname = form.lastname.data
user.firstname = form.firstname.data
user.timezone = form.timezone.data
db.session.commit()
return redirect(url_for('bp_users.profile'))
return render_template('users/edit.jinja2', pcfg=pcfg, form=form, user=user)

15
crons.py

@ -0,0 +1,15 @@
from __future__ import print_function
import gzip
import os
import shutil
import urllib.parse
import urllib.error
import urllib.request
import xml.etree.ElementTree as ElementTree
import re
import datetime
from dateutil import parser
from flask import current_app

46
dbseed.py

@ -0,0 +1,46 @@
from models import user_datastore, Config, Role
def make_db_seed(db):
print("== Seeding database")
db.session.begin(subtransactions=True)
try:
print("++ Seeding config")
seed_config(db)
seed_users(db) # after timezones because not null relation
# also seeds roles admin/user
except:
db.session.rollback()
raise
def seed_users(db):
print("++ Seeding users")
role_usr = Role()
role_usr.name = 'user'
role_usr.description = 'Simple user'
role_adm = Role()
role_adm.name = 'admin'
role_adm.description = 'Admin user'
db.session.add(role_usr)
db.session.add(role_adm)
user_datastore.create_user(
email='dashie@sigpipe.me',
password='fluttershy',
name='toto',
timezone='UTC',
roles=[role_adm]
)
db.session.commit()
return
def seed_config(db):
a = Config(app_name='My reel2bits instance')
db.session.add(a)
db.session.commit()
db.session.commit()
# Bug, two commit necessary

71
forms.py

@ -0,0 +1,71 @@
import datetime
from libqth import is_valid_qth
from flask_security import RegisterForm, current_user
from flask_uploads import UploadSet, IMAGES
from flask_wtf import Form
from flask_wtf.file import FileField, FileAllowed, FileRequired
from wtforms import PasswordField, SubmitField, TextAreaField, SelectField, IntegerField, \
HiddenField, BooleanField
from wtforms.ext.dateutil.fields import DateTimeField
from wtforms.ext.sqlalchemy.fields import QuerySelectField
from wtforms.validators import DataRequired, ValidationError, Optional
from wtforms_alchemy import model_form_factory
from wtforms_components.fields import SelectField as WTFComponentsSelectField
from wtforms import widgets
from wtforms.fields.core import StringField
from models import db, User
from utils import dt_utc_to_user_tz
BaseModelForm = model_form_factory(Form)
pictures = UploadSet('pictures', IMAGES)
class PasswordFieldNotHidden(StringField):
"""
Original source: https://github.com/wtforms/wtforms/blob/2.0.2/wtforms/fields/simple.py#L35-L42
A StringField, except renders an ``<input type="password">``.
Also, whatever value is accepted by this field is not rendered back
to the browser like normal fields.
"""
widget = widgets.PasswordInput(hide_value=False)
class ModelForm(BaseModelForm):
@classmethod
def get_session(cls):
return db.session
class ExtendedRegisterForm(RegisterForm):
name = StringField('Name', [DataRequired()])
def validate_name(form, field):
if len(field.data) <= 0:
raise ValidationError("Username required")
u = User.query.filter(User.name == field.data).first()
if u:
raise ValidationError("Username already taken")
class UserProfileForm(ModelForm):
class Meta:
model = User
password = PasswordField('Password')
name = StringField('Name')
email = StringField('Email')
firstname = StringField('Firstname')
lastname = StringField('Lastname')
timezone = SelectField(coerce=str, label='Timezone', default='UTC')
submit = SubmitField('Update profile')
class ConfigForm(Form):
app_name = StringField('App Name', [DataRequired()])
submit = SubmitField('Update config')

1
migrations/README

@ -0,0 +1 @@
Generic single-database configuration.

45
migrations/alembic.ini

@ -0,0 +1,45 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

88
migrations/env.py

@ -0,0 +1,88 @@
from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
import logging
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option('sqlalchemy.url',
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.readthedocs.org/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
engine = engine_from_config(config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
connection = engine.connect()
context.configure(connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
transaction_per_migration=True,
**current_app.extensions['migrate'].configure_args)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

22
migrations/script.py.mako

@ -0,0 +1,22 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision}
Create Date: ${create_date}
"""
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

91
migrations/versions/00_795db5a5e99b_.py

@ -0,0 +1,91 @@
"""Initial revision
Revision ID: 795db5a5e99b
Revises: None
Create Date: 2016-12-31 14:00:38.437184
"""
# revision identifiers, used by Alembic.
revision = '795db5a5e99b'
down_revision = None
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table('config',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('app_name', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('role',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=False),
sa.Column('description', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(length=255), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('password', sa.String(length=255), nullable=False),
sa.Column('active', sa.Boolean(), nullable=True),
sa.Column('confirmed_at', sa.DateTime(), nullable=True),
sa.Column('firstname', sa.String(length=32), nullable=True),
sa.Column('lastname', sa.String(length=32), nullable=True),
sa.Column('timezone', sa.String(length=255), nullable=False),
sa.Column('slug', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('name'),
sa.UniqueConstraint('slug')
)
op.create_table('apitoken',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('token', sa.String(length=255), nullable=False),
sa.Column('secret', sa.String(length=255), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('secret'),
sa.UniqueConstraint('token')
)
op.create_table('logging',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('category', sa.String(length=255), nullable=False),
sa.Column('level', sa.String(length=255), nullable=False),
sa.Column('message', sa.Text(), nullable=False),
sa.Column('timestamp', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('roles_users',
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('role_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['role_id'], ['role.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], )
)
op.create_table('user_logging',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('category', sa.String(length=255), nullable=False),
sa.Column('level', sa.String(length=255), nullable=False),
sa.Column('message', sa.Text(), nullable=False),
sa.Column('timestamp', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
def downgrade():
op.drop_table('user_logging')
op.drop_table('roles_users')
op.drop_table('logging')
op.drop_table('apitoken')
op.drop_table('user')
op.drop_table('role')
op.drop_table('config')

49
migrations/versions/01_da3273ca0f0f_.py

@ -0,0 +1,49 @@
"""Add Sound and SoundInfo
Revision ID: da3273ca0f0f
Revises: 795db5a5e99b
Create Date: 2016-12-31 14:43:11.818727
"""
# revision identifiers, used by Alembic.
revision = 'da3273ca0f0f'
down_revision = '795db5a5e99b'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('sound',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=255), nullable=True),
sa.Column('uploaded', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
sa.Column('description', sa.UnicodeText(), nullable=True),
sa.Column('public', sa.Boolean(), nullable=False),
sa.Column('slug', sa.String(length=255), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('slug')
)
op.create_table('sound_info',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('duration', sa.Float(), nullable=True),
sa.Column('format', sa.String(length=255), nullable=True),
sa.Column('rate', sa.String(length=255), nullable=True),
sa.Column('channels', sa.Integer(), nullable=True),
sa.Column('codec', sa.String(length=255), nullable=True),
sa.Column('sound_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['sound_id'], ['sound.id'], ),
sa.PrimaryKeyConstraint('id')
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('sound_info')
op.drop_table('sound')
### end Alembic commands ###

152
models.py

@ -0,0 +1,152 @@
import datetime
from libqth import is_valid_qth, qth_to_coords, coords_to_qth
from flask_security import SQLAlchemyUserDatastore, UserMixin, RoleMixin
from flask_sqlalchemy import SQLAlchemy, BaseQuery
from geohelper import distance
from sqlalchemy.sql import func
from sqlalchemy_searchable import make_searchable, SearchQueryMixin
from sqlalchemy import event
from slugify import slugify
from sqlalchemy_utils.types import TSVectorType
db = SQLAlchemy()
make_searchable()
class LogQuery(BaseQuery, SearchQueryMixin):
pass
roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False, info={'label': 'Name'})
description = db.Column(db.String(255), info={'label': 'Description'})
__mapper_args__ = {"order_by": name}
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Email'})
name = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Name'})
password = db.Column(db.String(255), nullable=False, info={'label': 'Password'})
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
firstname = db.Column(db.String(32))
lastname = db.Column(db.String(32))
timezone = db.Column(db.String(255), nullable=False, default='UTC') # Managed and fed by pytz
slug = db.Column(db.String(255), unique=True, nullable=True)
roles = db.relationship('Role', secondary=roles_users, backref=db.backref('users', lazy='dynamic'))
apitokens = db.relationship('Apitoken', backref='user', lazy='dynamic', cascade="delete")
user_loggings = db.relationship('UserLogging', backref='user', lazy='dynamic', cascade="delete")
loggings = db.relationship('Logging', backref='user', lazy='dynamic', cascade="delete")
sounds = db.relationship('Sound', backref='user', lazy='dynamic', cascade="delete")
__mapper_args__ = {"order_by": name}
def join_roles(self, string):
return string.join([i.description for i in self.roles])
class Apitoken(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=False)
token = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Token'})
secret = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Secret'})
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
class Config(db.Model):
__tablename__ = "config"
id = db.Column(db.Integer, primary_key=True)
app_name = db.Column(db.String(255), default=None)
class Logging(db.Model):
__tablename__ = "logging"
id = db.Column(db.Integer, primary_key=True)
category = db.Column(db.String(255), nullable=False, default="General")
level = db.Column(db.String(255), nullable=False, default="INFO")
message = db.Column(db.Text, nullable=False)
timestamp = db.Column(db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now())
user_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=True)
class UserLogging(db.Model):
__tablename__ = "user_logging"
id = db.Column(db.Integer, primary_key=True)
category = db.Column(db.String(255), nullable=False, default="General")
level = db.Column(db.String(255), nullable=False, default="INFO")
message = db.Column(db.Text, nullable=False)
timestamp = db.Column(db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now())
#log_id = db.Column(db.Integer(), db.ForeignKey('log.id'), nullable=True)
user_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=False)
class SoundInfo(db.Model):
__tablename__ = "sound_info"
id = db.Column(db.Integer, primary_key=True)
duration = db.Column(db.Float, nullable=True)
format = db.Column(db.String(255), nullable=True)
rate = db.Column(db.String(255), nullable=True)
channels = db.Column(db.Integer, nullable=True)
codec = db.Column(db.String(255), nullable=True)
sound_id = db.Column(db.Integer(), db.ForeignKey('sound.id'), nullable=False)
class Sound(db.Model):
__tablename__ = "sound"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(255), nullable=True)
uploaded = db.Column(db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now())
# TODO genre
# TODO tags
# TODO picture ?
description = db.Column(db.UnicodeText(), nullable=True)
public = db.Column(db.Boolean(), default=True, nullable=False)
slug = db.Column(db.String(255), unique=True, nullable=True)
user_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=False)
sound_infos = db.relationship('SoundInfo', backref='sound_info', lazy='dynamic', cascade="delete")
@event.listens_for(User, 'after_update')
@event.listens_for(User, 'after_insert')
def make_slug(mapper, connection, target):
title = "{0} {1}".format(target.id, target.name)
slug = slugify(title)
connection.execute(
User.__table__.update().where(User.__table__.c.id == target.id).values(slug=slug)
)
@event.listens_for(Sound, 'after_update')
@event.listens_for(Sound, 'after_insert')
def make_slug(mapper, connection, target):
if not target.slug or target.slug == "":
title = "{0} {1}".format(target.id, target.title)
slug = slugify(title)
connection.execute(
User.__table__.update().where(User.__table__.c.id == target.id).values(slug=slug)
)

78
reel2bits.py

@ -0,0 +1,78 @@
# encoding: utf-8
from pprint import pprint as pp
import datetime
import os
import texttable
from flask_debugtoolbar import DebugToolbarExtension
from flask_migrate import MigrateCommand
from flask_script import Manager
from dbseed import make_db_seed
from models import db
try:
from raven.contrib.flask import Sentry
import raven
print(" * Sentry support loaded")
HAS_SENTRY = True
except ImportError as e:
print(" * No Sentry support")
HAS_SENTRY = False
from app import create_app
app = create_app()
if HAS_SENTRY:
app.config['SENTRY_RELEASE'] = raven.fetch_git_sha(os.path.dirname(__file__))
sentry = Sentry(app, dsn=app.config['SENTRY_DSN'])
print(" * Sentry support activated")
print(" * Sentry DSN: %s" % app.config['SENTRY_DSN'])
toolbar = DebugToolbarExtension(app)
manager = Manager(app)
# Other commands
@manager.command
def routes():
"""Dump all routes of defined app"""
table = texttable.Texttable()
table.set_deco(texttable.Texttable().HEADER)
table.set_cols_dtype(['t', 't', 't'])
table.set_cols_align(["l", "l", "l"])
table.set_cols_width([60, 30, 90])
table.add_rows([["Prefix", "Verb", "URI Pattern"]])
for rule in sorted(app.url_map.iter_rules(), key=lambda x: str(x)):
methods = ','.join(rule.methods)
table.add_row([rule.endpoint, methods, rule])
print(table.draw())
@manager.command
def config():
"""Dump config"""
pp(app.config)
@MigrateCommand.command
def seed():
"""Seed database with default content"""
make_db_seed(db)
CacheCommand = Manager(usage='Perform cache actions')
CronCommand = Manager(usage='Perform crons actions')
manager.add_command('db', MigrateCommand)
manager.add_command('cache', CacheCommand)
manager.add_command('cron', CronCommand)
if __name__ == '__main__':
try:
manager.run()
except KeyboardInterrupt as e:
print("Got KeyboardInterrupt, halting...")
print(e)

55
requirements.txt

@ -0,0 +1,55 @@
alembic==0.8.6
bcrypt==2.0.0
blinker==1.4
cffi==1.6.0
click==6.6
dateutils==0.6.6
decorator==4.0.10
dominate==2.2.0
Flask==0.10.1
Flask-Bootstrap==3.3.6.0
Flask-DebugToolbar==0.10.0
Flask-Login==0.3.2
Flask-Mail==0.9.1
Flask-Migrate==1.8.0
Flask-Principal==0.4.0
Flask-Script==2.0.5
Flask-Security==1.7.5
Flask-SQLAlchemy==2.1
Flask-WTF==0.12
geohelper==0.2.0
gunicorn==19.6.0
infinity==1.4
intervals==0.7.1
itsdangerous==0.24
Jinja2==2.8
Mako==1.0.4
MarkupSafe==0.23
parsedatetime==2.1
passlib==1.6.5
pycparser==2.14
pyparsing==2.1.4
python-dateutil==2.5.3
python-editor==1.0
pytz==2016.6.1
requests==2.10.0
six==1.10.0
SQLAlchemy==1.0.13
SQLAlchemy-Searchable==0.10.1
SQLAlchemy-Utils==0.32.7
texttable==0.8.4
Unidecode==0.4.19
validators==0.10.1
virtualenv==1.11.6
visitor==0.1.3
Werkzeug==0.11.10
WTForms==2.1
WTForms-Alchemy==0.16.1
WTForms-Components==0.10.0
mysqlclient==1.3.7
beautifulsoup4==4.4.1
flask-uploads
python-slugify
LatLon
git+http://dev.sigpipe.me/DashieHam/pyHamQth.git#egg=pyhamqth
git+https://github.com/ggramaize/libqth.git#egg=libqth

587
static/css/bootstrap-theme.css

@ -0,0 +1,587 @@
/*!
* Bootstrap v3.3.6 (http://getbootstrap.com)
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
.btn-default,
.btn-primary,
.btn-success,
.btn-info,
.btn-warning,
.btn-danger {
text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
}
.btn-default:active,
.btn-primary:active,
.btn-success:active,
.btn-info:active,
.btn-warning:active,
.btn-danger:active,
.btn-default.active,
.btn-primary.active,
.btn-success.active,
.btn-info.active,
.btn-warning.active,
.btn-danger.active {
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
}
.btn-default.disabled,
.btn-primary.disabled,
.btn-success.disabled,
.btn-info.disabled,
.btn-warning.disabled,
.btn-danger.disabled,
.btn-default[disabled],
.btn-primary[disabled],
.btn-success[disabled],
.btn-info[disabled],
.btn-warning[disabled],
.btn-danger[disabled],
fieldset[disabled] .btn-default,
fieldset[disabled] .btn-primary,
fieldset[disabled] .btn-success,
fieldset[disabled] .btn-info,
fieldset[disabled] .btn-warning,
fieldset[disabled] .btn-danger {
-webkit-box-shadow: none;
box-shadow: none;
}
.btn-default .badge,
.btn-primary .badge,
.btn-success .badge,
.btn-info .badge,
.btn-warning .badge,
.btn-danger .badge {
text-shadow: none;
}
.btn:active,
.btn.active {
background-image: none;
}
.btn-default {
text-shadow: 0 1px 0 #fff;
background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #dbdbdb;
border-color: #ccc;
}
.btn-default:hover,
.btn-default:focus {
background-color: #e0e0e0;
background-position: 0 -15px;
}
.btn-default:active,
.btn-default.active {
background-color: #e0e0e0;
border-color: #dbdbdb;
}
.btn-default.disabled,
.btn-default[disabled],
fieldset[disabled] .btn-default,
.btn-default.disabled:hover,
.btn-default[disabled]:hover,
fieldset[disabled] .btn-default:hover,
.btn-default.disabled:focus,
.btn-default[disabled]:focus,
fieldset[disabled] .btn-default:focus,
.btn-default.disabled.focus,
.btn-default[disabled].focus,
fieldset[disabled] .btn-default.focus,
.btn-default.disabled:active,
.btn-default[disabled]:active,
fieldset[disabled] .btn-default:active,
.btn-default.disabled.active,
.btn-default[disabled].active,
fieldset[disabled] .btn-default.active {
background-color: #e0e0e0;
background-image: none;
}
.btn-primary {
background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));
background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #245580;
}
.btn-primary:hover,
.btn-primary:focus {
background-color: #265a88;
background-position: 0 -15px;
}
.btn-primary:active,
.btn-primary.active {
background-color: #265a88;
border-color: #245580;
}
.btn-primary.disabled,
.btn-primary[disabled],
fieldset[disabled] .btn-primary,
.btn-primary.disabled:hover,
.btn-primary[disabled]:hover,
fieldset[disabled] .btn-primary:hover,
.btn-primary.disabled:focus,
.btn-primary[disabled]:focus,
fieldset[disabled] .btn-primary:focus,
.btn-primary.disabled.focus,
.btn-primary[disabled].focus,
fieldset[disabled] .btn-primary.focus,
.btn-primary.disabled:active,
.btn-primary[disabled]:active,
fieldset[disabled] .btn-primary:active,
.btn-primary.disabled.active,
.btn-primary[disabled].active,
fieldset[disabled] .btn-primary.active {
background-color: #265a88;
background-image: none;
}
.btn-success {
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #3e8f3e;
}
.btn-success:hover,
.btn-success:focus {
background-color: #419641;
background-position: 0 -15px;
}
.btn-success:active,
.btn-success.active {
background-color: #419641;
border-color: #3e8f3e;
}
.btn-success.disabled,
.btn-success[disabled],
fieldset[disabled] .btn-success,
.btn-success.disabled:hover,
.btn-success[disabled]:hover,
fieldset[disabled] .btn-success:hover,
.btn-success.disabled:focus,
.btn-success[disabled]:focus,
fieldset[disabled] .btn-success:focus,
.btn-success.disabled.focus,
.btn-success[disabled].focus,
fieldset[disabled] .btn-success.focus,
.btn-success.disabled:active,
.btn-success[disabled]:active,
fieldset[disabled] .btn-success:active,
.btn-success.disabled.active,
.btn-success[disabled].active,
fieldset[disabled] .btn-success.active {
background-color: #419641;
background-image: none;
}
.btn-info {
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #28a4c9;
}
.btn-info:hover,
.btn-info:focus {
background-color: #2aabd2;
background-position: 0 -15px;
}
.btn-info:active,
.btn-info.active {
background-color: #2aabd2;
border-color: #28a4c9;
}
.btn-info.disabled,
.btn-info[disabled],
fieldset[disabled] .btn-info,
.btn-info.disabled:hover,
.btn-info[disabled]:hover,
fieldset[disabled] .btn-info:hover,
.btn-info.disabled:focus,
.btn-info[disabled]:focus,
fieldset[disabled] .btn-info:focus,
.btn-info.disabled.focus,
.btn-info[disabled].focus,
fieldset[disabled] .btn-info.focus,
.btn-info.disabled:active,
.btn-info[disabled]:active,
fieldset[disabled] .btn-info:active,
.btn-info.disabled.active,
.btn-info[disabled].active,
fieldset[disabled] .btn-info.active {
background-color: #2aabd2;
background-image: none;
}
.btn-warning {
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #e38d13;
}
.btn-warning:hover,
.btn-warning:focus {
background-color: #eb9316;
background-position: 0 -15px;
}
.btn-warning:active,
.btn-warning.active {
background-color: #eb9316;
border-color: #e38d13;
}
.btn-warning.disabled,
.btn-warning[disabled],
fieldset[disabled] .btn-warning,
.btn-warning.disabled:hover,
.btn-warning[disabled]:hover,
fieldset[disabled] .btn-warning:hover,
.btn-warning.disabled:focus,
.btn-warning[disabled]:focus,
fieldset[disabled] .btn-warning:focus,
.btn-warning.disabled.focus,
.btn-warning[disabled].focus,
fieldset[disabled] .btn-warning.focus,
.btn-warning.disabled:active,
.btn-warning[disabled]:active,
fieldset[disabled] .btn-warning:active,
.btn-warning.disabled.active,
.btn-warning[disabled].active,
fieldset[disabled] .btn-warning.active {
background-color: #eb9316;
background-image: none;
}
.btn-danger {
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #b92c28;
}
.btn-danger:hover,
.btn-danger:focus {
background-color: #c12e2a;
background-position: 0 -15px;
}
.btn-danger:active,
.btn-danger.active {
background-color: #c12e2a;
border-color: #b92c28;
}
.btn-danger.disabled,
.btn-danger[disabled],
fieldset[disabled] .btn-danger,
.btn-danger.disabled:hover,
.btn-danger[disabled]:hover,
fieldset[disabled] .btn-danger:hover,
.btn-danger.disabled:focus,
.btn-danger[disabled]:focus,