diff --git a/oshipka/persistance/__init__.py b/oshipka/persistance/__init__.py index 3ede07d..9bf0fcb 100644 --- a/oshipka/persistance/__init__.py +++ b/oshipka/persistance/__init__.py @@ -5,15 +5,18 @@ import re from json import JSONEncoder from uuid import uuid4 -from config import SQLALCHEMY_DATABASE_URI, MAKEDIRS, DATABASE_FILE +from config import SQLALCHEMY_DATABASE_URI, MAKEDIRS, DATABASE_FILE, SEARCH_INDEX_PATH from flask_security import RoleMixin, UserMixin from flask_security import Security, SQLAlchemyUserDatastore from flask_sqlalchemy import SQLAlchemy from markupsafe import escape, Markup +from sqlalchemy import Boolean +from sqlalchemy import TypeDecorator from sqlalchemy.ext.declarative import declared_attr, DeclarativeMeta from sqlalchemy.orm.collections import InstrumentedList from sqlalchemy_utils import Choice from tww.lib import solve_query, resolve_timezone, dt_tz_translation, time_ago +from whooshalchemy import IndexService db = SQLAlchemy() @@ -40,6 +43,15 @@ class ModelJsonEncoder(JSONEncoder): return o.id +class LiberalBoolean(TypeDecorator): + impl = Boolean + + def process_bind_param(self, value, dialect): + if value is not None: + value = bool(int(value)) + return value + + def camel_case_to_snake_case(name): """ Convertes a CamelCase name to snake_case @@ -203,9 +215,19 @@ def register_filters(app): return time_ago(None, diff) +class Proxy(object): + def __init__(self, proxied): + self.proxied = proxied + + +index_service = Proxy(None) + + def init_db(app): + rv = False app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + app.config["WHOOSH_BASE"] = SEARCH_INDEX_PATH from oshipka.webapp import test_bp, oshipka_bp app.register_blueprint(test_bp) @@ -221,4 +243,7 @@ def init_db(app): if not os.path.exists(DATABASE_FILE): with app.app_context(): db.create_all() - return True + rv = True + global index_service + index_service.proxied = IndexService(config=app.config, session=db.session) + return rv diff --git a/oshipka/webapp/static/css/autocomplete.css b/oshipka/webapp/static/css/autocomplete.css new file mode 100644 index 0000000..63435ff --- /dev/null +++ b/oshipka/webapp/static/css/autocomplete.css @@ -0,0 +1,37 @@ +.autocomplete-suggestions { + text-align: left; + cursor: default; + border: 1px solid #ccc; + border-top: 0; + background: #fff; + box-shadow: -1px 1px 3px rgba(0, 0, 0, .1); + + /* core styles should not be changed */ + position: absolute; + display: none; + z-index: 9999; + max-height: 254px; + overflow: hidden; + overflow-y: auto; + box-sizing: border-box; +} + +.autocomplete-suggestion { + position: relative; + padding: 0 .6em; + line-height: 23px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 1.02em; + color: #333; +} + +.autocomplete-suggestion b { + font-weight: normal; + color: #1f8dd6; +} + +.autocomplete-suggestion.selected { + background: #f0f0f0; +} \ No newline at end of file diff --git a/oshipka/webapp/static/js/autocomplete.js b/oshipka/webapp/static/js/autocomplete.js new file mode 100644 index 0000000..b714812 --- /dev/null +++ b/oshipka/webapp/static/js/autocomplete.js @@ -0,0 +1,222 @@ +/* + JavaScript autoComplete v1.0.4 + Copyright (c) 2014 Simon Steinberger / Pixabay + GitHub: https://github.com/Pixabay/JavaScript-autoComplete + License: http://www.opensource.org/licenses/mit-license.php +*/ + +var autoComplete = (function(){ + // "use strict"; + function autoComplete(options){ + if (!document.querySelector) return; + + // helpers + function hasClass(el, className){ return el.classList ? el.classList.contains(className) : new RegExp('\\b'+ className+'\\b').test(el.className); } + + function addEvent(el, type, handler){ + if (el.attachEvent) el.attachEvent('on'+type, handler); else el.addEventListener(type, handler); + } + function removeEvent(el, type, handler){ + // if (el.removeEventListener) not working in IE11 + if (el.detachEvent) el.detachEvent('on'+type, handler); else el.removeEventListener(type, handler); + } + function live(elClass, event, cb, context){ + addEvent(context || document, event, function(e){ + var found, el = e.target || e.srcElement; + while (el && !(found = hasClass(el, elClass))) el = el.parentElement; + if (found) cb.call(el, e); + }); + } + + var o = { + selector: 0, + source: 0, + minChars: 3, + delay: 150, + offsetLeft: 0, + offsetTop: 1, + cache: 1, + menuClass: '', + renderItem: function (item, search){ + // escape special characters + search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + var re = new RegExp("(" + search.split(' ').join('|') + ")", "gi"); + return '