From d979ed89c6f6ad8d6e0d89ea68c959dc073794d2 Mon Sep 17 00:00:00 2001 From: Cervinko Cera Date: Mon, 28 Mar 2016 16:42:38 +0200 Subject: [PATCH 1/6] Fixes for editing Authors --- cps/db.py | 2 +- cps/web.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cps/db.py b/cps/db.py index d4d0381f..33c4f061 100755 --- a/cps/db.py +++ b/cps/db.py @@ -88,7 +88,7 @@ class Authors(Base): def __init__(self, name, sort, link): self.name = name self.sort = sort - self.sort = link + self.link = link def __repr__(self): return u"".format(self.name, self.sort, self.link) diff --git a/cps/web.py b/cps/web.py index 8a45db4d..26520a76 100755 --- a/cps/web.py +++ b/cps/web.py @@ -662,7 +662,20 @@ def edit_book(book_id): to_save = request.form.to_dict() #print to_save book.title = to_save["book_title"] - book.authors[0].name = to_save["author_name"] + + is_author = db.session.query(db.Authors).filter(db.Authors.name.like('%' + to_save["author_name"].strip() + '%')).first() + if book.authors[0].name not in ("Unknown", "Unbekannt", "", " "): + if is_author: + book.authors.append(is_author) + book.authors.remove(db.session.query(db.Authors).get(book.authors[0].id)) + else: + book.authors[0].name = to_save["author_name"].strip() + else: + if is_author: + book.authors.append(is_author) + else: + book.authors.append(db.Authors(to_save["author_name"].strip(), "", "")) + book.authors.remove(db.session.query(db.Authors).get(book.authors[0].id)) if to_save["cover_url"] and os.path.splitext(to_save["cover_url"])[1].lower() == ".jpg": img = requests.get(to_save["cover_url"]) From cd0aeb28571bd164ceb5cf6c80a67422adf8e5a7 Mon Sep 17 00:00:00 2001 From: Cervinko Cera Date: Tue, 29 Mar 2016 00:09:11 +0200 Subject: [PATCH 2/6] Add Typeahead for Author input --- cps/static/css/typeaheadjs.css | 93 +++++ cps/static/js/bootstrap3-typeahead.js | 523 ++++++++++++++++++++++++++ cps/templates/edit_book.html | 22 +- cps/templates/layout.html | 20 +- cps/web.py | 9 + 5 files changed, 657 insertions(+), 10 deletions(-) create mode 100644 cps/static/css/typeaheadjs.css create mode 100644 cps/static/js/bootstrap3-typeahead.js diff --git a/cps/static/css/typeaheadjs.css b/cps/static/css/typeaheadjs.css new file mode 100644 index 00000000..64c10736 --- /dev/null +++ b/cps/static/css/typeaheadjs.css @@ -0,0 +1,93 @@ +span.twitter-typeahead .tt-menu, +span.twitter-typeahead .tt-dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + font-size: 14px; + text-align: left; + background-color: #ffffff; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + background-clip: padding-box; +} +span.twitter-typeahead .tt-suggestion { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #333333; + white-space: nowrap; +} +span.twitter-typeahead .tt-suggestion.tt-cursor, +span.twitter-typeahead .tt-suggestion:hover, +span.twitter-typeahead .tt-suggestion:focus { + color: #ffffff; + text-decoration: none; + outline: 0; + background-color: #337ab7; +} +.input-group.input-group-lg span.twitter-typeahead .form-control { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.input-group.input-group-sm span.twitter-typeahead .form-control { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +span.twitter-typeahead { + width: 100%; +} +.input-group span.twitter-typeahead { + display: block !important; + height: 34px; +} +.input-group span.twitter-typeahead .tt-menu, +.input-group span.twitter-typeahead .tt-dropdown-menu { + top: 32px !important; +} +.input-group span.twitter-typeahead:not(:first-child):not(:last-child) .form-control { + border-radius: 0; +} +.input-group span.twitter-typeahead:first-child .form-control { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group span.twitter-typeahead:last-child .form-control { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} +.input-group.input-group-sm span.twitter-typeahead { + height: 30px; +} +.input-group.input-group-sm span.twitter-typeahead .tt-menu, +.input-group.input-group-sm span.twitter-typeahead .tt-dropdown-menu { + top: 30px !important; +} +.input-group.input-group-lg span.twitter-typeahead { + height: 46px; +} +.input-group.input-group-lg span.twitter-typeahead .tt-menu, +.input-group.input-group-lg span.twitter-typeahead .tt-dropdown-menu { + top: 46px !important; +} diff --git a/cps/static/js/bootstrap3-typeahead.js b/cps/static/js/bootstrap3-typeahead.js new file mode 100644 index 00000000..936d8937 --- /dev/null +++ b/cps/static/js/bootstrap3-typeahead.js @@ -0,0 +1,523 @@ +/* ============================================================= + * bootstrap3-typeahead.js v3.1.0 + * https://github.com/bassjobsen/Bootstrap-3-Typeahead + * ============================================================= + * Original written by @mdo and @fat + * ============================================================= + * Copyright 2014 Bass Jobsen @bassjobsen + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +(function (root, factory) { + + 'use strict'; + + // CommonJS module is defined + if (typeof module !== 'undefined' && module.exports) { + module.exports = factory(require('jquery')); + } + // AMD module is defined + else if (typeof define === 'function' && define.amd) { + define(['jquery'], function ($) { + return factory ($); + }); + } else { + factory(root.jQuery); + } + +}(this, function ($) { + + 'use strict'; + // jshint laxcomma: true + + + /* TYPEAHEAD PUBLIC CLASS DEFINITION + * ================================= */ + + var Typeahead = function (element, options) { + this.$element = $(element); + this.options = $.extend({}, $.fn.typeahead.defaults, options); + this.matcher = this.options.matcher || this.matcher; + this.sorter = this.options.sorter || this.sorter; + this.select = this.options.select || this.select; + this.autoSelect = typeof this.options.autoSelect == 'boolean' ? this.options.autoSelect : true; + this.highlighter = this.options.highlighter || this.highlighter; + this.render = this.options.render || this.render; + this.updater = this.options.updater || this.updater; + this.displayText = this.options.displayText || this.displayText; + this.source = this.options.source; + this.delay = this.options.delay; + this.$menu = $(this.options.menu); + this.$appendTo = this.options.appendTo ? $(this.options.appendTo) : null; + this.shown = false; + this.listen(); + this.showHintOnFocus = typeof this.options.showHintOnFocus == 'boolean' ? this.options.showHintOnFocus : false; + this.afterSelect = this.options.afterSelect; + this.addItem = false; + }; + + Typeahead.prototype = { + + constructor: Typeahead, + + select: function () { + var val = this.$menu.find('.active').data('value'); + this.$element.data('active', val); + if(this.autoSelect || val) { + var newVal = this.updater(val); + // Updater can be set to any random functions via "options" parameter in constructor above. + // Add null check for cases when updater returns void or undefined. + if (!newVal) { + newVal = ""; + } + this.$element + .val(this.displayText(newVal) || newVal) + .change(); + this.afterSelect(newVal); + } + return this.hide(); + }, + + updater: function (item) { + return item; + }, + + setSource: function (source) { + this.source = source; + }, + + show: function () { + var pos = $.extend({}, this.$element.position(), { + height: this.$element[0].offsetHeight + }), scrollHeight; + + scrollHeight = typeof this.options.scrollHeight == 'function' ? + this.options.scrollHeight.call() : + this.options.scrollHeight; + + var element; + if (this.shown) { + element = this.$menu; + } else if (this.$appendTo) { + element = this.$menu.appendTo(this.$appendTo); + } else { + element = this.$menu.insertAfter(this.$element); + } + element.css({ + top: pos.top + pos.height + scrollHeight + , left: pos.left + }) + .show(); + + this.shown = true; + return this; + }, + + hide: function () { + this.$menu.hide(); + this.shown = false; + return this; + }, + + lookup: function (query) { + var items; + if (typeof(query) != 'undefined' && query !== null) { + this.query = query; + } else { + this.query = this.$element.val() || ''; + } + + if (this.query.length < this.options.minLength && !this.options.showHintOnFocus) { + return this.shown ? this.hide() : this; + } + + var worker = $.proxy(function() { + + if($.isFunction(this.source)) this.source(this.query, $.proxy(this.process, this)); + else if (this.source) { + this.process(this.source); + } + }, this); + + clearTimeout(this.lookupWorker); + this.lookupWorker = setTimeout(worker, this.delay); + }, + + process: function (items) { + var that = this; + + items = $.grep(items, function (item) { + return that.matcher(item); + }); + + items = this.sorter(items); + + if (!items.length && !this.options.addItem) { + return this.shown ? this.hide() : this; + } + + if (items.length > 0) { + this.$element.data('active', items[0]); + } else { + this.$element.data('active', null); + } + + // Add item + if (this.options.addItem){ + items.push(this.options.addItem); + } + + if (this.options.items == 'all') { + return this.render(items).show(); + } else { + return this.render(items.slice(0, this.options.items)).show(); + } + }, + + matcher: function (item) { + var it = this.displayText(item); + return ~it.toLowerCase().indexOf(this.query.toLowerCase()); + }, + + sorter: function (items) { + var beginswith = [] + , caseSensitive = [] + , caseInsensitive = [] + , item; + + while ((item = items.shift())) { + var it = this.displayText(item); + if (!it.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item); + else if (~it.indexOf(this.query)) caseSensitive.push(item); + else caseInsensitive.push(item); + } + + return beginswith.concat(caseSensitive, caseInsensitive); + }, + + highlighter: function (item) { + var html = $('
'); + var query = this.query; + var i = item.toLowerCase().indexOf(query.toLowerCase()); + var len, leftPart, middlePart, rightPart, strong; + len = query.length; + if(len === 0){ + return html.text(item).html(); + } + while (i > -1) { + leftPart = item.substr(0, i); + middlePart = item.substr(i, len); + rightPart = item.substr(i + len); + strong = $('').text(middlePart); + html + .append(document.createTextNode(leftPart)) + .append(strong); + item = rightPart; + i = item.toLowerCase().indexOf(query.toLowerCase()); + } + return html.append(document.createTextNode(item)).html(); + }, + + render: function (items) { + var that = this; + var self = this; + var activeFound = false; + var data = []; + var _category = that.options.separator; + + $.each(items, function (key,value) { + // inject separator + if (key > 0 && value[_category] !== items[key - 1][_category]){ + data.push({ + __type: 'divider' + }); + } + + // inject category header + if (value[_category] && (key === 0 || value[_category] !== items[key - 1][_category])){ + data.push({ + __type: 'category', + name: value[_category] + }); + } + data.push(value); + }); + + items = $(data).map(function (i, item) { + + if ((item.__type || false) == 'category'){ + return $(that.options.headerHtml).text(item.name)[0]; + } + + if ((item.__type || false) == 'divider'){ + return $(that.options.headerDivider)[0]; + } + + var text = self.displayText(item); + i = $(that.options.item).data('value', item); + i.find('a').html(that.highlighter(text, item)); + if (text == self.$element.val()) { + i.addClass('active'); + self.$element.data('active', item); + activeFound = true; + } + return i[0]; + }); + + if (this.autoSelect && !activeFound) { + items.filter(':not(.dropdown-header)').first().addClass('active'); + this.$element.data('active', items.first().data('value')); + } + this.$menu.html(items); + return this; + }, + + displayText: function(item) { + return typeof item !== 'undefined' && typeof item.name != 'undefined' && item.name || item; + }, + + next: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , next = active.next(); + + if (!next.length) { + next = $(this.$menu.find('li')[0]); + } + + next.addClass('active'); + }, + + prev: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , prev = active.prev(); + + if (!prev.length) { + prev = this.$menu.find('li').last(); + } + + prev.addClass('active'); + }, + + listen: function () { + this.$element + .on('focus', $.proxy(this.focus, this)) + .on('blur', $.proxy(this.blur, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('input', $.proxy(this.input, this)) + .on('keyup', $.proxy(this.keyup, this)); + + if (this.eventSupported('keydown')) { + this.$element.on('keydown', $.proxy(this.keydown, this)); + } + + this.$menu + .on('click', $.proxy(this.click, this)) + .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) + .on('mouseleave', 'li', $.proxy(this.mouseleave, this)); + }, + + destroy : function () { + this.$element.data('typeahead',null); + this.$element.data('active',null); + this.$element + .off('focus') + .off('blur') + .off('keypress') + .off('input') + .off('keyup'); + + if (this.eventSupported('keydown')) { + this.$element.off('keydown'); + } + + this.$menu.remove(); + }, + + eventSupported: function(eventName) { + var isSupported = eventName in this.$element; + if (!isSupported) { + this.$element.setAttribute(eventName, 'return;'); + isSupported = typeof this.$element[eventName] === 'function'; + } + return isSupported; + }, + + move: function (e) { + if (!this.shown) return; + + switch(e.keyCode) { + case 9: // tab + case 13: // enter + case 27: // escape + e.preventDefault(); + break; + + case 38: // up arrow + // with the shiftKey (this is actually the left parenthesis) + if (e.shiftKey) return; + e.preventDefault(); + this.prev(); + break; + + case 40: // down arrow + // with the shiftKey (this is actually the right parenthesis) + if (e.shiftKey) return; + e.preventDefault(); + this.next(); + break; + } + }, + + keydown: function (e) { + this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27]); + if (!this.shown && e.keyCode == 40) { + this.lookup(); + } else { + this.move(e); + } + }, + + keypress: function (e) { + if (this.suppressKeyPressRepeat) return; + this.move(e); + }, + + input: function (e) { + this.lookup(); + e.preventDefault(); + }, + + keyup: function (e) { + switch(e.keyCode) { + case 40: // down arrow + case 38: // up arrow + case 16: // shift + case 17: // ctrl + case 18: // alt + break; + + case 9: // tab + case 13: // enter + if (!this.shown) return; + this.select(); + break; + + case 27: // escape + if (!this.shown) return; + this.hide(); + break; + } + + e.preventDefault(); + }, + + focus: function (e) { + if (!this.focused) { + this.focused = true; + if (this.options.showHintOnFocus) { + this.lookup(''); + } + } + }, + + blur: function (e) { + this.focused = false; + if (!this.mousedover && this.shown) this.hide(); + }, + + click: function (e) { + e.preventDefault(); + this.select(); + this.$element.focus(); + this.hide(); + }, + + mouseenter: function (e) { + this.mousedover = true; + this.$menu.find('.active').removeClass('active'); + $(e.currentTarget).addClass('active'); + }, + + mouseleave: function (e) { + this.mousedover = false; + if (!this.focused && this.shown) this.hide(); + } + + }; + + + /* TYPEAHEAD PLUGIN DEFINITION + * =========================== */ + + var old = $.fn.typeahead; + + $.fn.typeahead = function (option) { + var arg = arguments; + if (typeof option == 'string' && option == 'getActive') { + return this.data('active'); + } + return this.each(function () { + var $this = $(this) + , data = $this.data('typeahead') + , options = typeof option == 'object' && option; + if (!data) $this.data('typeahead', (data = new Typeahead(this, options))); + if (typeof option == 'string' && data[option]) { + if (arg.length > 1) { + data[option].apply(data, Array.prototype.slice.call(arg ,1)); + } else { + data[option](); + } + } + }); + }; + + $.fn.typeahead.defaults = { + source: [], + items: 8, + menu: '', + item: '
  • ', + minLength: 1, + scrollHeight: 0, + autoSelect: true, + afterSelect: $.noop, + addItem: false, + delay: 0, + separator: 'category', + headerHtml: '', + headerDivider: '' + }; + + $.fn.typeahead.Constructor = Typeahead; + + + /* TYPEAHEAD NO CONFLICT + * =================== */ + + $.fn.typeahead.noConflict = function () { + $.fn.typeahead = old; + return this; + }; + + + /* TYPEAHEAD DATA-API + * ================== */ + + $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { + var $this = $(this); + if ($this.data('typeahead')) return; + $this.typeahead($this.data()); + }); + +})); diff --git a/cps/templates/edit_book.html b/cps/templates/edit_book.html index 4dc5d220..75858c77 100644 --- a/cps/templates/edit_book.html +++ b/cps/templates/edit_book.html @@ -1,6 +1,26 @@ {% extends "layout.html" %} {% block body %} {% if book %} + +
    {% if book.has_cover is defined %} @@ -21,7 +41,7 @@

    {{author.name.join(" & ")}}

    {% endfor %} {% endif %} - +
    diff --git a/cps/templates/layout.html b/cps/templates/layout.html index ffcea76c..68fa4b46 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -18,6 +18,17 @@ + + + + + + + + + + + @@ -127,14 +138,5 @@
    - - - - - - - - - diff --git a/cps/web.py b/cps/web.py index 26520a76..11cee883 100755 --- a/cps/web.py +++ b/cps/web.py @@ -17,6 +17,7 @@ from werkzeug.security import generate_password_hash, check_password_hash from functools import wraps import base64 from sqlalchemy.sql import * +import json app = (Flask(__name__)) @@ -221,6 +222,14 @@ def get_opds_download_link(book_id, format): suffix=format ) return response + +@app.route("/get_authors_json", methods = ['GET', 'POST']) +def get_authors_json(): + if request.method == "POST": + form = request.form.to_dict() + entries = db.session.execute("select name from authors where name like '%" + form['query'] + "%'") + return json.dumps([dict(r) for r in entries]) + @app.route("/", defaults={'page': 1}) @app.route('/page/') From 1a922d455283b9c6c6dbd29c677f6e059b459a3e Mon Sep 17 00:00:00 2001 From: Cervinko Cera Date: Tue, 29 Mar 2016 00:30:59 +0200 Subject: [PATCH 3/6] Fix crash when 'view book after edit' is not checked --- cps/web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/web.py b/cps/web.py index 11cee883..c0edd002 100755 --- a/cps/web.py +++ b/cps/web.py @@ -723,7 +723,7 @@ def edit_book(book_id): new_rating = db.Ratings(rating=int(to_save["rating"].strip())) book.ratings[0] = new_rating db.session.commit() - if to_save["detail_view"]: + if "detail_view" in to_save: return redirect(url_for('show_book', id=book.id)) else: return render_template('edit_book.html', book=book) From a395e9f301f2dffc15b0d600b3c17f19c277a2cc Mon Sep 17 00:00:00 2001 From: Cervinko Cera Date: Sun, 3 Apr 2016 00:31:30 +0200 Subject: [PATCH 4/6] remove orphaned authors --- cps/web.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cps/web.py b/cps/web.py index 082eb3ca..5b459608 100755 --- a/cps/web.py +++ b/cps/web.py @@ -665,11 +665,16 @@ def edit_book(book_id): to_save = request.form.to_dict() book.title = to_save["book_title"] + author_id = book.authors[0].id + is_author = db.session.query(db.Authors).filter(db.Authors.name.like('%' + to_save["author_name"].strip() + '%')).first() if book.authors[0].name not in ("Unknown", "Unbekannt", "", " "): if is_author: book.authors.append(is_author) book.authors.remove(db.session.query(db.Authors).get(book.authors[0].id)) + authors_books_count = db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id.is_(author_id))).count() + if authors_books_count == 0: + db.session.query(db.Authors).filter(db.Authors.id == author_id).delete() else: book.authors[0].name = to_save["author_name"].strip() else: From a96417f2a5e0dade80638ecb2d70eb887f759d23 Mon Sep 17 00:00:00 2001 From: Cervinko Cera Date: Sun, 3 Apr 2016 15:12:48 +0200 Subject: [PATCH 5/6] fix for password edit at the admin page --- cps/web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/web.py b/cps/web.py index 5b459608..7712535b 100755 --- a/cps/web.py +++ b/cps/web.py @@ -637,7 +637,7 @@ def edit_user(user_id): return redirect(url_for('user_list')) else: if to_save["password"]: - content.password == generate_password_hash(to_save["password"]) + content.password = generate_password_hash(to_save["password"]) if "admin_user" in to_save and content.role != 1: content.role = 1 elif not "admin_user" in to_save and content.role == 1: From cb2ac4d1425ea135cd339e99e0366e458d63fbdb Mon Sep 17 00:00:00 2001 From: Cervinko Cera Date: Sun, 3 Apr 2016 23:22:26 +0200 Subject: [PATCH 6/6] for for editing Author --- cps/web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/web.py b/cps/web.py index 7712535b..5ca3fb24 100755 --- a/cps/web.py +++ b/cps/web.py @@ -667,7 +667,7 @@ def edit_book(book_id): author_id = book.authors[0].id - is_author = db.session.query(db.Authors).filter(db.Authors.name.like('%' + to_save["author_name"].strip() + '%')).first() + is_author = db.session.query(db.Authors).filter(db.Authors.name == to_save["author_name"].strip()).first() if book.authors[0].name not in ("Unknown", "Unbekannt", "", " "): if is_author: book.authors.append(is_author)