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 '
' + item.replace(re, "$1") + '
'; + }, + onSelect: function(e, term, item){} + }; + for (var k in options) { if (options.hasOwnProperty(k)) o[k] = options[k]; } + + // init + var elems = typeof o.selector == 'object' ? [o.selector] : document.querySelectorAll(o.selector); + for (var i=0; i 0) + that.sc.scrollTop = selTop + that.sc.suggestionHeight + scrTop - that.sc.maxHeight; + else if (selTop < 0) + that.sc.scrollTop = selTop + scrTop; + } + } + } + addEvent(window, 'resize', that.updateSC); + document.body.appendChild(that.sc); + + live('autocomplete-suggestion', 'mouseleave', function(e){ + var sel = that.sc.querySelector('.autocomplete-suggestion.selected'); + if (sel) setTimeout(function(){ sel.className = sel.className.replace('selected', ''); }, 20); + }, that.sc); + + live('autocomplete-suggestion', 'mouseover', function(e){ + var sel = that.sc.querySelector('.autocomplete-suggestion.selected'); + if (sel) sel.className = sel.className.replace('selected', ''); + this.className += ' selected'; + }, that.sc); + + live('autocomplete-suggestion', 'mousedown', function(e){ + if (hasClass(this, 'autocomplete-suggestion')) { // else outside click + var v = this.getAttribute('data-val'); + that.value = v; + o.onSelect(e, v, this); + that.sc.style.display = 'none'; + } + }, that.sc); + + that.blurHandler = function(){ + try { var over_sb = document.querySelector('.autocomplete-suggestions:hover'); } catch(e){ var over_sb = 0; } + if (!over_sb) { + that.last_val = that.value; + that.sc.style.display = 'none'; + setTimeout(function(){ that.sc.style.display = 'none'; }, 350); // hide suggestions on fast input + } else if (that !== document.activeElement) setTimeout(function(){ that.focus(); }, 20); + }; + addEvent(that, 'blur', that.blurHandler); + + var suggest = function(data){ + var val = that.value; + that.cache[val] = data; + if (data.length && val.length >= o.minChars) { + var s = ''; + for (var i=0;i 40) && key != 13 && key != 27) { + var val = that.value; + if (val.length >= o.minChars) { + if (val != that.last_val) { + that.last_val = val; + clearTimeout(that.timer); + if (o.cache) { + if (val in that.cache) { suggest(that.cache[val]); return; } + // no requests if previous suggestions were empty + for (var i=1; i