Commit 1ef2a964 authored by cbartondock's avatar cbartondock

Merge branch 'master' of https://github.com/janeczku/calibre-web

parents 85f1ca61 6e5a1a1f
......@@ -20,16 +20,18 @@
from __future__ import division, print_function, unicode_literals
import os
import sys
import json
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
from sqlalchemy.exc import OperationalError
from sqlalchemy.sql.expression import text
try:
# Compatibility with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from . import constants, cli, logger, ub
from . import constants, cli, logger
log = logger.create()
......@@ -260,7 +262,6 @@ class _ConfigSQL(object):
"""
new_value = dictionary.get(field, default)
if new_value is None:
# log.debug("_ConfigSQL set_from_dictionary field '%s' not found", field)
return False
if field not in self.__dict__:
......@@ -277,7 +278,6 @@ class _ConfigSQL(object):
if current_value == new_value:
return False
# log.debug("_ConfigSQL set_from_dictionary '%s' = %r (was %r)", field, new_value, current_value)
setattr(self, field, new_value)
return True
......@@ -368,20 +368,23 @@ def _migrate_table(session, orm_class):
column_default = ""
else:
if isinstance(column.default.arg, bool):
column_default = ("DEFAULT %r" % int(column.default.arg))
column_default = "DEFAULT {}".format(int(column.default.arg))
else:
column_default = ("DEFAULT '%r'" % column.default.arg)
column_default = "DEFAULT `{}`".format(column.default.arg)
if isinstance(column.type, JSON):
column_type = "JSON"
else:
column_type = column.type
alter_table = "ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
alter_table = text("ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
column_name,
column_type,
column_default)
column_default))
log.debug(alter_table)
session.execute(alter_table)
changed = True
except json.decoder.JSONDecodeError as e:
log.error("Database corrupt column: {}".format(column_name))
log.debug(e)
if changed:
try:
......
......@@ -121,6 +121,8 @@ class Identifiers(Base):
return u"Douban"
elif format_type == "goodreads":
return u"Goodreads"
elif format_type == "babelio":
return u"Babelio"
elif format_type == "google":
return u"Google Books"
elif format_type == "kobo":
......@@ -148,6 +150,8 @@ class Identifiers(Base):
return u"https://dx.doi.org/{0}".format(self.val)
elif format_type == "goodreads":
return u"https://www.goodreads.com/book/show/{0}".format(self.val)
elif format_type == "babelio":
return u"https://www.babelio.com/livres/titre/{0}".format(self.val)
elif format_type == "douban":
return u"https://book.douban.com/subject/{0}".format(self.val)
elif format_type == "google":
......@@ -702,14 +706,21 @@ class CalibreDB():
return self.session.query(Books) \
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first()
def search_query(self, term):
def search_query(self, term, *join):
term.strip().lower()
self.session.connection().connection.connection.create_function("lower", 1, lcase)
q = list()
authorterms = re.split("[, ]+", term)
for authorterm in authorterms:
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
return self.session.query(Books).filter(self.common_filters(True)).filter(
query = self.session.query(Books)
if len(join) == 3:
query = query.outerjoin(join[0], join[1]).outerjoin(join[2])
elif len(join) == 2:
query = query.outerjoin(join[0], join[1])
elif len(join) == 1:
query = query.outerjoin(join[0])
return query.filter(self.common_filters(True)).filter(
or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")),
Books.series.any(func.lower(Series.name).ilike("%" + term + "%")),
Books.authors.any(and_(*q)),
......@@ -718,10 +729,10 @@ class CalibreDB():
))
# read search results from calibre-database and return it (function is used for feed and simple search
def get_search_results(self, term, offset=None, order=None, limit=None):
def get_search_results(self, term, offset=None, order=None, limit=None, *join):
order = order or [Books.sort]
pagination = None
result = self.search_query(term).order_by(*order).all()
result = self.search_query(term, *join).order_by(*order).all()
result_count = len(result)
if offset != None and limit != None:
offset = int(offset)
......
......@@ -49,5 +49,5 @@ except ImportError as err:
try:
from . import gmail
except ImportError as err:
log.debug("Cannot import Gmail, sending books via G-Mail Accounts will not work: %s", err)
log.debug("Cannot import gmail, sending books via Gmail Oauth2 Verification will not work: %s", err)
gmail = None
......@@ -53,6 +53,7 @@ def setup_gmail(token):
'expiry': creds.expiry.isoformat(),
'email': user_info
}
return {}
def get_user_info(credentials):
user_info_service = build(serviceName='oauth2', version='v2',credentials=credentials)
......
......@@ -84,15 +84,24 @@ body {
#progress .bar-load,
#progress .bar-read {
display: flex;
align-items: flex-end;
justify-content: flex-end;
position: absolute;
top: 0;
left: 0;
bottom: 0;
transition: width 150ms ease-in-out;
}
#progress .from-left {
left: 0;
align-items: flex-end;
justify-content: flex-end;
}
#progress .from-right {
right: 0;
align-items: flex-start;
justify-content: flex-start;
}
#progress .bar-load {
color: #000;
background-color: #ccc;
......
......@@ -171,7 +171,10 @@ kthoom.ImageFile = function(file) {
function initProgressClick() {
$("#progress").click(function(e) {
var page = Math.max(1, Math.ceil((e.offsetX / $(this).width()) * totalImages)) - 1;
var offset = $(this).offset();
var x = e.pageX - offset.left;
var rate = settings.direction === 0 ? x / $(this).width() : 1 - x / $(this).width();
var page = Math.max(1, Math.ceil(rate * totalImages)) - 1;
currentImage = page;
updatePage();
});
......@@ -285,6 +288,22 @@ function updatePage() {
}
function updateProgress(loadPercentage) {
if (settings.direction === 0) {
$("#progress .bar-read")
.removeClass("from-right")
.addClass("from-left");
$("#progress .bar-load")
.removeClass("from-right")
.addClass("from-left");
} else {
$("#progress .bar-read")
.removeClass("from-left")
.addClass("from-right");
$("#progress .bar-load")
.removeClass("from-left")
.addClass("from-right");
}
// Set the load/unzip progress if it's passed in
if (loadPercentage) {
$("#progress .bar-load").css({ width: loadPercentage + "%" });
......@@ -526,18 +545,17 @@ function keyHandler(evt) {
break;
case kthoom.Key.SPACE:
var container = $("#mainContent");
var atTop = container.scrollTop() === 0;
var atBottom = container.scrollTop() >= container[0].scrollHeight - container.height();
// var atTop = container.scrollTop() === 0;
// var atBottom = container.scrollTop() >= container[0].scrollHeight - container.height();
if (evt.shiftKey && atTop) {
if (evt.shiftKey) {
evt.preventDefault();
// If it's Shift + Space and the container is at the top of the page
showLeftPage();
} else if (!evt.shiftKey && atBottom) {
showPrevPage();
} else {
evt.preventDefault();
// If you're at the bottom of the page and you only pressed space
showRightPage();
container.scrollTop(0);
showNextPage();
}
break;
default:
......
......@@ -114,6 +114,20 @@ $(document).ready(function() {
}
});
$(".session").click(function() {
window.sessionStorage.setItem("back", window.location.pathname);
});
$("#back").click(function() {
var loc = sessionStorage.getItem("back");
if (!loc) {
loc = $(this).data("back");
}
sessionStorage.removeItem("back");
window.location.href = loc;
});
function confirmDialog(id, dialogid, dataValue, yesFn, noFn) {
var $confirm = $("#" + dialogid);
$confirm.modal('show');
......
This diff is collapsed.
......@@ -26,7 +26,7 @@
{% for user in allUser %}
{% if not user.role_anonymous() or config.config_anonbrowse %}
<tr>
<td><a href="{{url_for('admin.edit_user', user_id=user.id)}}">{{user.name}}</a></td>
<td><a class="session" href="{{url_for('admin.edit_user', user_id=user.id)}}">{{user.name}}</a></td>
<td>{{user.email}}</td>
<td>{{user.kindle_mail}}</td>
<td>{{user.downloads.count()}}</td>
......
......@@ -47,13 +47,13 @@
{{ text_table_row('title', _('Enter Title'),_('Title'), true, true) }}
{{ text_table_row('sort', _('Enter Title Sort'),_('Title Sort'), false, true) }}
{{ text_table_row('author_sort', _('Enter Author Sort'),_('Author Sort'), false, true) }}
{{ text_table_row('authors', _('Enter Authors'),_('Authors'), true) }}
{{ text_table_row('tags', _('Enter Categories'),_('Categories'), false, false) }}
{{ text_table_row('series', _('Enter Series'),_('Series'), false, false) }}
{{ text_table_row('authors', _('Enter Authors'),_('Authors'), true, true) }}
{{ text_table_row('tags', _('Enter Categories'),_('Categories'), false, true) }}
{{ text_table_row('series', _('Enter Series'),_('Series'), false, true) }}
<th data-field="series_index" id="series_index" data-visible="{{visiblility.get('series_index')}}" data-edit-validate="{{ _('This Field is Required') }}" data-sortable="true" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.01" data-editable-min="0" data-editable-url="{{ url_for('editbook.edit_list_book', param='series_index')}}" data-edit="true" data-editable-title="{{_('Enter title')}}"{% endif %}>{{_('Series Index')}}</th>
{{ text_table_row('languages', _('Enter Languages'),_('Languages'), false, false) }}
{{ text_table_row('languages', _('Enter Languages'),_('Languages'), false, true) }}
<!--th data-field="pubdate" data-type="date" data-visible="{{visiblility.get('pubdate')}}" data-viewformat="dd.mm.yyyy" id="pubdate" data-sortable="true">{{_('Publishing Date')}}</th-->
{{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false, false) }}
{{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false, true) }}
{% if g.user.role_delete_books() and g.user.role_edit()%}
<th data-align="right" data-formatter="EbookActions" data-switchable="false">{{_('Delete')}}</th>
{% endif %}
......
......@@ -12,7 +12,7 @@
<label for="mail_server_type">{{_('Choose Server Type')}}</label>
<select name="mail_server_type" id="config_email_type" class="form-control" data-control="email-settings">
<option value="0" {% if content.mail_server_type == 0 %}selected{% endif %}>{{_('Use Standard E-Mail Account')}}</option>
<option value="1" {% if content.mail_server_type == 1 %}selected{% endif %}>{{_('G-Mail Account with OAuth2 Verfification')}}</option>
<option value="1" {% if content.mail_server_type == 1 %}selected{% endif %}>{{_('Gmail Account with OAuth2 Verfification')}}</option>
</select>
</div>
<div data-related="email-settings-1">
......@@ -20,7 +20,7 @@
{% if content.mail_gmail_token == {} %}
<button type="submit" id="gmail_server" name="gmail" value="submit" class="btn btn-default">{{_('Setup Gmail Account as E-Mail Server')}}</button>
{% else %}
<button type="submit" id="invalidate_server" name="invalidate" value="submit" class="btn btn-danger">{{_('Revoke G-Mail Access')}}</button>
<button type="submit" id="invalidate_server" name="invalidate" value="submit" class="btn btn-danger">{{_('Revoke Gmail Access')}}</button>
{% endif %}
</div>
</div>
......@@ -66,7 +66,7 @@
{% if feature_support['gmail'] %}
</div>
{% endif %}
<a href="{{ url_for('admin.admin') }}" id="back" class="btn btn-default">{{_('Back')}}</a>
<a href="{{ url_for('admin.admin') }}" id="email_back" class="btn btn-default">{{_('Back')}}</a>
</form>
{% if g.allow_registration %}
<div class="col-md-10 col-lg-6">
......
......@@ -60,12 +60,12 @@
<a id="fullscreen" class="icon-resize-full">Fullscreen</a>
</div>
<div id="progress" role="progressbar" class="loading">
<div class="bar-load">
<div class="bar-load from-left">
<div class="text load">
Loading...
</div>
</div>
<div class="bar-read">
<div class="bar-read from-left">
<div class="text page"></div>
</div>
</div>
......
......@@ -129,7 +129,7 @@
<div class="col-sm-12">
<div id="user_submit" class="btn btn-default">{{_('Save')}}</div>
{% if not profile %}
<a href="{{ url_for('admin.admin') }}" id="back" class="btn btn-default">{{_('Cancel')}}</a>
<div class="btn btn-default" data-back="{{ url_for('admin.admin') }}" id="back">{{_('Cancel')}}</div>
{% endif %}
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
<div class="btn btn-danger" id="btndeluser" data-value="{{ content.id }}" data-remote="false" >{{_('Delete User')}}</div>
......
......@@ -24,15 +24,17 @@
data-column="{{value.get(array_field)}}"
data-formatter="checkboxFormatter">
<div class="form-check">
<div>
<label>
<input type="radio" class="check_head" name="options_{{array_field}}" onchange="checkboxHeader('false', '{{parameter}}', {{value.get(array_field)}})" disabled>{{_('Deny')}}
<input type="radio" class="check_head" data-val={{value.get(array_field)}} name="options_{{array_field}}" data-name="{{parameter}}" disabled>{{_('Deny')}}
</label>
</div>
<div class="form-check">
<div>
<label>
<input type="radio" class="check_head" name="options_{{array_field}}" onchange="checkboxHeader('true', '{{parameter}}', {{value.get(array_field)}})" disabled>{{_('Allow')}}
<input type="radio" class="check_head" data-val={{value.get(array_field)}} name="options_{{array_field}}" data-name="{{parameter}}" disabled>{{_('Allow')}}
</label>
</div>
</div>
{{show_text}}
</th>
{%- endmacro %}
......@@ -48,14 +50,13 @@
data-editable-source={{url}}
{% if validate %}data-edit-validate="{{ _('This Field is Required') }}"{% endif %}>
<div>
<select id="select_{{ parameter }}" class="header_select" onchange="selectHeader(this, '{{parameter}}')" disabled="">
<select id="select_{{ parameter }}" class="header_select" disabled="">
<option value="all">{{ _('Show All') }}</option>
{% for language in languages %}
<option value="{{language.lang_code}}">{{language.name}}</option>
{% endfor %}
</select>
</div><br>
</div>
{{ show_text }}
</th>
{%- endmacro %}
......@@ -71,13 +72,13 @@
data-editable-source={{url}}
{% if validate %}data-edit-validate="{{ _('This Field is Required') }}"{% endif %}>
<div>
<select id="select_{{ parameter }}" class="header_select" onchange="selectHeader(this, '{{parameter}}')" disabled="">
<select id="select_{{ parameter }}" class="header_select" disabled="">
<option value="None">{{_('Select...')}}</option>
{% for translation in translations %}
<option value="{{translation}}">{{translation.display_name|capitalize}}</option>
{% endfor %}
</select>
</div><br>
</div>
{{ show_text }}
</th>
{%- endmacro %}
......@@ -137,6 +138,9 @@
</tr>
</thead>
</table>
<div class="errorlink">
<div class="btn btn-default" data-back="{{ url_for('admin.admin') }}" id="back">{{_('Back')}}</div>
</div>
{% endblock %}
{% block modal %}
{{ delete_confirm_modal() }}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -75,8 +75,9 @@ def load_user_from_auth_header(header_val):
basic_username = basic_password = '' # nosec
try:
header_val = base64.b64decode(header_val).decode('utf-8')
basic_username = header_val.split(':')[0]
basic_password = header_val.split(':')[1]
# Users with colon are invalid: rfc7617 page 4
basic_username = header_val.split(':', 1)[0]
basic_password = header_val.split(':', 1)[1]
except (TypeError, UnicodeDecodeError, binascii.Error):
pass
user = _fetch_user_by_name(basic_username)
......
......@@ -760,10 +760,26 @@ def list_books():
sort = request.args.get("sort", "id")
order = request.args.get("order", "").lower()
state = None
join = tuple()
if sort == "state":
state = json.loads(request.args.get("state", "[]"))
if sort != "state" and order:
elif sort == "tags":
order = [db.Tags.name.asc()] if order == "asc" else [db.Tags.name.desc()]
join = db.books_tags_link,db.Books.id == db.books_tags_link.c.book, db.Tags
elif sort == "series":
order = [db.Series.name.asc()] if order == "asc" else [db.Series.name.desc()]
join = db.books_series_link,db.Books.id == db.books_series_link.c.book, db.Series
elif sort == "publishers":
order = [db.Publishers.name.asc()] if order == "asc" else [db.Publishers.name.desc()]
join = db.books_publishers_link,db.Books.id == db.books_publishers_link.c.book, db.Publishers
elif sort == "authors":
order = [db.Authors.name.asc()] if order == "asc" else [db.Authors.name.desc()]
join = db.books_authors_link,db.Books.id == db.books_authors_link.c.book, db.Authors
elif sort == "languages":
order = [db.Languages.lang_code.asc()] if order == "asc" else [db.Languages.lang_code.desc()]
join = db.books_languages_link,db.Books.id == db.books_languages_link.c.book, db.Languages
elif order and sort in ["sort", "title", "authors_sort", "series_index"]:
order = [text(sort + " " + order)]
elif not state:
order = [db.Books.timestamp.desc()]
......@@ -778,9 +794,9 @@ def list_books():
books = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).all()
entries = calibre_db.get_checkbox_sorted(books, state, off, limit,order)
elif search:
entries, filtered_count, __ = calibre_db.get_search_results(search, off, order, limit)
entries, filtered_count, __ = calibre_db.get_search_results(search, off, order, limit, *join)
else:
entries, __, __ = calibre_db.fill_indexpage((int(off) / (int(limit)) + 1), limit, db.Books, True, order)
entries, __, __ = calibre_db.fill_indexpage((int(off) / (int(limit)) + 1), limit, db.Books, True, order, *join)
for entry in entries:
for index in range(0, len(entry.languages)):
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment