Browse Source

That's ugly

Dashie der otter 1 year ago
parent
commit
0cebb67f58
Signed by: Dashie <dashie@sigpipe.me> GPG Key ID: C2D57B325840B755
100 changed files with 28782 additions and 0 deletions
  1. 7
    0
      .gitignore
  2. 114
    0
      app.py
  3. 35
    0
      config.py.sample
  4. 0
    0
      controllers/__init__.py
  5. 43
    0
      controllers/admin.py
  6. 36
    0
      controllers/api.py
  7. 33
    0
      controllers/main.py
  8. 61
    0
      controllers/sound.py
  9. 61
    0
      controllers/users.py
  10. 15
    0
      crons.py
  11. 46
    0
      dbseed.py
  12. 71
    0
      forms.py
  13. 1
    0
      migrations/README
  14. 45
    0
      migrations/alembic.ini
  15. 88
    0
      migrations/env.py
  16. 22
    0
      migrations/script.py.mako
  17. 91
    0
      migrations/versions/00_795db5a5e99b_.py
  18. 49
    0
      migrations/versions/01_da3273ca0f0f_.py
  19. 152
    0
      models.py
  20. 78
    0
      reel2bits.py
  21. 55
    0
      requirements.txt
  22. 587
    0
      static/css/bootstrap-theme.css
  23. 1
    0
      static/css/bootstrap-theme.css.map
  24. 6
    0
      static/css/bootstrap-theme.min.css
  25. 1
    0
      static/css/bootstrap-theme.min.css.map
  26. 6760
    0
      static/css/bootstrap.css
  27. 1
    0
      static/css/bootstrap.css.map
  28. 6
    0
      static/css/bootstrap.min.css
  29. 1
    0
      static/css/bootstrap.min.css.map
  30. BIN
      static/css/chosen-sprite.png
  31. BIN
      static/css/chosen-sprite@2x.png
  32. 450
    0
      static/css/chosen.css
  33. 3
    0
      static/css/chosen.min.css
  34. 239
    0
      static/css/font-awesome.css
  35. 4
    0
      static/css/font-awesome.min.css
  36. 15
    0
      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
    0
      static/css/jquery-ui.css
  64. 7
    0
      static/css/jquery-ui.min.css
  65. 833
    0
      static/css/jquery-ui.structure.css
  66. 5
    0
      static/css/jquery-ui.structure.min.css
  67. 410
    0
      static/css/jquery-ui.theme.css
  68. 5
    0
      static/css/jquery-ui.theme.min.css
  69. 62
    0
      static/css/pygments.css
  70. 74
    0
      static/css/style.css
  71. 180
    0
      static/css/timeline.css
  72. 18
    0
      static/datatables/Contributing.md
  73. 53
    0
      static/datatables/Readme.md
  74. 762
    0
      static/datatables/examples/advanced_init/column_render.html
  75. 755
    0
      static/datatables/examples/advanced_init/complex_header.html
  76. 746
    0
      static/datatables/examples/advanced_init/defaults.html
  77. 755
    0
      static/datatables/examples/advanced_init/dom_multiple_elements.html
  78. 756
    0
      static/datatables/examples/advanced_init/dom_toolbar.html
  79. 761
    0
      static/datatables/examples/advanced_init/dt_events.html
  80. 745
    0
      static/datatables/examples/advanced_init/events_live.html
  81. 743
    0
      static/datatables/examples/advanced_init/footer_callback.html
  82. 745
    0
      static/datatables/examples/advanced_init/html5-data-attributes.html
  83. 744
    0
      static/datatables/examples/advanced_init/html5-data-options.html
  84. 73
    0
      static/datatables/examples/advanced_init/index.html
  85. 742
    0
      static/datatables/examples/advanced_init/language_file.html
  86. 744
    0
      static/datatables/examples/advanced_init/length_menu.html
  87. 771
    0
      static/datatables/examples/advanced_init/object_dom_read.html
  88. 755
    0
      static/datatables/examples/advanced_init/row_callback.html
  89. 807
    0
      static/datatables/examples/advanced_init/row_grouping.html
  90. 764
    0
      static/datatables/examples/advanced_init/sort_direction_control.html
  91. 310
    0
      static/datatables/examples/ajax/custom_data_flat.html
  92. 296
    0
      static/datatables/examples/ajax/custom_data_property.html
  93. 460
    0
      static/datatables/examples/ajax/data/arrays.txt
  94. 460
    0
      static/datatables/examples/ajax/data/arrays_custom_prop.txt
  95. 688
    0
      static/datatables/examples/ajax/data/arrays_subobjects.txt
  96. 460
    0
      static/datatables/examples/ajax/data/objects.txt
  97. 688
    0
      static/datatables/examples/ajax/data/objects_deep.txt
  98. 458
    0
      static/datatables/examples/ajax/data/objects_root_array.txt
  99. 745
    0
      static/datatables/examples/ajax/data/objects_subarrays.txt
  100. 0
    0
      static/datatables/examples/ajax/data/orthogonal.txt

+ 7
- 0
.gitignore View File

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

+ 114
- 0
app.py View File

@@ -0,0 +1,114 @@
1
+# encoding: utf-8
2
+import logging
3
+import os
4
+import subprocess
5
+from logging.handlers import RotatingFileHandler
6
+
7
+from flask import Flask, render_template, g, send_from_directory, jsonify
8
+from flask_bootstrap import Bootstrap
9
+from flask_mail import Mail
10
+from flask_migrate import Migrate
11
+from flask_security import Security
12
+from flask_uploads import configure_uploads, UploadSet, IMAGES
13
+
14
+from controllers.admin import bp_admin
15
+from controllers.main import bp_main
16
+from controllers.users import bp_users
17
+from forms import ExtendedRegisterForm
18
+from models import db, user_datastore
19
+from utils import dt_utc_to_user_tz, InvalidUsage, show_date_no_offset, is_admin, gcfg
20
+
21
+__VERSION__ = "0.0.1"
22
+
23
+
24
+def create_app(cfg=None):
25
+    # App Configuration
26
+    if cfg is None:
27
+        cfg = {}
28
+    app = Flask(__name__)
29
+    app.config.from_pyfile("config.py")
30
+    app.config.update(cfg)
31
+
32
+    Bootstrap(app)
33
+
34
+    app.jinja_env.add_extension('jinja2.ext.with_')
35
+    app.jinja_env.add_extension('jinja2.ext.do')
36
+    app.jinja_env.globals.update(is_admin=is_admin)
37
+    app.jinja_env.globals.update(gcfg=gcfg)
38
+
39
+    # Logging
40
+    if not app.debug:
41
+        formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')
42
+        file_handler = RotatingFileHandler("%s/errors_app.log" % os.getcwd(), 'a', 1000000, 1)
43
+        file_handler.setLevel(logging.DEBUG)
44
+        file_handler.setFormatter(formatter)
45
+        app.logger.addHandler(file_handler)
46
+
47
+    mail = Mail(app)
48
+    migrate = Migrate(app, db)
49
+
50
+    db.init_app(app)
51
+
52
+    # Setup Flask-Security
53
+    security = Security(app, user_datastore,
54
+                        register_form=ExtendedRegisterForm)
55
+
56
+    git_version = ""
57
+    gitpath = os.path.join(os.getcwd(), ".git")
58
+    if os.path.isdir(gitpath):
59
+        git_version = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'])
60
+        if git_version:
61
+            git_version = git_version.strip().decode('UTF-8')
62
+
63
+    @app.before_request
64
+    def before_request():
65
+        g.cfg = {
66
+            'REEL2BITS_VERSION_VER': __VERSION__,
67
+            'REEL2BITS_VERSION_GIT': git_version,
68
+            'REEL2BITS_VERSION': "{0} ({1})".format(__VERSION__, git_version),
69
+        }
70
+
71
+    @app.errorhandler(InvalidUsage)
72
+    def handle_invalid_usage(error):
73
+        response = jsonify(error.to_dict())
74
+        response.status_code = error.status_code
75
+        return response
76
+
77
+    pictures = UploadSet('pictures', IMAGES)
78
+    configure_uploads(app, pictures)
79
+
80
+    app.register_blueprint(bp_main)
81
+    app.register_blueprint(bp_users)
82
+    app.register_blueprint(bp_admin)
83
+
84
+    # Used in development
85
+    @app.route('/uploads/<path:stuff>', methods=['GET'])
86
+    def get_uploads_stuff(stuff):
87
+        print("Get {0} from {1}".format(stuff, app.config['UPLOADS_DEFAULT_DEST']))
88
+        return send_from_directory(app.config['UPLOADS_DEFAULT_DEST'], stuff, as_attachment=False)
89
+
90
+    @app.errorhandler(404)
91
+    def page_not_found(msg):
92
+        pcfg = {"title": "Whoops, something failed.",
93
+                "error": 404, "message": "Page not found", "e": msg}
94
+        return render_template('error_page.jinja2', pcfg=pcfg), 404
95
+
96
+    @app.errorhandler(403)
97
+    def err_forbidden(msg):
98
+        pcfg = {"title": "Whoops, something failed.",
99
+                "error": 403, "message": "Access forbidden", "e": msg}
100
+        return render_template('error_page.jinja2', pcfg=pcfg), 403
101
+
102
+    @app.errorhandler(410)
103
+    def err_gone(msg):
104
+        pcfg = {"title": "Whoops, something failed.",
105
+                "error": 410, "message": "Gone", "e": msg}
106
+        return render_template('error_page.jinja2', pcfg=pcfg), 410
107
+
108
+    if not app.debug:
109
+        @app.errorhandler(500)
110
+        def err_failed(msg):
111
+            pcfg = {"title": "Whoops, something failed.", "error": 500, "message": "Something is broken", "e": msg}
112
+            return render_template('error_page.jinja2', pcfg=pcfg), 500
113
+
114
+    return app

+ 35
- 0
config.py.sample View File

@@ -0,0 +1,35 @@
1
+DEBUG = True
2
+
3
+SECRET_KEY = 'aglaglawx2r31ht1g0m1vcq'
4
+# SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://dashie@localhost/reel2bits'
5
+SQLALCHEMY_DATABASE_URI = 'postgresql+pg8000://dashie@localhost/reel2bits'
6
+# SQLALCHEMY_DATABASE_URI = 'sqlite:///reel2bits.db'
7
+SQLALCHEMY_ECHO = False
8
+SQLALCHEMY_TRACK_MODIFICATIONS = False
9
+
10
+SECURITY_CONFIRMABLE = False
11
+SECURITY_REGISTERABLE = False  # deactivate registration
12
+SECURITY_RECOVERABLE = True
13
+SECURITY_TRACKABLE = False
14
+SECURITY_CHANGEABLE = True
15
+SECURITY_PASSWORD_HASH = 'bcrypt'
16
+SECURITY_PASSWORD_SALT = 'ottersrulestheworld'
17
+# SECURITY_URL_PREFIX = '/sec'
18
+
19
+SECURITY_SEND_REGISTER_EMAIL = False
20
+SECURITY_SEND_PASSWORD_CHANGE_EMAIL = False
21
+SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL = False
22
+
23
+BOOTSTRAP_USE_MINIFIED = True
24
+BOOTSTRAP_SERVE_LOCAL = True
25
+BOOTSTRAP_CDN_FORCE_SSL = True
26
+BOOTSTRAP_QUERYSTRING_REVVING = True
27
+
28
+DEBUG_TB_PROFILER_ENABLED = True
29
+DEBUG_TB_INTERCEPT_REDIRECTS = False
30
+
31
+# Paginations
32
+
33
+UPLOADED_SND_DEST = "/home/reel2bits/uploads/snd"
34
+UPLOADS_DEFAULT_DEST = "/home/reel2bits/uploads"
35
+TEMP_DOWNLOAD_FOLDER = "/home/reel2bits/tmp"

+ 0
- 0
controllers/__init__.py View File


+ 43
- 0
controllers/admin.py View File

@@ -0,0 +1,43 @@
1
+from flask import Blueprint, render_template, request, redirect, url_for, flash
2
+from flask_security import login_required
3
+
4
+from forms import ConfigForm
5
+from models import db, Logging, Config
6
+from utils import is_admin
7
+
8
+bp_admin = Blueprint('bp_admin', __name__)
9
+
10
+
11
+@bp_admin.route('/admin/logs', methods=['GET'])
12
+@login_required
13
+def logs():
14
+    if not is_admin():
15
+        return redirect(url_for('bp_home.home'))
16
+    pcfg = {"title": "Application Logs"}
17
+    _logs = Logging.query.order_by(Logging.timestamp.desc()).limit(100).all()
18
+    return render_template('admin/logs.jinja2', pcfg=pcfg, logs=_logs)
19
+
20
+
21
+@bp_admin.route('/admin/config', methods=['GET', 'POST'])
22
+@login_required
23
+def config():
24
+    if not is_admin():
25
+        return redirect(url_for('bp_main.home'))
26
+
27
+    pcfg = {"title": "Application Config"}
28
+
29
+    _config = Config.query.one()
30
+    if not _config:
31
+        flash("Config not found", 'error')
32
+        return redirect(url_for("bp_main.home"))
33
+
34
+    form = ConfigForm(request.form, _config)
35
+
36
+    if form.validate_on_submit():
37
+        _config.app_name = form.app_name.data
38
+
39
+        db.session.commit()
40
+        flash("Configuration updated", "info")
41
+        return redirect(url_for('bp_admin.config'))
42
+
43
+    return render_template('admin/config.jinja2', pcfg=pcfg, form=form)

+ 36
- 0
controllers/api.py View File

@@ -0,0 +1,36 @@
1
+from flask import Blueprint, redirect, url_for, abort, flash
2
+from flask_security import login_required, current_user
3
+
4
+from models import db, Apitoken
5
+from utils import generate_uniques_apitoken
6
+
7
+bp_api = Blueprint('bp_api', __name__)
8
+
9
+
10
+@bp_api.route('/api/token/new')
11
+@login_required
12
+def apitoken_new():
13
+    apitoken = generate_uniques_apitoken()
14
+    if not apitoken:
15
+        return abort(500)
16
+
17
+    a = Apitoken()
18
+    a.user_id = current_user.id
19
+    a.token = apitoken["token"]
20
+    a.secret = apitoken["secret"]
21
+    db.session.add(a)
22
+    db.session.commit()
23
+    return redirect(url_for('bp_users.user_profile'))
24
+
25
+
26
+@bp_api.route('/api/token/<string:apit>/del')
27
+@login_required
28
+def apitoken_del(apit):
29
+    apitoken = Apitoken.query.filter(Apitoken.id == apit).first()
30
+    if not apitoken:
31
+        flash("API Token not found", "error")
32
+        return redirect(url_for("bp_main.home"))
33
+
34
+    db.session.delete(apitoken)
35
+    db.session.commit()
36
+    return redirect(url_for('bp_users.user_profile'))

+ 33
- 0
controllers/main.py View File

@@ -0,0 +1,33 @@
1
+from flask import Blueprint, render_template, flash, redirect, url_for
2
+from flask_security import current_user
3
+from sqlalchemy import func
4
+
5
+from models import db, User, Config
6
+
7
+bp_main = Blueprint('bp_main', __name__)
8
+
9
+
10
+# Show public logbooks
11
+@bp_main.route('/')
12
+def home():
13
+    _config = Config.query.one()
14
+    if not _config:
15
+        flash("Config not found", 'error')
16
+        return redirect(url_for("bp_main.home"))
17
+
18
+    pcfg = {"title": "Home", "app_name": _config.app_name}
19
+    users = User.query.all()
20
+
21
+    return render_template('home.jinja2', pcfg=pcfg, users=users)
22
+
23
+
24
+@bp_main.route('/about')
25
+def about():
26
+    _config = Config.query.one()
27
+    if not _config:
28
+        flash("Config not found", 'error')
29
+        return redirect(url_for("bp_main.home"))
30
+
31
+    pcfg = {"title": _config.app_name}
32
+
33
+    return render_template('about.jinja2', pcfg=pcfg)

+ 61
- 0
controllers/sound.py View File

@@ -0,0 +1,61 @@
1
+import pytz
2
+from flask import Blueprint, render_template, request, redirect, url_for, flash
3
+from flask_security import login_required, current_user
4
+from sqlalchemy import func
5
+
6
+from forms import UserProfileForm
7
+from models import db, User, UserLogging
8
+
9
+bp_users = Blueprint('bp_users', __name__)
10
+
11
+
12
+@bp_users.route('/user/logs', methods=['GET'])
13
+@login_required
14
+def logs():
15
+    level = request.args.get('level')
16
+    pcfg = {"title": "User Logs"}
17
+    if level:
18
+        _logs = UserLogging.query.filter(UserLogging.level == level.upper(),
19
+                                         UserLogging.user_id == current_user.id
20
+                                         ).order_by(UserLogging.timestamp.desc()).limit(100).all()
21
+    else:
22
+        _logs = UserLogging.query.filter(UserLogging.user_id == current_user.id
23
+                                         ).order_by(UserLogging.timestamp.desc()).limit(100).all()
24
+    return render_template('users/user_logs.jinja2', pcfg=pcfg, logs=_logs)
25
+
26
+
27
+@bp_users.route('/user', methods=['GET'])
28
+@login_required
29
+def profile():
30
+    pcfg = {"title": "My Profile"}
31
+
32
+    user = User.query.filter(User.id == current_user.id).first()
33
+    if not user:
34
+        flash("User not found", 'error')
35
+        return redirect(url_for("bp_main.home"))
36
+
37
+    return render_template('users/profile.jinja2', pcfg=pcfg, user=user)
38
+
39
+
40
+@bp_users.route('/user/edit', methods=['GET', 'POST'])
41
+@login_required
42
+def edit():
43
+    pcfg = {"title": "Edit my profile"}
44
+
45
+    user = User.query.filter(User.id == current_user.id).first()
46
+    if not user:
47
+        flash("User not found", 'error')
48
+        return redirect(url_for("bp_main.home"))
49
+
50
+    form = UserProfileForm(request.form, user)
51
+    form.timezone.choices = [[str(i), str(i)] for i in pytz.all_timezones]
52
+
53
+    if form.validate_on_submit():
54
+        user.lastname = form.lastname.data
55
+        user.firstname = form.firstname.data
56
+        user.timezone = form.timezone.data
57
+
58
+        db.session.commit()
59
+        return redirect(url_for('bp_users.profile'))
60
+
61
+    return render_template('users/edit.jinja2', pcfg=pcfg, form=form, user=user)

+ 61
- 0
controllers/users.py View File

@@ -0,0 +1,61 @@
1
+import pytz
2
+from flask import Blueprint, render_template, request, redirect, url_for, flash
3
+from flask_security import login_required, current_user
4
+from sqlalchemy import func
5
+
6
+from forms import UserProfileForm
7
+from models import db, User, UserLogging
8
+
9
+bp_users = Blueprint('bp_users', __name__)
10
+
11
+
12
+@bp_users.route('/user/logs', methods=['GET'])
13
+@login_required
14
+def logs():
15
+    level = request.args.get('level')
16
+    pcfg = {"title": "User Logs"}
17
+    if level:
18
+        _logs = UserLogging.query.filter(UserLogging.level == level.upper(),
19
+                                         UserLogging.user_id == current_user.id
20
+                                         ).order_by(UserLogging.timestamp.desc()).limit(100).all()
21
+    else:
22
+        _logs = UserLogging.query.filter(UserLogging.user_id == current_user.id
23
+                                         ).order_by(UserLogging.timestamp.desc()).limit(100).all()
24
+    return render_template('users/user_logs.jinja2', pcfg=pcfg, logs=_logs)
25
+
26
+
27
+@bp_users.route('/user', methods=['GET'])
28
+@login_required
29
+def profile():
30
+    pcfg = {"title": "My Profile"}
31
+
32
+    user = User.query.filter(User.id == current_user.id).first()
33
+    if not user:
34
+        flash("User not found", 'error')
35
+        return redirect(url_for("bp_main.home"))
36
+
37
+    return render_template('users/profile.jinja2', pcfg=pcfg, user=user)
38
+
39
+
40
+@bp_users.route('/user/edit', methods=['GET', 'POST'])
41
+@login_required
42
+def edit():
43
+    pcfg = {"title": "Edit my profile"}
44
+
45
+    user = User.query.filter(User.id == current_user.id).first()
46
+    if not user:
47
+        flash("User not found", 'error')
48
+        return redirect(url_for("bp_main.home"))
49
+
50
+    form = UserProfileForm(request.form, user)
51
+    form.timezone.choices = [[str(i), str(i)] for i in pytz.all_timezones]
52
+
53
+    if form.validate_on_submit():
54
+        user.lastname = form.lastname.data
55
+        user.firstname = form.firstname.data
56
+        user.timezone = form.timezone.data
57
+
58
+        db.session.commit()
59
+        return redirect(url_for('bp_users.profile'))
60
+
61
+    return render_template('users/edit.jinja2', pcfg=pcfg, form=form, user=user)

+ 15
- 0
crons.py View File

@@ -0,0 +1,15 @@
1
+from __future__ import print_function
2
+
3
+import gzip
4
+import os
5
+import shutil
6
+import urllib.parse
7
+import urllib.error
8
+import urllib.request
9
+import xml.etree.ElementTree as ElementTree
10
+import re
11
+import datetime
12
+
13
+from dateutil import parser
14
+from flask import current_app
15
+

+ 46
- 0
dbseed.py View File

@@ -0,0 +1,46 @@
1
+from models import user_datastore, Config, Role
2
+
3
+
4
+def make_db_seed(db):
5
+    print("== Seeding database")
6
+    db.session.begin(subtransactions=True)
7
+    try:
8
+        print("++ Seeding config")
9
+        seed_config(db)
10
+        seed_users(db)  # after timezones because not null relation
11
+        # also seeds roles admin/user
12
+    except:
13
+        db.session.rollback()
14
+        raise
15
+
16
+
17
+def seed_users(db):
18
+    print("++ Seeding users")
19
+    role_usr = Role()
20
+    role_usr.name = 'user'
21
+    role_usr.description = 'Simple user'
22
+
23
+    role_adm = Role()
24
+    role_adm.name = 'admin'
25
+    role_adm.description = 'Admin user'
26
+
27
+    db.session.add(role_usr)
28
+    db.session.add(role_adm)
29
+
30
+    user_datastore.create_user(
31
+        email='dashie@sigpipe.me',
32
+        password='fluttershy',
33
+        name='toto',
34
+        timezone='UTC',
35
+        roles=[role_adm]
36
+    )
37
+    db.session.commit()
38
+    return
39
+
40
+
41
+def seed_config(db):
42
+    a = Config(app_name='My reel2bits instance')
43
+    db.session.add(a)
44
+    db.session.commit()
45
+    db.session.commit()
46
+    # Bug, two commit necessary

+ 71
- 0
forms.py View File

@@ -0,0 +1,71 @@
1
+import datetime
2
+from libqth import is_valid_qth
3
+
4
+from flask_security import RegisterForm, current_user
5
+from flask_uploads import UploadSet, IMAGES
6
+from flask_wtf import Form
7
+from flask_wtf.file import FileField, FileAllowed, FileRequired
8
+from wtforms import PasswordField, SubmitField, TextAreaField, SelectField, IntegerField, \
9
+    HiddenField, BooleanField
10
+from wtforms.ext.dateutil.fields import DateTimeField
11
+from wtforms.ext.sqlalchemy.fields import QuerySelectField
12
+from wtforms.validators import DataRequired, ValidationError, Optional
13
+from wtforms_alchemy import model_form_factory
14
+from wtforms_components.fields import SelectField as WTFComponentsSelectField
15
+from wtforms import widgets
16
+from wtforms.fields.core import StringField
17
+
18
+from models import db, User
19
+from utils import dt_utc_to_user_tz
20
+
21
+BaseModelForm = model_form_factory(Form)
22
+
23
+pictures = UploadSet('pictures', IMAGES)
24
+
25
+
26
+class PasswordFieldNotHidden(StringField):
27
+    """
28
+    Original source: https://github.com/wtforms/wtforms/blob/2.0.2/wtforms/fields/simple.py#L35-L42
29
+
30
+    A StringField, except renders an ``<input type="password">``.
31
+    Also, whatever value is accepted by this field is not rendered back
32
+    to the browser like normal fields.
33
+    """
34
+    widget = widgets.PasswordInput(hide_value=False)
35
+
36
+
37
+class ModelForm(BaseModelForm):
38
+    @classmethod
39
+    def get_session(cls):
40
+        return db.session
41
+
42
+
43
+class ExtendedRegisterForm(RegisterForm):
44
+    name = StringField('Name', [DataRequired()])
45
+
46
+    def validate_name(form, field):
47
+        if len(field.data) <= 0:
48
+            raise ValidationError("Username required")
49
+
50
+        u = User.query.filter(User.name == field.data).first()
51
+        if u:
52
+            raise ValidationError("Username already taken")
53
+
54
+
55
+class UserProfileForm(ModelForm):
56
+    class Meta:
57
+        model = User
58
+
59
+    password = PasswordField('Password')
60
+    name = StringField('Name')
61
+    email = StringField('Email')
62
+    firstname = StringField('Firstname')
63
+    lastname = StringField('Lastname')
64
+    timezone = SelectField(coerce=str, label='Timezone', default='UTC')
65
+    submit = SubmitField('Update profile')
66
+
67
+
68
+class ConfigForm(Form):
69
+    app_name = StringField('App Name', [DataRequired()])
70
+
71
+    submit = SubmitField('Update config')

+ 1
- 0
migrations/README View File

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

+ 45
- 0
migrations/alembic.ini View File

@@ -0,0 +1,45 @@
1
+# A generic, single database configuration.
2
+
3
+[alembic]
4
+# template used to generate migration files
5
+# file_template = %%(rev)s_%%(slug)s
6
+
7
+# set to 'true' to run the environment during
8
+# the 'revision' command, regardless of autogenerate
9
+# revision_environment = false
10
+
11
+
12
+# Logging configuration
13
+[loggers]
14
+keys = root,sqlalchemy,alembic
15
+
16
+[handlers]
17
+keys = console
18
+
19
+[formatters]
20
+keys = generic
21
+
22
+[logger_root]
23
+level = WARN
24
+handlers = console
25
+qualname =
26
+
27
+[logger_sqlalchemy]
28
+level = WARN
29
+handlers =
30
+qualname = sqlalchemy.engine
31
+
32
+[logger_alembic]
33
+level = INFO
34
+handlers =
35
+qualname = alembic
36
+
37
+[handler_console]
38
+class = StreamHandler
39
+args = (sys.stderr,)
40
+level = NOTSET
41
+formatter = generic
42
+
43
+[formatter_generic]
44
+format = %(levelname)-5.5s [%(name)s] %(message)s
45
+datefmt = %H:%M:%S

+ 88
- 0
migrations/env.py View File

@@ -0,0 +1,88 @@
1
+from __future__ import with_statement
2
+from alembic import context
3
+from sqlalchemy import engine_from_config, pool
4
+from logging.config import fileConfig
5
+import logging
6
+
7
+# this is the Alembic Config object, which provides
8
+# access to the values within the .ini file in use.
9
+config = context.config
10
+
11
+# Interpret the config file for Python logging.
12
+# This line sets up loggers basically.
13
+fileConfig(config.config_file_name)
14
+logger = logging.getLogger('alembic.env')
15
+
16
+# add your model's MetaData object here
17
+# for 'autogenerate' support
18
+# from myapp import mymodel
19
+# target_metadata = mymodel.Base.metadata
20
+from flask import current_app
21
+config.set_main_option('sqlalchemy.url',
22
+                       current_app.config.get('SQLALCHEMY_DATABASE_URI'))
23
+target_metadata = current_app.extensions['migrate'].db.metadata
24
+
25
+# other values from the config, defined by the needs of env.py,
26
+# can be acquired:
27
+# my_important_option = config.get_main_option("my_important_option")
28
+# ... etc.
29
+
30
+
31
+def run_migrations_offline():
32
+    """Run migrations in 'offline' mode.
33
+
34
+    This configures the context with just a URL
35
+    and not an Engine, though an Engine is acceptable
36
+    here as well.  By skipping the Engine creation
37
+    we don't even need a DBAPI to be available.
38
+
39
+    Calls to context.execute() here emit the given string to the
40
+    script output.
41
+
42
+    """
43
+    url = config.get_main_option("sqlalchemy.url")
44
+    context.configure(url=url)
45
+
46
+    with context.begin_transaction():
47
+        context.run_migrations()
48
+
49
+
50
+def run_migrations_online():
51
+    """Run migrations in 'online' mode.
52
+
53
+    In this scenario we need to create an Engine
54
+    and associate a connection with the context.
55
+
56
+    """
57
+
58
+    # this callback is used to prevent an auto-migration from being generated
59
+    # when there are no changes to the schema
60
+    # reference: http://alembic.readthedocs.org/en/latest/cookbook.html
61
+    def process_revision_directives(context, revision, directives):
62
+        if getattr(config.cmd_opts, 'autogenerate', False):
63
+            script = directives[0]
64
+            if script.upgrade_ops.is_empty():
65
+                directives[:] = []
66
+                logger.info('No changes in schema detected.')
67
+
68
+    engine = engine_from_config(config.get_section(config.config_ini_section),
69
+                                prefix='sqlalchemy.',
70
+                                poolclass=pool.NullPool)
71
+
72
+    connection = engine.connect()
73
+    context.configure(connection=connection,
74
+                      target_metadata=target_metadata,
75
+                      process_revision_directives=process_revision_directives,
76
+                      transaction_per_migration=True,
77
+                      **current_app.extensions['migrate'].configure_args)
78
+
79
+    try:
80
+        with context.begin_transaction():
81
+            context.run_migrations()
82
+    finally:
83
+        connection.close()
84
+
85
+if context.is_offline_mode():
86
+    run_migrations_offline()
87
+else:
88
+    run_migrations_online()

+ 22
- 0
migrations/script.py.mako View File

@@ -0,0 +1,22 @@
1
+"""${message}
2
+
3
+Revision ID: ${up_revision}
4
+Revises: ${down_revision}
5
+Create Date: ${create_date}
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = ${repr(up_revision)}
11
+down_revision = ${repr(down_revision)}
12
+
13
+from alembic import op
14
+import sqlalchemy as sa
15
+${imports if imports else ""}
16
+
17
+def upgrade():
18
+    ${upgrades if upgrades else "pass"}
19
+
20
+
21
+def downgrade():
22
+    ${downgrades if downgrades else "pass"}

+ 91
- 0
migrations/versions/00_795db5a5e99b_.py View File

@@ -0,0 +1,91 @@
1
+"""Initial revision
2
+
3
+Revision ID: 795db5a5e99b
4
+Revises: None
5
+Create Date: 2016-12-31 14:00:38.437184
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = '795db5a5e99b'
11
+down_revision = None
12
+
13
+from alembic import op
14
+import sqlalchemy as sa
15
+
16
+
17
+def upgrade():
18
+    op.create_table('config',
19
+    sa.Column('id', sa.Integer(), nullable=False),
20
+    sa.Column('app_name', sa.String(length=255), nullable=True),
21
+    sa.PrimaryKeyConstraint('id')
22
+    )
23
+    op.create_table('role',
24
+    sa.Column('id', sa.Integer(), nullable=False),
25
+    sa.Column('name', sa.String(length=80), nullable=False),
26
+    sa.Column('description', sa.String(length=255), nullable=True),
27
+    sa.PrimaryKeyConstraint('id'),
28
+    sa.UniqueConstraint('name')
29
+    )
30
+    op.create_table('user',
31
+    sa.Column('id', sa.Integer(), nullable=False),
32
+    sa.Column('email', sa.String(length=255), nullable=False),
33
+    sa.Column('name', sa.String(length=255), nullable=False),
34
+    sa.Column('password', sa.String(length=255), nullable=False),
35
+    sa.Column('active', sa.Boolean(), nullable=True),
36
+    sa.Column('confirmed_at', sa.DateTime(), nullable=True),
37
+    sa.Column('firstname', sa.String(length=32), nullable=True),
38
+    sa.Column('lastname', sa.String(length=32), nullable=True),
39
+    sa.Column('timezone', sa.String(length=255), nullable=False),
40
+    sa.Column('slug', sa.String(length=255), nullable=True),
41
+    sa.PrimaryKeyConstraint('id'),
42
+    sa.UniqueConstraint('email'),
43
+    sa.UniqueConstraint('name'),
44
+    sa.UniqueConstraint('slug')
45
+    )
46
+    op.create_table('apitoken',
47
+    sa.Column('id', sa.Integer(), nullable=False),
48
+    sa.Column('user_id', sa.Integer(), nullable=False),
49
+    sa.Column('token', sa.String(length=255), nullable=False),
50
+    sa.Column('secret', sa.String(length=255), nullable=False),
51
+    sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
52
+    sa.PrimaryKeyConstraint('id'),
53
+    sa.UniqueConstraint('secret'),
54
+    sa.UniqueConstraint('token')
55
+    )
56
+    op.create_table('logging',
57
+    sa.Column('id', sa.Integer(), nullable=False),
58
+    sa.Column('category', sa.String(length=255), nullable=False),
59
+    sa.Column('level', sa.String(length=255), nullable=False),
60
+    sa.Column('message', sa.Text(), nullable=False),
61
+    sa.Column('timestamp', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
62
+    sa.Column('user_id', sa.Integer(), nullable=True),
63
+    sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
64
+    sa.PrimaryKeyConstraint('id')
65
+    )
66
+    op.create_table('roles_users',
67
+    sa.Column('user_id', sa.Integer(), nullable=True),
68
+    sa.Column('role_id', sa.Integer(), nullable=True),
69
+    sa.ForeignKeyConstraint(['role_id'], ['role.id'], ),
70
+    sa.ForeignKeyConstraint(['user_id'], ['user.id'], )
71
+    )
72
+    op.create_table('user_logging',
73
+    sa.Column('id', sa.Integer(), nullable=False),
74
+    sa.Column('category', sa.String(length=255), nullable=False),
75
+    sa.Column('level', sa.String(length=255), nullable=False),
76
+    sa.Column('message', sa.Text(), nullable=False),
77
+    sa.Column('timestamp', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
78
+    sa.Column('user_id', sa.Integer(), nullable=False),
79
+    sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
80
+    sa.PrimaryKeyConstraint('id')
81
+    )
82
+
83
+
84
+def downgrade():
85
+    op.drop_table('user_logging')
86
+    op.drop_table('roles_users')
87
+    op.drop_table('logging')
88
+    op.drop_table('apitoken')
89
+    op.drop_table('user')
90
+    op.drop_table('role')
91
+    op.drop_table('config')

+ 49
- 0
migrations/versions/01_da3273ca0f0f_.py View File

@@ -0,0 +1,49 @@
1
+"""Add Sound and SoundInfo
2
+
3
+Revision ID: da3273ca0f0f
4
+Revises: 795db5a5e99b
5
+Create Date: 2016-12-31 14:43:11.818727
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = 'da3273ca0f0f'
11
+down_revision = '795db5a5e99b'
12
+
13
+from alembic import op
14
+import sqlalchemy as sa
15
+
16
+
17
+def upgrade():
18
+    ### commands auto generated by Alembic - please adjust! ###
19
+    op.create_table('sound',
20
+    sa.Column('id', sa.Integer(), nullable=False),
21
+    sa.Column('title', sa.String(length=255), nullable=True),
22
+    sa.Column('uploaded', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
23
+    sa.Column('description', sa.UnicodeText(), nullable=True),
24
+    sa.Column('public', sa.Boolean(), nullable=False),
25
+    sa.Column('slug', sa.String(length=255), nullable=True),
26
+    sa.Column('user_id', sa.Integer(), nullable=False),
27
+    sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
28
+    sa.PrimaryKeyConstraint('id'),
29
+    sa.UniqueConstraint('slug')
30
+    )
31
+    op.create_table('sound_info',
32
+    sa.Column('id', sa.Integer(), nullable=False),
33
+    sa.Column('duration', sa.Float(), nullable=True),
34
+    sa.Column('format', sa.String(length=255), nullable=True),
35
+    sa.Column('rate', sa.String(length=255), nullable=True),
36
+    sa.Column('channels', sa.Integer(), nullable=True),
37
+    sa.Column('codec', sa.String(length=255), nullable=True),
38
+    sa.Column('sound_id', sa.Integer(), nullable=False),
39
+    sa.ForeignKeyConstraint(['sound_id'], ['sound.id'], ),
40
+    sa.PrimaryKeyConstraint('id')
41
+    )
42
+    ### end Alembic commands ###
43
+
44
+
45
+def downgrade():
46
+    ### commands auto generated by Alembic - please adjust! ###
47
+    op.drop_table('sound_info')
48
+    op.drop_table('sound')
49
+    ### end Alembic commands ###

+ 152
- 0
models.py View File

@@ -0,0 +1,152 @@
1
+import datetime
2
+from libqth import is_valid_qth, qth_to_coords, coords_to_qth
3
+
4
+from flask_security import SQLAlchemyUserDatastore, UserMixin, RoleMixin
5
+from flask_sqlalchemy import SQLAlchemy, BaseQuery
6
+from geohelper import distance
7
+from sqlalchemy.sql import func
8
+from sqlalchemy_searchable import make_searchable, SearchQueryMixin
9
+from sqlalchemy import event
10
+from slugify import slugify
11
+from sqlalchemy_utils.types import TSVectorType
12
+
13
+db = SQLAlchemy()
14
+make_searchable()
15
+
16
+
17
+class LogQuery(BaseQuery, SearchQueryMixin):
18
+    pass
19
+
20
+
21
+roles_users = db.Table('roles_users',
22
+                       db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
23
+                       db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
24
+
25
+
26
+class Role(db.Model, RoleMixin):
27
+    id = db.Column(db.Integer(), primary_key=True)
28
+    name = db.Column(db.String(80), unique=True, nullable=False, info={'label': 'Name'})
29
+    description = db.Column(db.String(255), info={'label': 'Description'})
30
+
31
+    __mapper_args__ = {"order_by": name}
32
+
33
+
34
+class User(db.Model, UserMixin):
35
+    id = db.Column(db.Integer, primary_key=True)
36
+    email = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Email'})
37
+    name = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Name'})
38
+    password = db.Column(db.String(255), nullable=False, info={'label': 'Password'})
39
+    active = db.Column(db.Boolean())
40
+    confirmed_at = db.Column(db.DateTime())
41
+
42
+    firstname = db.Column(db.String(32))
43
+    lastname = db.Column(db.String(32))
44
+
45
+    timezone = db.Column(db.String(255), nullable=False, default='UTC')  # Managed and fed by pytz
46
+
47
+    slug = db.Column(db.String(255), unique=True, nullable=True)
48
+
49
+    roles = db.relationship('Role', secondary=roles_users, backref=db.backref('users', lazy='dynamic'))
50
+    apitokens = db.relationship('Apitoken', backref='user', lazy='dynamic', cascade="delete")
51
+
52
+    user_loggings = db.relationship('UserLogging', backref='user', lazy='dynamic', cascade="delete")
53
+    loggings = db.relationship('Logging', backref='user', lazy='dynamic', cascade="delete")
54
+
55
+    sounds = db.relationship('Sound', backref='user', lazy='dynamic', cascade="delete")
56
+
57
+    __mapper_args__ = {"order_by": name}
58
+
59
+    def join_roles(self, string):
60
+        return string.join([i.description for i in self.roles])
61
+
62
+
63
+class Apitoken(db.Model):
64
+    id = db.Column(db.Integer, primary_key=True)
65
+    user_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=False)
66
+    token = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Token'})
67
+    secret = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Secret'})
68
+
69
+
70
+user_datastore = SQLAlchemyUserDatastore(db, User, Role)
71
+
72
+
73
+class Config(db.Model):
74
+    __tablename__ = "config"
75
+
76
+    id = db.Column(db.Integer, primary_key=True)
77
+    app_name = db.Column(db.String(255), default=None)
78
+
79
+
80
+class Logging(db.Model):
81
+    __tablename__ = "logging"
82
+
83
+    id = db.Column(db.Integer, primary_key=True)
84
+    category = db.Column(db.String(255), nullable=False, default="General")
85
+    level = db.Column(db.String(255), nullable=False, default="INFO")
86
+    message = db.Column(db.Text, nullable=False)
87
+    timestamp = db.Column(db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now())
88
+
89
+    user_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=True)
90
+
91
+
92
+class UserLogging(db.Model):
93
+    __tablename__ = "user_logging"
94
+
95
+    id = db.Column(db.Integer, primary_key=True)
96
+    category = db.Column(db.String(255), nullable=False, default="General")
97
+    level = db.Column(db.String(255), nullable=False, default="INFO")
98
+    message = db.Column(db.Text, nullable=False)
99
+    timestamp = db.Column(db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now())
100
+
101
+    #log_id = db.Column(db.Integer(), db.ForeignKey('log.id'), nullable=True)
102
+    user_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=False)
103
+
104
+
105
+class SoundInfo(db.Model):
106
+    __tablename__ = "sound_info"
107
+
108
+    id = db.Column(db.Integer, primary_key=True)
109
+    duration = db.Column(db.Float, nullable=True)
110
+    format = db.Column(db.String(255), nullable=True)
111
+    rate = db.Column(db.String(255), nullable=True)
112
+    channels = db.Column(db.Integer, nullable=True)
113
+    codec = db.Column(db.String(255), nullable=True)
114
+
115
+    sound_id = db.Column(db.Integer(), db.ForeignKey('sound.id'), nullable=False)
116
+
117
+
118
+class Sound(db.Model):
119
+    __tablename__ = "sound"
120
+
121
+    id = db.Column(db.Integer, primary_key=True)
122
+    title = db.Column(db.String(255), nullable=True)
123
+    uploaded = db.Column(db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now())
124
+    # TODO genre
125
+    # TODO tags
126
+    # TODO picture ?
127
+    description = db.Column(db.UnicodeText(), nullable=True)
128
+    public = db.Column(db.Boolean(), default=True, nullable=False)
129
+    slug = db.Column(db.String(255), unique=True, nullable=True)
130
+
131
+    user_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=False)
132
+    sound_infos = db.relationship('SoundInfo', backref='sound_info', lazy='dynamic', cascade="delete")
133
+
134
+@event.listens_for(User, 'after_update')
135
+@event.listens_for(User, 'after_insert')
136
+def make_slug(mapper, connection, target):
137
+    title = "{0} {1}".format(target.id, target.name)
138
+    slug = slugify(title)
139
+    connection.execute(
140
+        User.__table__.update().where(User.__table__.c.id == target.id).values(slug=slug)
141
+    )
142
+
143
+
144
+@event.listens_for(Sound, 'after_update')
145
+@event.listens_for(Sound, 'after_insert')
146
+def make_slug(mapper, connection, target):
147
+    if not target.slug or target.slug == "":
148
+        title = "{0} {1}".format(target.id, target.title)
149
+        slug = slugify(title)
150
+        connection.execute(
151
+            User.__table__.update().where(User.__table__.c.id == target.id).values(slug=slug)
152
+        )

+ 78
- 0
reel2bits.py View File

@@ -0,0 +1,78 @@
1
+# encoding: utf-8
2
+from pprint import pprint as pp
3
+import datetime
4
+import os
5
+import texttable
6
+from flask_debugtoolbar import DebugToolbarExtension
7
+from flask_migrate import MigrateCommand
8
+from flask_script import Manager
9
+from dbseed import make_db_seed
10
+from models import db
11
+
12
+try:
13
+    from raven.contrib.flask import Sentry
14
+    import raven
15
+    print(" * Sentry support loaded")
16
+    HAS_SENTRY = True
17
+except ImportError as e:
18
+    print(" * No Sentry support")
19
+    HAS_SENTRY = False
20
+
21
+from app import create_app
22
+
23
+app = create_app()
24
+
25
+if HAS_SENTRY:
26
+    app.config['SENTRY_RELEASE'] = raven.fetch_git_sha(os.path.dirname(__file__))
27
+    sentry = Sentry(app, dsn=app.config['SENTRY_DSN'])
28
+    print(" * Sentry support activated")
29
+    print(" * Sentry DSN: %s" % app.config['SENTRY_DSN'])
30
+
31
+toolbar = DebugToolbarExtension(app)
32
+manager = Manager(app)
33
+
34
+
35
+# Other commands
36
+@manager.command
37
+def routes():
38
+    """Dump all routes of defined app"""
39
+    table = texttable.Texttable()
40
+    table.set_deco(texttable.Texttable().HEADER)
41
+    table.set_cols_dtype(['t', 't', 't'])
42
+    table.set_cols_align(["l", "l", "l"])
43
+    table.set_cols_width([60, 30, 90])
44
+
45
+    table.add_rows([["Prefix", "Verb", "URI Pattern"]])
46
+
47
+    for rule in sorted(app.url_map.iter_rules(), key=lambda x: str(x)):
48
+        methods = ','.join(rule.methods)
49
+        table.add_row([rule.endpoint, methods, rule])
50
+
51
+    print(table.draw())
52
+
53
+
54
+@manager.command
55
+def config():
56
+    """Dump config"""
57
+    pp(app.config)
58
+
59
+
60
+@MigrateCommand.command
61
+def seed():
62
+    """Seed database with default content"""
63
+    make_db_seed(db)
64
+
65
+CacheCommand = Manager(usage='Perform cache actions')
66
+CronCommand = Manager(usage='Perform crons actions')
67
+
68
+
69
+manager.add_command('db', MigrateCommand)
70
+manager.add_command('cache', CacheCommand)
71
+manager.add_command('cron', CronCommand)
72
+
73
+if __name__ == '__main__':
74
+    try:
75
+        manager.run()
76
+    except KeyboardInterrupt as e:
77
+        print("Got KeyboardInterrupt, halting...")
78
+        print(e)

+ 55
- 0
requirements.txt View File

@@ -0,0 +1,55 @@
1
+alembic==0.8.6
2
+bcrypt==2.0.0
3
+blinker==1.4
4
+cffi==1.6.0
5
+click==6.6
6
+dateutils==0.6.6
7
+decorator==4.0.10
8
+dominate==2.2.0
9
+Flask==0.10.1
10
+Flask-Bootstrap==3.3.6.0
11
+Flask-DebugToolbar==0.10.0
12
+Flask-Login==0.3.2
13
+Flask-Mail==0.9.1
14
+Flask-Migrate==1.8.0
15
+Flask-Principal==0.4.0
16
+Flask-Script==2.0.5
17
+Flask-Security==1.7.5
18
+Flask-SQLAlchemy==2.1
19
+Flask-WTF==0.12
20
+geohelper==0.2.0
21
+gunicorn==19.6.0
22
+infinity==1.4
23
+intervals==0.7.1
24
+itsdangerous==0.24
25
+Jinja2==2.8
26
+Mako==1.0.4
27
+MarkupSafe==0.23
28
+parsedatetime==2.1
29
+passlib==1.6.5
30
+pycparser==2.14
31
+pyparsing==2.1.4
32
+python-dateutil==2.5.3
33
+python-editor==1.0
34
+pytz==2016.6.1
35
+requests==2.10.0
36
+six==1.10.0
37
+SQLAlchemy==1.0.13
38
+SQLAlchemy-Searchable==0.10.1
39
+SQLAlchemy-Utils==0.32.7
40
+texttable==0.8.4
41
+Unidecode==0.4.19
42
+validators==0.10.1
43
+virtualenv==1.11.6
44
+visitor==0.1.3
45
+Werkzeug==0.11.10
46
+WTForms==2.1
47
+WTForms-Alchemy==0.16.1
48
+WTForms-Components==0.10.0
49
+mysqlclient==1.3.7
50
+beautifulsoup4==4.4.1
51
+flask-uploads
52
+python-slugify
53
+LatLon
54
+git+http://dev.sigpipe.me/DashieHam/pyHamQth.git#egg=pyhamqth
55
+git+https://github.com/ggramaize/libqth.git#egg=libqth

+ 587
- 0
static/css/bootstrap-theme.css View File

@@ -0,0 +1,587 @@
1
+/*!
2
+ * Bootstrap v3.3.6 (http://getbootstrap.com)
3
+ * Copyright 2011-2015 Twitter, Inc.
4
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5
+ */
6
+.btn-default,
7
+.btn-primary,
8
+.btn-success,
9
+.btn-info,
10
+.btn-warning,
11
+.btn-danger {
12
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
13
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
14
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
15
+}
16
+.btn-default:active,
17
+.btn-primary:active,
18
+.btn-success:active,
19
+.btn-info:active,
20
+.btn-warning:active,
21
+.btn-danger:active,
22
+.btn-default.active,
23
+.btn-primary.active,
24
+.btn-success.active,
25
+.btn-info.active,
26
+.btn-warning.active,
27
+.btn-danger.active {
28
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
29
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
30
+}
31
+.btn-default.disabled,
32
+.btn-primary.disabled,
33
+.btn-success.disabled,
34
+.btn-info.disabled,
35
+.btn-warning.disabled,
36
+.btn-danger.disabled,
37
+.btn-default[disabled],
38
+.btn-primary[disabled],
39
+.btn-success[disabled],
40
+.btn-info[disabled],
41
+.btn-warning[disabled],
42
+.btn-danger[disabled],
43
+fieldset[disabled] .btn-default,
44
+fieldset[disabled] .btn-primary,
45
+fieldset[disabled] .btn-success,
46
+fieldset[disabled] .btn-info,
47
+fieldset[disabled] .btn-warning,
48
+fieldset[disabled] .btn-danger {
49
+  -webkit-box-shadow: none;
50
+          box-shadow: none;
51
+}
52
+.btn-default .badge,
53
+.btn-primary .badge,
54
+.btn-success .badge,
55
+.btn-info .badge,
56
+.btn-warning .badge,
57
+.btn-danger .badge {
58
+  text-shadow: none;
59
+}
60
+.btn:active,
61
+.btn.active {
62
+  background-image: none;
63
+}
64
+.btn-default {
65
+  text-shadow: 0 1px 0 #fff;
66
+  background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
67
+  background-image:      -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
68
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
69
+  background-image:         linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
70
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
71
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
72
+  background-repeat: repeat-x;
73
+  border-color: #dbdbdb;
74
+  border-color: #ccc;
75
+}
76
+.btn-default:hover,
77
+.btn-default:focus {
78
+  background-color: #e0e0e0;
79
+  background-position: 0 -15px;
80
+}
81
+.btn-default:active,
82
+.btn-default.active {
83
+  background-color: #e0e0e0;
84
+  border-color: #dbdbdb;
85
+}
86
+.btn-default.disabled,
87
+.btn-default[disabled],
88
+fieldset[disabled] .btn-default,
89
+.btn-default.disabled:hover,
90
+.btn-default[disabled]:hover,
91
+fieldset[disabled] .btn-default:hover,
92
+.btn-default.disabled:focus,
93
+.btn-default[disabled]:focus,
94
+fieldset[disabled] .btn-default:focus,
95
+.btn-default.disabled.focus,
96
+.btn-default[disabled].focus,
97
+fieldset[disabled] .btn-default.focus,
98
+.btn-default.disabled:active,
99
+.btn-default[disabled]:active,
100
+fieldset[disabled] .btn-default:active,
101
+.btn-default.disabled.active,
102
+.btn-default[disabled].active,
103
+fieldset[disabled] .btn-default.active {
104
+  background-color: #e0e0e0;
105
+  background-image: none;
106
+}
107
+.btn-primary {
108
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);
109
+  background-image:      -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
110
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));
111
+  background-image:         linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
112
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);
113
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
114
+  background-repeat: repeat-x;
115
+  border-color: #245580;
116
+}
117
+.btn-primary:hover,
118
+.btn-primary:focus {
119
+  background-color: #265a88;
120
+  background-position: 0 -15px;
121
+}
122
+.btn-primary:active,
123
+.btn-primary.active {
124
+  background-color: #265a88;
125
+  border-color: #245580;
126
+}
127
+.btn-primary.disabled,
128
+.btn-primary[disabled],
129
+fieldset[disabled] .btn-primary,
130
+.btn-primary.disabled:hover,
131
+.btn-primary[disabled]:hover,
132
+fieldset[disabled] .btn-primary:hover,
133
+.btn-primary.disabled:focus,
134
+.btn-primary[disabled]:focus,
135
+fieldset[disabled] .btn-primary:focus,
136
+.btn-primary.disabled.focus,
137
+.btn-primary[disabled].focus,
138
+fieldset[disabled] .btn-primary.focus,
139
+.btn-primary.disabled:active,
140
+.btn-primary[disabled]:active,
141
+fieldset[disabled] .btn-primary:active,
142
+.btn-primary.disabled.active,
143
+.btn-primary[disabled].active,
144
+fieldset[disabled] .btn-primary.active {
145
+  background-color: #265a88;
146
+  background-image: none;
147
+}
148
+.btn-success {
149
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
150
+  background-image:      -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
151
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
152
+  background-image:         linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
153
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
154
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
155
+  background-repeat: repeat-x;
156
+  border-color: #3e8f3e;
157
+}
158
+.btn-success:hover,
159
+.btn-success:focus {
160
+  background-color: #419641;
161
+  background-position: 0 -15px;
162
+}
163
+.btn-success:active,
164
+.btn-success.active {
165
+  background-color: #419641;
166
+  border-color: #3e8f3e;
167
+}
168
+.btn-success.disabled,
169
+.btn-success[disabled],
170
+fieldset[disabled] .btn-success,
171
+.btn-success.disabled:hover,
172
+.btn-success[disabled]:hover,
173
+fieldset[disabled] .btn-success:hover,
174
+.btn-success.disabled:focus,
175
+.btn-success[disabled]:focus,
176
+fieldset[disabled] .btn-success:focus,
177
+.btn-success.disabled.focus,
178
+.btn-success[disabled].focus,
179
+fieldset[disabled] .btn-success.focus,
180
+.btn-success.disabled:active,
181
+.btn-success[disabled]:active,
182
+fieldset[disabled] .btn-success:active,
183
+.btn-success.disabled.active,
184
+.btn-success[disabled].active,
185
+fieldset[disabled] .btn-success.active {
186
+  background-color: #419641;
187
+  background-image: none;
188
+}
189
+.btn-info {
190
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
191
+  background-image:      -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
192
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
193
+  background-image:         linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
194
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
195
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
196
+  background-repeat: repeat-x;
197
+  border-color: #28a4c9;
198
+}
199
+.btn-info:hover,
200
+.btn-info:focus {
201
+  background-color: #2aabd2;
202
+  background-position: 0 -15px;
203
+}
204
+.btn-info:active,
205
+.btn-info.active {
206
+  background-color: #2aabd2;
207
+  border-color: #28a4c9;
208
+}
209
+.btn-info.disabled,
210
+.btn-info[disabled],
211
+fieldset[disabled] .btn-info,
212
+.btn-info.disabled:hover,
213
+.btn-info[disabled]:hover,
214
+fieldset[disabled] .btn-info:hover,
215
+.btn-info.disabled:focus,
216
+.btn-info[disabled]:focus,
217
+fieldset[disabled] .btn-info:focus,
218
+.btn-info.disabled.focus,
219
+.btn-info[disabled].focus,
220
+fieldset[disabled] .btn-info.focus,
221
+.btn-info.disabled:active,
222
+.btn-info[disabled]:active,
223
+fieldset[disabled] .btn-info:active,
224
+.btn-info.disabled.active,
225
+.btn-info[disabled].active,
226
+fieldset[disabled] .btn-info.active {
227
+  background-color: #2aabd2;
228
+  background-image: none;
229
+}
230
+.btn-warning {
231
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
232
+  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
233
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
234
+  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
235
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
236
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
237
+  background-repeat: repeat-x;
238
+  border-color: #e38d13;
239
+}
240
+.btn-warning:hover,
241
+.btn-warning:focus {
242
+  background-color: #eb9316;
243
+  background-position: 0 -15px;
244
+}
245
+.btn-warning:active,
246
+.btn-warning.active {
247
+  background-color: #eb9316;
248
+  border-color: #e38d13;
249
+}
250
+.btn-warning.disabled,
251
+.btn-warning[disabled],
252
+fieldset[disabled] .btn-warning,
253
+.btn-warning.disabled:hover,
254
+.btn-warning[disabled]:hover,
255
+fieldset[disabled] .btn-warning:hover,
256
+.btn-warning.disabled:focus,
257
+.btn-warning[disabled]:focus,
258
+fieldset[disabled] .btn-warning:focus,
259
+.btn-warning.disabled.focus,
260
+.btn-warning[disabled].focus,
261
+fieldset[disabled] .btn-warning.focus,
262
+.btn-warning.disabled:active,
263
+.btn-warning[disabled]:active,
264
+fieldset[disabled] .btn-warning:active,
265
+.btn-warning.disabled.active,
266
+.btn-warning[disabled].active,
267
+fieldset[disabled] .btn-warning.active {
268
+  background-color: #eb9316;
269
+  background-image: none;
270
+}
271
+.btn-danger {
272
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
273
+  background-image:      -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
274
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
275
+  background-image:         linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
276
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
277
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
278
+  background-repeat: repeat-x;
279
+  border-color: #b92c28;
280
+}
281
+.btn-danger:hover,
282
+.btn-danger:focus {
283
+  background-color: #c12e2a;
284
+  background-position: 0 -15px;
285
+}
286
+.btn-danger:active,
287
+.btn-danger.active {
288
+  background-color: #c12e2a;
289
+  border-color: #b92c28;
290
+}
291
+.btn-danger.disabled,
292
+.btn-danger[disabled],
293
+fieldset[disabled] .btn-danger,
294
+.btn-danger.disabled:hover,
295
+.btn-danger[disabled]:hover,
296
+fieldset[disabled] .btn-danger:hover,
297
+.btn-danger.disabled:focus,
298
+.btn-danger[disabled]:focus,
299
+fieldset[disabled] .btn-danger:focus,
300
+.btn-danger.disabled.focus,
301
+.btn-danger[disabled].focus,
302
+fieldset[disabled] .btn-danger.focus,
303
+.btn-danger.disabled:active,
304
+.btn-danger[disabled]:active,
305
+fieldset[disabled] .btn-danger:active,
306
+.btn-danger.disabled.active,
307
+.btn-danger[disabled].active,
308
+fieldset[disabled] .btn-danger.active {
309
+  background-color: #c12e2a;
310
+  background-image: none;
311
+}
312
+.thumbnail,
313
+.img-thumbnail {
314
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
315
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
316
+}
317
+.dropdown-menu > li > a:hover,
318
+.dropdown-menu > li > a:focus {
319
+  background-color: #e8e8e8;
320
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
321
+  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
322
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
323
+  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
324
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
325
+  background-repeat: repeat-x;
326
+}
327
+.dropdown-menu > .active > a,
328
+.dropdown-menu > .active > a:hover,
329
+.dropdown-menu > .active > a:focus {
330
+  background-color: #2e6da4;
331
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
332
+  background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
333
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
334
+  background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
335
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
336
+  background-repeat: repeat-x;
337
+}
338
+.navbar-default {
339
+  background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
340
+  background-image:      -o-linear-gradient(top, #fff 0%, #f8f8f8 100%);
341
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8));
342
+  background-image:         linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
343
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
344
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
345
+  background-repeat: repeat-x;
346
+  border-radius: 4px;
347
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
348
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
349
+}
350
+.navbar-default .navbar-nav > .open > a,
351
+.navbar-default .navbar-nav > .active > a {
352
+  background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
353
+  background-image:      -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
354
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));
355
+  background-image:         linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
356
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);
357
+  background-repeat: repeat-x;
358
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
359
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
360
+}
361
+.navbar-brand,
362
+.navbar-nav > li > a {
363
+  text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
364
+}
365
+.navbar-inverse {
366
+  background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
367
+  background-image:      -o-linear-gradient(top, #3c3c3c 0%, #222 100%);
368
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));
369
+  background-image:         linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
370
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
371
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
372
+  background-repeat: repeat-x;
373
+  border-radius: 4px;
374
+}
375
+.navbar-inverse .navbar-nav > .open > a,
376
+.navbar-inverse .navbar-nav > .active > a {
377
+  background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);
378
+  background-image:      -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);
379
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));
380
+  background-image:         linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);
381
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);
382
+  background-repeat: repeat-x;
383
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
384
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
385
+}
386
+.navbar-inverse .navbar-brand,
387
+.navbar-inverse .navbar-nav > li > a {
388
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
389
+}
390
+.navbar-static-top,
391
+.navbar-fixed-top,
392
+.navbar-fixed-bottom {
393
+  border-radius: 0;
394
+}
395
+@media (max-width: 767px) {
396
+  .navbar .navbar-nav .open .dropdown-menu > .active > a,
397
+  .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,
398
+  .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {
399
+    color: #fff;
400
+    background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
401
+    background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
402
+    background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
403
+    background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
404
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
405
+    background-repeat: repeat-x;
406
+  }
407
+}
408
+.alert {
409
+  text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
410
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
411
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
412
+}
413
+.alert-success {
414
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
415
+  background-image:      -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
416
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));
417
+  background-image:         linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
418
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
419
+  background-repeat: repeat-x;
420
+  border-color: #b2dba1;
421
+}
422
+.alert-info {
423
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
424
+  background-image:      -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
425
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));
426
+  background-image:         linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
427
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
428
+  background-repeat: repeat-x;
429
+  border-color: #9acfea;
430
+}
431
+.alert-warning {
432
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
433
+  background-image:      -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
434
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));
435
+  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
436
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
437
+  background-repeat: repeat-x;
438
+  border-color: #f5e79e;
439
+}
440
+.alert-danger {
441
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
442
+  background-image:      -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
443
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));
444
+  background-image:         linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
445
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
446
+  background-repeat: repeat-x;
447
+  border-color: #dca7a7;
448
+}
449
+.progress {
450
+  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
451
+  background-image:      -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
452
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));
453
+  background-image:         linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
454
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
455
+  background-repeat: repeat-x;
456
+}
457
+.progress-bar {
458
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);
459
+  background-image:      -o-linear-gradient(top, #337ab7 0%, #286090 100%);
460
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090));
461
+  background-image:         linear-gradient(to bottom, #337ab7 0%, #286090 100%);
462
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);
463
+  background-repeat: repeat-x;
464
+}
465
+.progress-bar-success {
466
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
467
+  background-image:      -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);
468
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));
469
+  background-image:         linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
470
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
471
+  background-repeat: repeat-x;
472
+}
473
+.progress-bar-info {
474
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
475
+  background-image:      -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
476
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));
477
+  background-image:         linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
478
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
479
+  background-repeat: repeat-x;
480
+}
481
+.progress-bar-warning {
482
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
483
+  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
484
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));
485
+  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
486
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
487
+  background-repeat: repeat-x;
488
+}
489
+.progress-bar-danger {
490
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
491
+  background-image:      -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);
492
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));
493
+  background-image:         linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
494
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
495
+  background-repeat: repeat-x;
496
+}
497
+.progress-bar-striped {
498
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
499
+  background-image:      -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
500
+  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
501
+}
502
+.list-group {
503
+  border-radius: 4px;
504
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
505
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
506
+}
507
+.list-group-item.active,
508
+.list-group-item.active:hover,
509
+.list-group-item.active:focus {
510
+  text-shadow: 0 -1px 0 #286090;
511
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);
512
+  background-image:      -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);
513
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a));
514
+  background-image:         linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);
515
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);
516
+  background-repeat: repeat-x;
517
+  border-color: #2b669a;
518
+}
519
+.list-group-item.active .badge,
520
+.list-group-item.active:hover .badge,
521
+.list-group-item.active:focus .badge {
522
+  text-shadow: none;
523
+}
524
+.panel {
525
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
526
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
527
+}
528
+.panel-default > .panel-heading {
529
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
530
+  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
531
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
532
+  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
533
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
534
+  background-repeat: repeat-x;
535
+}
536
+.panel-primary > .panel-heading {
537
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
538
+  background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
539
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
540
+  background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
541
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
542
+  background-repeat: repeat-x;
543
+}
544
+.panel-success > .panel-heading {
545
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
546
+  background-image:      -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
547
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));
548
+  background-image:         linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
549
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
550
+  background-repeat: repeat-x;
551
+}
552
+.panel-info > .panel-heading {
553
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
554
+  background-image:      -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
555
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));
556
+  background-image:         linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
557
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
558
+  background-repeat: repeat-x;
559
+}
560
+.panel-warning > .panel-heading {
561
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
562
+  background-image:      -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
563
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));
564
+  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
565
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
566
+  background-repeat: repeat-x;
567
+}
568
+.panel-danger > .panel-heading {
569
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
570
+  background-image:      -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
571
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));
572
+  background-image:         linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
573
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
574
+  background-repeat: repeat-x;
575
+}
576
+.well {
577
+  background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
578
+  background-image:      -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
579
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));
580
+  background-image:         linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
581
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
582
+  background-repeat: repeat-x;
583
+  border-color: #dcdcdc;
584
+  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
585
+          box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
586
+}
587
+/*# sourceMappingURL=bootstrap-theme.css.map */

+ 1
- 0
static/css/bootstrap-theme.css.map
File diff suppressed because it is too large
View File


+ 6
- 0
static/css/bootstrap-theme.min.css
File diff suppressed because it is too large
View File


+ 1
- 0
static/css/bootstrap-theme.min.css.map
File diff suppressed because it is too large
View File


+ 6760
- 0
static/css/bootstrap.css
File diff suppressed because it is too large
View File


+ 1
- 0
static/css/bootstrap.css.map
File diff suppressed because it is too large
View File


+ 6
- 0
static/css/bootstrap.min.css
File diff suppressed because it is too large
View File


+ 1
- 0
static/css/bootstrap.min.css.map
File diff suppressed because it is too large
View File


BIN
static/css/chosen-sprite.png View File


BIN
static/css/chosen-sprite@2x.png View File


+ 450
- 0
static/css/chosen.css View File

@@ -0,0 +1,450 @@
1
+/*!
2
+Chosen, a Select Box Enhancer for jQuery and Prototype
3
+by Patrick Filler for Harvest, http://getharvest.com
4
+
5
+Version 1.4.2
6
+Full source at https://github.com/harvesthq/chosen
7
+Copyright (c) 2011-2015 Harvest http://getharvest.com
8
+
9
+MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md
10
+This file is generated by `grunt build`, do not edit it by hand.
11
+*/
12
+
13
+/* @group Base */
14
+.chosen-container {
15
+  position: relative;
16
+  display: inline-block;
17
+  vertical-align: middle;
18
+  font-size: 13px;
19
+  zoom: 1;
20
+  *display: inline;
21
+  -webkit-user-select: none;
22
+  -moz-user-select: none;
23
+  user-select: none;
24
+}
25
+.chosen-container * {
26
+  -webkit-box-sizing: border-box;
27
+  -moz-box-sizing: border-box;
28
+  box-sizing: border-box;
29
+}
30
+.chosen-container .chosen-drop {
31
+  position: absolute;
32
+  top: 100%;
33
+  left: -9999px;
34
+  z-index: 1010;
35
+  width: 100%;
36
+  border: 1px solid #aaa;
37
+  border-top: 0;
38
+  background: #fff;
39
+  box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15);
40
+}
41
+.chosen-container.chosen-with-drop .chosen-drop {
42
+  left: 0;
43
+}
44
+.chosen-container a {
45
+  cursor: pointer;
46
+}
47
+.chosen-container .search-choice .group-name, .chosen-container .chosen-single .group-name {
48
+  margin-right: 4px;
49
+  overflow: hidden;
50
+  white-space: nowrap;
51
+  text-overflow: ellipsis;
52
+  font-weight: normal;
53
+  color: #999999;
54
+}
55
+.chosen-container .search-choice .group-name:after, .chosen-container .chosen-single .group-name:after {
56
+  content: ":";
57
+  padding-left: 2px;
58
+  vertical-align: top;
59
+}
60
+
61
+/* @end */
62
+/* @group Single Chosen */
63
+.chosen-container-single .chosen-single {
64
+  position: relative;
65
+  display: block;
66
+  overflow: hidden;
67
+  padding: 0 0 0 8px;
68
+  height: 25px;
69
+  border: 1px solid #aaa;
70
+  border-radius: 5px;
71
+  background-color: #fff;
72
+  background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #ffffff), color-stop(50%, #f6f6f6), color-stop(52%, #eeeeee), color-stop(100%, #f4f4f4));
73
+  background: -webkit-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
74
+  background: -moz-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
75
+  background: -o-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
76
+  background: linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
77
+  background-clip: padding-box;
78
+  box-shadow: 0 0 3px white inset, 0 1px 1px rgba(0, 0, 0, 0.1);
79
+  color: #444;
80
+  text-decoration: none;
81
+  white-space: nowrap;
82
+  line-height: 24px;
83
+}
84
+.chosen-container-single .chosen-default {
85
+  color: #999;
86
+}
87
+.chosen-container-single .chosen-single span {
88
+  display: block;
89
+  overflow: hidden;
90
+  margin-right: 26px;
91
+  text-overflow: ellipsis;
92
+  white-space: nowrap;
93
+}
94
+.chosen-container-single .chosen-single-with-deselect span {
95
+  margin-right: 38px;
96
+}
97
+.chosen-container-single .chosen-single abbr {
98
+  position: absolute;
99
+  top: 6px;
100
+  right: 26px;
101
+  display: block;
102
+  width: 12px;
103
+  height: 12px;
104
+  background: url('chosen-sprite.png') -42px 1px no-repeat;
105
+  font-size: 1px;
106
+}
107
+.chosen-container-single .chosen-single abbr:hover {
108
+  background-position: -42px -10px;
109
+}
110
+.chosen-container-single.chosen-disabled .chosen-single abbr:hover {
111
+  background-position: -42px -10px;
112
+}
113
+.chosen-container-single .chosen-single div {
114
+  position: absolute;
115
+  top: 0;
116
+  right: 0;
117
+  display: block;
118
+  width: 18px;
119
+  height: 100%;
120
+}
121
+.chosen-container-single .chosen-single div b {
122
+  display: block;
123
+  width: 100%;
124
+  height: 100%;
125
+  background: url('chosen-sprite.png') no-repeat 0px 2px;
126
+}
127
+.chosen-container-single .chosen-search {
128
+  position: relative;
129
+  z-index: 1010;
130
+  margin: 0;
131
+  padding: 3px 4px;
132
+  white-space: nowrap;
133
+}
134
+.chosen-container-single .chosen-search input[type="text"] {
135
+  margin: 1px 0;
136
+  padding: 4px 20px 4px 5px;
137
+  width: 100%;
138
+  height: auto;
139
+  outline: 0;
140
+  border: 1px solid #aaa;
141
+  background: white url('chosen-sprite.png') no-repeat 100% -20px;
142
+  background: url('chosen-sprite.png') no-repeat 100% -20px;
143
+  font-size: 1em;
144
+  font-family: sans-serif;
145
+  line-height: normal;
146
+  border-radius: 0;
147
+}
148
+.chosen-container-single .chosen-drop {
149
+  margin-top: -1px;
150
+  border-radius: 0 0 4px 4px;
151
+  background-clip: padding-box;
152
+}
153
+.chosen-container-single.chosen-container-single-nosearch .chosen-search {
154
+  position: absolute;
155
+  left: -9999px;
156
+}
157
+
158
+/* @end */
159
+/* @group Results */
160
+.chosen-container .chosen-results {
161
+  color: #444;
162
+  position: relative;
163
+  overflow-x: hidden;
164
+  overflow-y: auto;
165
+  margin: 0 4px 4px 0;
166
+  padding: 0 0 0 4px;
167
+  max-height: 240px;
168
+  -webkit-overflow-scrolling: touch;
169
+}
170
+.chosen-container .chosen-results li {
171
+  display: none;
172
+  margin: 0;
173
+  padding: 5px 6px;
174
+  list-style: none;
175
+  line-height: 15px;
176
+  word-wrap: break-word;
177
+  -webkit-touch-callout: none;
178
+}
179
+.chosen-container .chosen-results li.active-result {
180
+  display: list-item;
181
+  cursor: pointer;
182
+}
183
+.chosen-container .chosen-results li.disabled-result {
184
+  display: list-item;
185
+  color: #ccc;
186
+  cursor: default;
187
+}
188
+.chosen-container .chosen-results li.highlighted {
189
+  background-color: #3875d7;
190
+  background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #3875d7), color-stop(90%, #2a62bc));
191
+  background-image: -webkit-linear-gradient(#3875d7 20%, #2a62bc 90%);
192
+  background-image: -moz-linear-gradient(#3875d7 20%, #2a62bc 90%);
193
+  background-image: -o-linear-gradient(#3875d7 20%, #2a62bc 90%);
194
+  background-image: linear-gradient(#3875d7 20%, #2a62bc 90%);
195
+  color: #fff;
196
+}
197
+.chosen-container .chosen-results li.no-results {
198
+  color: #777;
199
+  display: list-item;
200
+  background: #f4f4f4;
201
+}
202
+.chosen-container .chosen-results li.group-result {
203
+  display: list-item;
204
+  font-weight: bold;
205
+  cursor: default;
206
+}
207
+.chosen-container .chosen-results li.group-option {
208
+  padding-left: 15px;
209
+}
210
+.chosen-container .chosen-results li em {
211
+  font-style: normal;
212
+  text-decoration: underline;
213
+}
214
+
215
+/* @end */
216
+/* @group Multi Chosen */
217
+.chosen-container-multi .chosen-choices {
218
+  position: relative;
219
+  overflow: hidden;
220
+  margin: 0;
221
+  padding: 0 5px;
222
+  width: 100%;
223
+  height: auto !important;
224
+  height: 1%;
225
+  border: 1px solid #aaa;
226
+  background-color: #fff;
227
+  background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
228
+  background-image: -webkit-linear-gradient(#eeeeee 1%, #ffffff 15%);
229
+  background-image: -moz-linear-gradient(#eeeeee 1%, #ffffff 15%);
230
+  background-image: -o-linear-gradient(#eeeeee 1%, #ffffff 15%);
231
+  background-image: linear-gradient(#eeeeee 1%, #ffffff 15%);
232
+  cursor: text;
233
+}
234
+.chosen-container-multi .chosen-choices li {
235
+  float: left;
236
+  list-style: none;
237
+}
238
+.chosen-container-multi .chosen-choices li.search-field {
239
+  margin: 0;
240
+  padding: 0;
241
+  white-space: nowrap;
242
+}
243
+.chosen-container-multi .chosen-choices li.search-field input[type="text"] {
244
+  margin: 1px 0;
245
+  padding: 0;
246
+  height: 25px;
247
+  outline: 0;
248
+  border: 0 !important;
249
+  background: transparent !important;
250
+  box-shadow: none;
251
+  color: #999;
252
+  font-size: 100%;
253
+  font-family: sans-serif;
254
+  line-height: normal;
255
+  border-radius: 0;
256
+}
257
+.chosen-container-multi .chosen-choices li.search-choice {
258
+  position: relative;
259
+  margin: 3px 5px 3px 0;
260
+  padding: 3px 20px 3px 5px;
261
+  border: 1px solid #aaa;
262
+  max-width: 100%;
263
+  border-radius: 3px;
264
+  background-color: #eeeeee;
265
+  background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
266
+  background-image: -webkit-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
267
+  background-image: -moz-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
268
+  background-image: -o-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
269
+  background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
270
+  background-size: 100% 19px;
271
+  background-repeat: repeat-x;
272
+  background-clip: padding-box;
273
+  box-shadow: 0 0 2px white inset, 0 1px 0 rgba(0, 0, 0, 0.05);
274
+  color: #333;
275
+  line-height: 13px;
276
+  cursor: default;
277
+}
278
+.chosen-container-multi .chosen-choices li.search-choice span {
279
+  word-wrap: break-word;
280
+}
281
+.chosen-container-multi .chosen-choices li.search-choice .search-choice-close {
282
+  position: absolute;
283
+  top: 4px;
284
+  right: 3px;
285
+  display: block;
286
+  width: 12px;
287
+  height: 12px;
288
+  background: url('chosen-sprite.png') -42px 1px no-repeat;
289
+  font-size: 1px;
290
+}
291
+.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover {
292
+  background-position: -42px -10px;
293
+}
294
+.chosen-container-multi .chosen-choices li.search-choice-disabled {
295
+  padding-right: 5px;
296
+  border: 1px solid #ccc;
297
+  background-color: #e4e4e4;
298
+  background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
299
+  background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
300
+  background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
301
+  background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
302
+  background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
303
+  color: #666;
304
+}
305
+.chosen-container-multi .chosen-choices li.search-choice-focus {
306
+  background: #d4d4d4;
307
+}
308
+.chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close {
309
+  background-position: -42px -10px;
310
+}
311
+.chosen-container-multi .chosen-results {
312
+  margin: 0;
313
+  padding: 0;
314
+}
315
+.chosen-container-multi .chosen-drop .result-selected {
316
+  display: list-item;
317
+  color: #ccc;
318
+  cursor: default;
319
+}
320
+
321
+/* @end */
322
+/* @group Active  */
323
+.chosen-container-active .chosen-single {
324
+  border: 1px solid #5897fb;
325
+  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
326
+}
327
+.chosen-container-active.chosen-with-drop .chosen-single {
328
+  border: 1px solid #aaa;
329
+  -moz-border-radius-bottomright: 0;
330
+  border-bottom-right-radius: 0;
331
+  -moz-border-radius-bottomleft: 0;
332
+  border-bottom-left-radius: 0;
333
+  background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #eeeeee), color-stop(80%, #ffffff));
334
+  background-image: -webkit-linear-gradient(#eeeeee 20%, #ffffff 80%);
335
+  background-image: -moz-linear-gradient(#eeeeee 20%, #ffffff 80%);
336
+  background-image: -o-linear-gradient(#eeeeee 20%, #ffffff 80%);
337
+  background-image: linear-gradient(#eeeeee 20%, #ffffff 80%);
338
+  box-shadow: 0 1px 0 #fff inset;
339
+}
340
+.chosen-container-active.chosen-with-drop .chosen-single div {
341
+  border-left: none;
342
+  background: transparent;
343
+}
344
+.chosen-container-active.chosen-with-drop .chosen-single div b {
345
+  background-position: -18px 2px;
346
+}
347
+.chosen-container-active .chosen-choices {
348
+  border: 1px solid #5897fb;
349
+  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
350
+}
351
+.chosen-container-active .chosen-choices li.search-field input[type="text"] {
352
+  color: #222 !important;
353
+}
354
+
355
+/* @end */
356
+/* @group Disabled Support */
357
+.chosen-disabled {
358
+  opacity: 0.5 !important;
359
+  cursor: default;
360
+}
361
+.chosen-disabled .chosen-single {
362
+  cursor: default;
363
+}
364
+.chosen-disabled .chosen-choices .search-choice .search-choice-close {
365
+  cursor: default;
366
+}
367
+
368
+/* @end */
369
+/* @group Right to Left */
370
+.chosen-rtl {
371
+  text-align: right;
372
+}
373
+.chosen-rtl .chosen-single {
374
+  overflow: visible;
375
+  padding: 0 8px 0 0;
376
+}
377
+.chosen-rtl .chosen-single span {
378
+  margin-right: 0;
379
+  margin-left: 26px;
380
+  direction: rtl;
381
+}
382
+.chosen-rtl .chosen-single-with-deselect span {
383
+  margin-left: 38px;
384
+}
385
+.chosen-rtl .chosen-single div {
386
+  right: auto;
387
+  left: 3px;
388
+}
389
+.chosen-rtl .chosen-single abbr {
390
+  right: auto;
391
+  left: 26px;
392
+}
393
+.chosen-rtl .chosen-choices li {
394
+  float: right;
395
+}
396
+.chosen-rtl .chosen-choices li.search-field input[type="text"] {
397
+  direction: rtl;
398
+}
399
+.chosen-rtl .chosen-choices li.search-choice {
400
+  margin: 3px 5px 3px 0;
401
+  padding: 3px 5px 3px 19px;
402
+}
403
+.chosen-rtl .chosen-choices li.search-choice .search-choice-close {
404
+  right: auto;
405
+  left: 4px;
406
+}
407
+.chosen-rtl.chosen-container-single-nosearch .chosen-search,
408
+.chosen-rtl .chosen-drop {
409
+  left: 9999px;
410
+}
411
+.chosen-rtl.chosen-container-single .chosen-results {
412
+  margin: 0 0 4px 4px;
413
+  padding: 0 4px 0 0;
414
+}
415
+.chosen-rtl .chosen-results li.group-option {
416
+  padding-right: 15px;
417
+  padding-left: 0;
418
+}
419
+.chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div {
420
+  border-right: none;
421
+}
422
+.chosen-rtl .chosen-search input[type="text"] {
423
+  padding: 4px 5px 4px 20px;
424
+  background: white url('chosen-sprite.png') no-repeat -30px -20px;
425
+  background: url('chosen-sprite.png') no-repeat -30px -20px;
426
+  direction: rtl;
427
+}
428
+.chosen-rtl.chosen-container-single .chosen-single div b {
429
+  background-position: 6px 2px;
430
+}
431
+.chosen-rtl.chosen-container-single.chosen-with-drop .chosen-single div b {
432
+  background-position: -12px 2px;
433
+}
434
+
435
+/* @end */
436
+/* @group Retina compatibility */
437
+@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi), only screen and (min-resolution: 1.5dppx) {
438
+  .chosen-rtl .chosen-search input[type="text"],
439
+  .chosen-container-single .chosen-single abbr,
440
+  .chosen-container-single .chosen-single div b,
441
+  .chosen-container-single .chosen-search input[type="text"],
442
+  .chosen-container-multi .chosen-choices .search-choice .search-choice-close,
443
+  .chosen-container .chosen-results-scroll-down span,
444
+  .chosen-container .chosen-results-scroll-up span {
445
+    background-image: url('chosen-sprite@2x.png') !important;
446
+    background-size: 52px 37px !important;
447
+    background-repeat: no-repeat !important;
448
+  }
449
+}
450
+/* @end */

+ 3
- 0
static/css/chosen.min.css
File diff suppressed because it is too large
View File


+ 239
- 0
static/css/font-awesome.css View File

@@ -0,0 +1,239 @@
1
+/*  Font Awesome
2
+    the iconic font designed for use with Twitter Bootstrap
3
+    -------------------------------------------------------
4
+    The full suite of pictographic icons, examples, and documentation
5
+    can be found at: http://fortawesome.github.com/Font-Awesome/
6
+
7
+    License
8
+    -------------------------------------------------------
9
+    The Font Awesome webfont, CSS, and LESS files are licensed under CC BY 3.0:
10
+    http://creativecommons.org/licenses/by/3.0/ A mention of
11
+    'Font Awesome - http://fortawesome.github.com/Font-Awesome' in human-readable
12
+    source code is considered acceptable attribution (most common on the web).
13
+    If human readable source code is not available to the end user, a mention in
14
+    an 'About' or 'Credits' screen is considered acceptable (most common in desktop
15
+    or mobile software).
16
+
17
+    Contact
18
+    -------------------------------------------------------
19
+    Email: dave@davegandy.com
20
+    Twitter: http://twitter.com/fortaweso_me
21
+    Work: http://lemonwi.se co-founder
22
+
23
+    */
24
+
25
+@font-face {
26
+    font-family: 'FontAwesome';
27
+    src: url('../font/fontawesome-webfont.eot');
28
+    src: url('../font/fontawesome-webfont.eot?#iefix') format('embedded-opentype'), url('../font/fontawesome-webfont.woff') format('woff'), url('../font/fontawesome-webfont.ttf') format('truetype'), url('../font/fontawesome-webfont.svgz#FontAwesomeRegular') format('svg'), url('../font/fontawesome-webfont.svg#FontAwesomeRegular') format('svg');
29
+    font-weight: normal;
30
+    font-style: normal;
31
+}
32
+/* sprites.less reset */
33
+[class^="icon-"], [class*=" icon-"] {
34
+    display: inline;
35
+    width: auto;
36
+    height: auto;
37
+    line-height: inherit;
38
+    vertical-align: baseline;
39
+    background-image: none;
40
+    background-position: 0% 0%;
41
+    background-repeat: repeat;
42
+}
43
+li[class^="icon-"], li[class*=" icon-"] {
44
+    display: block;
45
+}
46
+/*  Font Awesome styles
47
+    ------------------------------------------------------- */
48
+[class^="icon-"]:before, [class*=" icon-"]:before {
49
+    font-family: FontAwesome;
50
+    font-weight: normal;
51
+    font-style: normal;
52
+    display: inline-block;
53
+    text-decoration: inherit;
54
+}
55
+a [class^="icon-"], a [class*=" icon-"] {
56
+    display: inline-block;
57
+    text-decoration: inherit;
58
+}
59
+/* makes the font 33% larger relative to the icon container */
60
+.icon-large:before {
61
+    vertical-align: top;
62
+    font-size: 1.3333333333333333em;
63
+}
64
+.btn [class^="icon-"], .btn [class*=" icon-"] {
65
+    /* keeps button heights with and without icons the same */
66
+    line-height: .9em;
67
+}
68
+li [class^="icon-"], li [class*=" icon-"] {
69
+    display: inline-block;
70
+    width: 1.25em;
71
+    text-align: center;
72
+}
73
+li .icon-large[class^="icon-"], li .icon-large[class*=" icon-"] {
74
+    /* 1.5 increased font size for icon-large * 1.25 width */
75
+    width: 1.875em;
76
+}
77
+li[class^="icon-"], li[class*=" icon-"] {
78
+    margin-left: 0;
79
+    list-style-type: none;
80
+}
81
+li[class^="icon-"]:before, li[class*=" icon-"]:before {
82
+    text-indent: -2em;
83
+    text-align: center;
84
+}
85
+li[class^="icon-"].icon-large:before, li[class*=" icon-"].icon-large:before {
86
+    text-indent: -1.3333333333333333em;
87
+}
88
+/*  Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
89
+    readers do not read off random characters that represent icons */
90
+.icon-glass:before              { content: "\f000"; }
91
+.icon-music:before              { content: "\f001"; }
92
+.icon-search:before             { content: "\f002"; }
93
+.icon-envelope:before           { content: "\f003"; }
94
+.icon-heart:before              { content: "\f004"; }
95
+.icon-star:before               { content: "\f005"; }
96
+.icon-star-empty:before         { content: "\f006"; }
97
+.icon-user:before               { content: "\f007"; }
98
+.icon-film:before               { content: "\f008"; }
99
+.icon-th-large:before           { content: "\f009"; }
100
+.icon-th:before                 { content: "\f00a"; }
101
+.icon-th-list:before            { content: "\f00b"; }
102
+.icon-ok:before                 { content: "\f00c"; }
103
+.icon-remove:before             { content: "\f00d"; }
104
+.icon-zoom-in:before            { content: "\f00e"; }
105
+
106
+.icon-zoom-out:before           { content: "\f010"; }
107
+.icon-off:before                { content: "\f011"; }
108
+.icon-signal:before             { content: "\f012"; }
109
+.icon-cog:before                { content: "\f013"; }
110
+.icon-trash:before              { content: "\f014"; }
111
+.icon-home:before               { content: "\f015"; }
112
+.icon-file:before               { content: "\f016"; }
113
+.icon-time:before               { content: "\f017"; }
114
+.icon-road:before               { content: "\f018"; }
115
+.icon-download-alt:before       { content: "\f019"; }
116
+.icon-download:before           { content: "\f01a"; }
117
+.icon-upload:before             { content: "\f01b"; }
118
+.icon-inbox:before              { content: "\f01c"; }
119
+.icon-play-circle:before        { content: "\f01d"; }
120
+.icon-repeat:before             { content: "\f01e"; }
121
+
122
+/* \f020 is not a valid unicode character. all shifted one down */
123
+.icon-refresh:before            { content: "\f021"; }
124
+.icon-list-alt:before           { content: "\f022"; }
125
+.icon-lock:before               { content: "\f023"; }
126
+.icon-flag:before               { content: "\f024"; }
127
+.icon-headphones:before         { content: "\f025"; }
128
+.icon-volume-off:before         { content: "\f026"; }
129
+.icon-volume-down:before        { content: "\f027"; }
130
+.icon-volume-up:before          { content: "\f028"; }
131
+.icon-qrcode:before             { content: "\f029"; }
132
+.icon-barcode:before            { content: "\f02a"; }
133
+.icon-tag:before                { content: "\f02b"; }
134
+.icon-tags:before               { content: "\f02c"; }
135
+.icon-book:before               { content: "\f02d"; }
136
+.icon-bookmark:before           { content: "\f02e"; }
137
+.icon-print:before              { content: "\f02f"; }
138
+
139
+.icon-camera:before             { content: "\f030"; }
140
+.icon-font:before               { content: "\f031"; }
141
+.icon-bold:before               { content: "\f032"; }
142
+.icon-italic:before             { content: "\f033"; }
143
+.icon-text-height:before        { content: "\f034"; }
144
+.icon-text-width:before         { content: "\f035"; }
145
+.icon-align-left:before         { content: "\f036"; }
146
+.icon-align-center:before       { content: "\f037"; }
147
+.icon-align-right:before        { content: "\f038"; }
148
+.icon-align-justify:before      { content: "\f039"; }
149
+.icon-list:before               { content: "\f03a"; }
150
+.icon-indent-left:before        { content: "\f03b"; }
151
+.icon-indent-right:before       { content: "\f03c"; }
152
+.icon-facetime-video:before     { content: "\f03d"; }
153
+.icon-picture:before            { content: "\f03e"; }
154
+
155
+.icon-pencil:before             { content: "\f040"; }
156
+.icon-map-marker:before         { content: "\f041"; }
157
+.icon-adjust:before             { content: "\f042"; }
158
+.icon-tint:before               { content: "\f043"; }
159
+.icon-edit:before               { content: "\f044"; }
160
+.icon-share:before              { content: "\f045"; }
161
+.icon-check:before              { content: "\f046"; }
162
+.icon-move:before               { content: "\f047"; }
163
+.icon-step-backward:before      { content: "\f048"; }
164
+.icon-fast-backward:before      { content: "\f049"; }
165
+.icon-backward:before           { content: "\f04a"; }
166
+.icon-play:before               { content: "\f04b"; }
167
+.icon-pause:before              { content: "\f04c"; }
168
+.icon-stop:before               { content: "\f04d"; }
169
+.icon-forward:before            { content: "\f04e"; }
170
+
171
+.icon-fast-forward:before       { content: "\f050"; }
172
+.icon-step-forward:before       { content: "\f051"; }
173
+.icon-eject:before              { content: "\f052"; }
174
+.icon-chevron-left:before       { content: "\f053"; }
175
+.icon-chevron-right:before      { content: "\f054"; }
176
+.icon-plus-sign:before          { content: "\f055"; }
177
+.icon-minus-sign:before         { content: "\f056"; }
178
+.icon-remove-sign:before        { content: "\f057"; }
179
+.icon-ok-sign:before            { content: "\f058"; }
180
+.icon-question-sign:before      { content: "\f059"; }
181
+.icon-info-sign:before          { content: "\f05a"; }
182
+.icon-screenshot:before         { content: "\f05b"; }
183
+.icon-remove-circle:before      { content: "\f05c"; }
184
+.icon-ok-circle:before          { content: "\f05d"; }
185
+.icon-ban-circle:before         { content: "\f05e"; }
186
+
187
+.icon-arrow-left:before         { content: "\f060"; }
188
+.icon-arrow-right:before        { content: "\f061"; }
189
+.icon-arrow-up:before           { content: "\f062"; }
190
+.icon-arrow-down:before         { content: "\f063"; }
191
+.icon-share-alt:before          { content: "\f064"; }
192
+.icon-resize-full:before        { content: "\f065"; }
193
+.icon-resize-small:before       { content: "\f066"; }
194
+.icon-plus:before               { content: "\f067"; }
195
+.icon-minus:before              { content: "\f068"; }
196
+.icon-asterisk:before           { content: "\f069"; }
197
+.icon-exclamation-sign:before   { content: "\f06a"; }
198
+.icon-gift:before               { content: "\f06b"; }
199
+.icon-leaf:before               { content: "\f06c"; }
200
+.icon-fire:before               { content: "\f06d"; }
201
+.icon-eye-open:before           { content: "\f06e"; }
202
+
203
+.icon-eye-close:before          { content: "\f070"; }
204
+.icon-warning-sign:before       { content: "\f071"; }
205
+.icon-plane:before              { content: "\f072"; }
206
+.icon-calendar:before           { content: "\f073"; }
207
+.icon-random:before             { content: "\f074"; }
208
+.icon-comment:before            { content: "\f075"; }
209
+.icon-magnet:before             { content: "\f076"; }
210
+.icon-chevron-up:before         { content: "\f077"; }
211
+.icon-chevron-down:before       { content: "\f078"; }
212
+.icon-retweet:before            { content: "\f079"; }
213
+.icon-shopping-cart:before      { content: "\f07a"; }
214
+.icon-folder-close:before       { content: "\f07b"; }
215
+.icon-folder-open:before        { content: "\f07c"; }
216
+.icon-resize-vertical:before    { content: "\f07d"; }
217
+.icon-resize-horizontal:before  { content: "\f07e"; }
218
+
219
+.icon-bar-chart:before          { content: "\f080"; }
220
+.icon-twitter-sign:before       { content: "\f081"; }
221
+.icon-facebook-sign:before      { content: "\f082"; }
222
+.icon-camera-retro:before       { content: "\f083"; }
223
+.icon-key:before                { content: "\f084"; }
224
+.icon-cogs:before               { content: "\f085"; }
225
+.icon-comments:before           { content: "\f086"; }
226
+.icon-thumbs-up:before          { content: "\f087"; }
227
+.icon-thumbs-down:before        { content: "\f088"; }
228
+.icon-star-half:before          { content: "\f089"; }
229
+.icon-heart-empty:before        { content: "\f08a"; }
230
+.icon-signout:before            { content: "\f08b"; }
231
+.icon-linkedin-sign:before      { content: "\f08c"; }
232
+.icon-pushpin:before            { content: "\f08d"; }
233
+.icon-external-link:before      { content: "\f08e"; }
234
+
235
+.icon-signin:before             { content: "\f090"; }
236
+.icon-trophy:before             { content: "\f091"; }
237
+.icon-github-sign:before        { content: "\f092"; }
238
+.icon-upload-alt:before         { content: "\f093"; }
239
+.icon-lemon:before              { content: "\f094"; }

+ 4
- 0
static/css/font-awesome.min.css
File diff suppressed because it is too large
View File


+ 15
- 0
static/css/ie10-viewport-bug-workaround.css View File

@@ -0,0 +1,15 @@
1
+/*!
2
+ * IE10 viewport hack for Surface/desktop Windows 8 bug
3
+ * Copyright 2014-2015 Twitter, Inc.
4
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5
+ */
6
+
7
+/*
8
+ * See the Getting Started docs for more information:
9
+ * http://getbootstrap.com/getting-started/#support-ie10-width
10
+ */
11
+@-webkit-viewport { width: device-width; }
12
+@-moz-viewport    { width: device-width; }
13
+@-ms-viewport     { width: device-width; }
14
+@-o-viewport      { width: device-width; }
15
+@viewport         { width: device-width; }

BIN
static/css/images/ui-anim_basic_16x16.gif View File


BIN
static/css/images/ui-bg_diagonals-thick_18_b81900_40x40.png View File


BIN
static/css/images/ui-bg_diagonals-thick_20_666666_40x40.png View File


BIN
static/css/images/ui-bg_flat_0_888888_40x100.png View File


BIN
static/css/images/ui-bg_flat_0_aaaaaa_40x100.png View File


BIN
static/css/images/ui-bg_flat_10_000000_40x100.png View File


BIN
static/css/images/ui-bg_flat_75_ffffff_40x100.png View File


BIN
static/css/images/ui-bg_glass_100_f6f6f6_1x400.png View File


BIN
static/css/images/ui-bg_glass_100_fdf5ce_1x400.png View File


BIN
static/css/images/ui-bg_glass_25_e1f0f5_1x400.png View File


BIN
static/css/images/ui-bg_glass_55_444444_1x400.png View File


BIN
static/css/images/ui-bg_glass_65_ffffff_1x400.png View File


BIN
static/css/images/ui-bg_glass_75_dadada_1x400.png View File


BIN
static/css/images/ui-bg_gloss-wave_35_f6a828_500x100.png View File


BIN
static/css/images/ui-bg_highlight-soft_100_eeeeee_1x100.png View File


BIN
static/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png View File


BIN
static/css/images/ui-bg_highlight-soft_75_ffe45c_1x100.png View File


BIN
static/css/images/ui-bg_inset-soft_95_fef1ec_1x100.png View File


BIN
static/css/images/ui-icons_222222_256x240.png