1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-11-24 18:47:23 +00:00
This commit is contained in:
cbartondock 2021-04-19 12:20:54 -04:00
commit 1ef2a96465
53 changed files with 5668 additions and 5017 deletions

View File

@ -20,16 +20,18 @@
from __future__ import division, print_function, unicode_literals from __future__ import division, print_function, unicode_literals
import os import os
import sys 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.exc import OperationalError
from sqlalchemy.sql.expression import text
try: try:
# Compatibility with sqlalchemy 2.0 # Compatibility with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base from sqlalchemy.orm import declarative_base
except ImportError: except ImportError:
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from . import constants, cli, logger, ub from . import constants, cli, logger
log = logger.create() log = logger.create()
@ -260,7 +262,6 @@ class _ConfigSQL(object):
""" """
new_value = dictionary.get(field, default) new_value = dictionary.get(field, default)
if new_value is None: if new_value is None:
# log.debug("_ConfigSQL set_from_dictionary field '%s' not found", field)
return False return False
if field not in self.__dict__: if field not in self.__dict__:
@ -277,7 +278,6 @@ class _ConfigSQL(object):
if current_value == new_value: if current_value == new_value:
return False return False
# log.debug("_ConfigSQL set_from_dictionary '%s' = %r (was %r)", field, new_value, current_value)
setattr(self, field, new_value) setattr(self, field, new_value)
return True return True
@ -368,20 +368,23 @@ def _migrate_table(session, orm_class):
column_default = "" column_default = ""
else: else:
if isinstance(column.default.arg, bool): if isinstance(column.default.arg, bool):
column_default = ("DEFAULT %r" % int(column.default.arg)) column_default = "DEFAULT {}".format(int(column.default.arg))
else: else:
column_default = ("DEFAULT '%r'" % column.default.arg) column_default = "DEFAULT `{}`".format(column.default.arg)
if isinstance(column.type, JSON): if isinstance(column.type, JSON):
column_type = "JSON" column_type = "JSON"
else: else:
column_type = column.type 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_name,
column_type, column_type,
column_default) column_default))
log.debug(alter_table) log.debug(alter_table)
session.execute(alter_table) session.execute(alter_table)
changed = True changed = True
except json.decoder.JSONDecodeError as e:
log.error("Database corrupt column: {}".format(column_name))
log.debug(e)
if changed: if changed:
try: try:

View File

@ -121,6 +121,8 @@ class Identifiers(Base):
return u"Douban" return u"Douban"
elif format_type == "goodreads": elif format_type == "goodreads":
return u"Goodreads" return u"Goodreads"
elif format_type == "babelio":
return u"Babelio"
elif format_type == "google": elif format_type == "google":
return u"Google Books" return u"Google Books"
elif format_type == "kobo": elif format_type == "kobo":
@ -148,6 +150,8 @@ class Identifiers(Base):
return u"https://dx.doi.org/{0}".format(self.val) return u"https://dx.doi.org/{0}".format(self.val)
elif format_type == "goodreads": elif format_type == "goodreads":
return u"https://www.goodreads.com/book/show/{0}".format(self.val) 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": elif format_type == "douban":
return u"https://book.douban.com/subject/{0}".format(self.val) return u"https://book.douban.com/subject/{0}".format(self.val)
elif format_type == "google": elif format_type == "google":
@ -702,14 +706,21 @@ class CalibreDB():
return self.session.query(Books) \ return self.session.query(Books) \
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first() .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() term.strip().lower()
self.session.connection().connection.connection.create_function("lower", 1, lcase) self.session.connection().connection.connection.create_function("lower", 1, lcase)
q = list() q = list()
authorterms = re.split("[, ]+", term) authorterms = re.split("[, ]+", term)
for authorterm in authorterms: for authorterm in authorterms:
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%"))) 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 + "%")), or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")),
Books.series.any(func.lower(Series.name).ilike("%" + term + "%")), Books.series.any(func.lower(Series.name).ilike("%" + term + "%")),
Books.authors.any(and_(*q)), 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 # 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] order = order or [Books.sort]
pagination = None 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) result_count = len(result)
if offset != None and limit != None: if offset != None and limit != None:
offset = int(offset) offset = int(offset)

View File

@ -49,5 +49,5 @@ except ImportError as err:
try: try:
from . import gmail from . import gmail
except ImportError as err: 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 gmail = None

View File

@ -53,6 +53,7 @@ def setup_gmail(token):
'expiry': creds.expiry.isoformat(), 'expiry': creds.expiry.isoformat(),
'email': user_info 'email': user_info
} }
return {}
def get_user_info(credentials): def get_user_info(credentials):
user_info_service = build(serviceName='oauth2', version='v2',credentials=credentials) user_info_service = build(serviceName='oauth2', version='v2',credentials=credentials)

View File

@ -84,15 +84,24 @@ body {
#progress .bar-load, #progress .bar-load,
#progress .bar-read { #progress .bar-read {
display: flex; display: flex;
align-items: flex-end;
justify-content: flex-end;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0;
bottom: 0; bottom: 0;
transition: width 150ms ease-in-out; 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 { #progress .bar-load {
color: #000; color: #000;
background-color: #ccc; background-color: #ccc;

View File

@ -171,7 +171,10 @@ kthoom.ImageFile = function(file) {
function initProgressClick() { function initProgressClick() {
$("#progress").click(function(e) { $("#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; currentImage = page;
updatePage(); updatePage();
}); });
@ -285,6 +288,22 @@ function updatePage() {
} }
function updateProgress(loadPercentage) { 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 // Set the load/unzip progress if it's passed in
if (loadPercentage) { if (loadPercentage) {
$("#progress .bar-load").css({ width: loadPercentage + "%" }); $("#progress .bar-load").css({ width: loadPercentage + "%" });
@ -526,18 +545,17 @@ function keyHandler(evt) {
break; break;
case kthoom.Key.SPACE: case kthoom.Key.SPACE:
var container = $("#mainContent"); var container = $("#mainContent");
var atTop = container.scrollTop() === 0; // var atTop = container.scrollTop() === 0;
var atBottom = container.scrollTop() >= container[0].scrollHeight - container.height(); // var atBottom = container.scrollTop() >= container[0].scrollHeight - container.height();
if (evt.shiftKey && atTop) { if (evt.shiftKey) {
evt.preventDefault(); evt.preventDefault();
// If it's Shift + Space and the container is at the top of the page // If it's Shift + Space and the container is at the top of the page
showLeftPage(); showPrevPage();
} else if (!evt.shiftKey && atBottom) { } else {
evt.preventDefault(); evt.preventDefault();
// If you're at the bottom of the page and you only pressed space // If you're at the bottom of the page and you only pressed space
showRightPage(); showNextPage();
container.scrollTop(0);
} }
break; break;
default: default:

View File

@ -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) { function confirmDialog(id, dialogid, dataValue, yesFn, noFn) {
var $confirm = $("#" + dialogid); var $confirm = $("#" + dialogid);
$confirm.modal('show'); $confirm.modal('show');

View File

@ -177,7 +177,6 @@ $(function() {
}, },
}); });
$("#domain_allow_submit").click(function(event) { $("#domain_allow_submit").click(function(event) {
event.preventDefault(); event.preventDefault();
$("#domain_add_allow").ajaxForm(); $("#domain_add_allow").ajaxForm();
@ -388,7 +387,6 @@ $(function() {
var target = $(e.relatedTarget).attr('id'); var target = $(e.relatedTarget).attr('id');
var dataId; var dataId;
$(e.relatedTarget).one('focus', function(e){$(this).blur();}); $(e.relatedTarget).one('focus', function(e){$(this).blur();});
//$(e.relatedTarget).blur();
if ($(e.relatedTarget).hasClass("button_head")) { if ($(e.relatedTarget).hasClass("button_head")) {
dataId = $('#user-table').bootstrapTable('getSelections').map(a => a.id); dataId = $('#user-table').bootstrapTable('getSelections').map(a => a.id);
} else { } else {
@ -454,18 +452,28 @@ $(function() {
$(this).next().text(elText); $(this).next().text(elText);
}); });
}, },
onLoadSuccess: function () { onPostHeader () {
var guest = $(".editable[data-name='name'][data-value='Guest']"); deactivateHeaderButtons();
guest.editable("disable");
$(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").editable("disable");
$("input[data-name='admin_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$("input[data-name='passwd_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$("input[data-name='edit_shelf_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$("input[data-name='sidebar_read_and_unread'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$(".user-remove[data-pk='"+guest.data("pk")+"']").hide();
}, },
onSort: function(a, b) { onLoadSuccess: function () {
console.log("huh"); loadSuccess();
var element = $(".header_select");
element.each(function() {
var item = $(this).parent();
var parent = item.parent().parent();
if (parent.prop('nodeName') === "TH") {
item.prependTo(parent);
}
});
var element = $(".form-check");
element.each(function() {
var item = $(this).parent();
var parent = item.parent().parent();
if (parent.prop('nodeName') === "TH") {
item.prependTo(parent);
}
});
}, },
onColumnSwitch: function () { onColumnSwitch: function () {
var visible = $("#user-table").bootstrapTable("getVisibleColumns"); var visible = $("#user-table").bootstrapTable("getVisibleColumns");
@ -491,7 +499,18 @@ $(function() {
$("#user_delete_selection").click(function() { $("#user_delete_selection").click(function() {
$("#user-table").bootstrapTable("uncheckAll"); $("#user-table").bootstrapTable("uncheckAll");
}); });
$("#select_locale").on("change",function() {
selectHeader(this, "locale");
});
$("#select_default_language").on("change",function() {
selectHeader(this, "default_language");
});
$(".check_head").on("change",function() {
var val = $(this).val() === "on";
var name = $(this).data("name");
var data = $(this).data("val");
checkboxHeader(val, name, data);
});
function user_handle (userId) { function user_handle (userId) {
$.ajax({ $.ajax({
method:"post", method:"post",
@ -505,6 +524,7 @@ $(function() {
timeout: 900, timeout: 900,
success:function(data) { success:function(data) {
$("#user-table").bootstrapTable("load", data); $("#user-table").bootstrapTable("load", data);
loadSuccess();
} }
}); });
} }
@ -560,10 +580,6 @@ function TableActions (value, row) {
].join(""); ].join("");
} }
function editEntry(param)
{
console.log(param);
}
/* Function for deleting domain restrictions */ /* Function for deleting domain restrictions */
function RestrictionActions (value, row) { function RestrictionActions (value, row) {
return [ return [
@ -582,7 +598,7 @@ function EbookActions (value, row) {
].join(""); ].join("");
} }
/* Function for deleting books */ /* Function for deleting Users */
function UserActions (value, row) { function UserActions (value, row) {
return [ return [
"<div class=\"user-remove\" data-value=\"delete\" onclick=\"deleteUser(this, '" + row.id + "')\" data-pk=\"" + row.id + "\" title=\"Remove\">", "<div class=\"user-remove\" data-value=\"delete\" onclick=\"deleteUser(this, '" + row.id + "')\" data-pk=\"" + row.id + "\" title=\"Remove\">",
@ -600,7 +616,7 @@ function responseHandler(res) {
} }
function singleUserFormatter(value, row) { function singleUserFormatter(value, row) {
return '<a class="btn btn-default" href="' + window.location.pathname + '/../../admin/user/' + row.id + '">' + this.buttontext + '</a>' return '<a class="btn btn-default" onclick="storeLocation()" href="' + window.location.pathname + '/../../admin/user/' + row.id + '">' + this.buttontext + '</a>'
} }
function checkboxFormatter(value, row, index){ function checkboxFormatter(value, row, index){
@ -610,15 +626,22 @@ function checkboxFormatter(value, row, index){
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', ' + this.column + ')">'; return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', ' + this.column + ')">';
} }
function checkboxChange(checkbox, userId, field, field_index) { function loadSuccess() {
$.ajax({ var guest = $(".editable[data-name='name'][data-value='Guest']");
method: "post", guest.editable("disable");
url: window.location.pathname + "/../../ajax/editlistusers/" + field, $(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").editable("disable");
data: {"pk": userId, "field_index": field_index, "value": checkbox.checked}, $(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").hide();
success: function (data) { $("input[data-name='admin_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
if (!jQuery.isEmptyObject(data)) { $("input[data-name='passwd_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$("input[data-name='edit_shelf_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$("input[data-name='sidebar_read_and_unread'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$(".user-remove[data-pk='"+guest.data("pk")+"']").hide();
}
function handleListServerResponse (data, disableButtons) {
$("#flash_success").remove(); $("#flash_success").remove();
$("#flash_danger").remove(); $("#flash_danger").remove();
if (!jQuery.isEmptyObject(data)) {
$( ".navbar" ).after( '<div class="row-fluid text-center" style="margin-top: -20px;">' + $( ".navbar" ).after( '<div class="row-fluid text-center" style="margin-top: -20px;">' +
'<div id="flash_'+data.type+'" class="alert alert-'+data.type+'">'+data.message+'</div>' + '<div id="flash_'+data.type+'" class="alert alert-'+data.type+'">'+data.message+'</div>' +
'</div>'); '</div>');
@ -630,13 +653,28 @@ function checkboxChange(checkbox, userId, field, field_index) {
timeout: 900, timeout: 900,
success: function (data) { success: function (data) {
$("#user-table").bootstrapTable("load", data); $("#user-table").bootstrapTable("load", data);
if (disableButtons) {
deactivateHeaderButtons();
} }
}); loadSuccess();
} }
}); });
} }
function deactivateHeaderButtons(e) {
function checkboxChange(checkbox, userId, field, field_index) {
$.ajax({
method: "post",
url: window.location.pathname + "/../../ajax/editlistusers/" + field,
data: {"pk": userId, "field_index": field_index, "value": checkbox.checked},
error: function(data) {
handleListServerResponse({type:"danger", message:data.responseText})
},
success: handleListServerResponse
});
}
function deactivateHeaderButtons() {
$("#user_delete_selection").addClass("disabled"); $("#user_delete_selection").addClass("disabled");
$("#user_delete_selection").attr("aria-disabled", true); $("#user_delete_selection").attr("aria-disabled", true);
$(".check_head").attr("aria-disabled", true); $(".check_head").attr("aria-disabled", true);
@ -655,18 +693,10 @@ function selectHeader(element, field) {
method: "post", method: "post",
url: window.location.pathname + "/../../ajax/editlistusers/" + field, url: window.location.pathname + "/../../ajax/editlistusers/" + field,
data: {"pk": result, "value": element.value}, data: {"pk": result, "value": element.value},
success: function () { error: function (data) {
$.ajax({ handleListServerResponse({type:"danger", message:data.responseText})
method: "get", },
url: window.location.pathname + "/../../ajax/listusers", success: handleListServerResponse,
async: true,
timeout: 900,
success: function (data) {
$("#user-table").bootstrapTable("load", data);
deactivateHeaderButtons();
}
});
}
}); });
}); });
} }
@ -679,25 +709,12 @@ function checkboxHeader(CheckboxState, field, field_index) {
method: "post", method: "post",
url: window.location.pathname + "/../../ajax/editlistusers/" + field, url: window.location.pathname + "/../../ajax/editlistusers/" + field,
data: {"pk": result, "field_index": field_index, "value": CheckboxState}, data: {"pk": result, "field_index": field_index, "value": CheckboxState},
success: function () { error: function (data) {
$.ajax({ handleListServerResponse({type:"danger", message:data.responseText}, true)
method: "get", },
url: window.location.pathname + "/../../ajax/listusers",
async: true,
timeout: 900,
success: function (data) { success: function (data) {
$("#user-table").bootstrapTable("load", data); handleListServerResponse (data, true)
$("#user_delete_selection").addClass("disabled"); },
$("#user_delete_selection").attr("aria-disabled", true);
$(".check_head").attr("aria-disabled", true);
$(".check_head").attr("disabled", true);
$(".check_head").prop('checked', false);
$(".button_head").attr("aria-disabled", true);
$(".button_head").addClass("disabled");
$(".header_select").attr("disabled", true);
}
});
}
}); });
}); });
} }
@ -712,24 +729,10 @@ function deleteUser(a,b){
method:"post", method:"post",
url: window.location.pathname + "/../../ajax/deleteuser", url: window.location.pathname + "/../../ajax/deleteuser",
data: {"userid":b}, data: {"userid":b},
success:function(data) { success: handleListServerResponse,
$("#flash_success").remove(); error: function (data) {
$("#flash_danger").remove(); handleListServerResponse({type:"danger", message:data.responseText})
if (!jQuery.isEmptyObject(data)) { },
$( ".navbar" ).after( '<div class="row-fluid text-center" style="margin-top: -20px;">' +
'<div id="flash_'+data.type+'" class="alert alert-'+data.type+'">'+data.message+'</div>' +
'</div>');
}
$.ajax({
method: "get",
url: window.location.pathname + "/../../ajax/listusers",
async: true,
timeout: 900,
success: function (data) {
$("#user-table").bootstrapTable("load", data);
}
});
}
}); });
} }
); );
@ -741,6 +744,6 @@ function queryParams(params)
return params; return params;
} }
function test(){ function storeLocation() {
console.log("hello"); window.sessionStorage.setItem("back", window.location.pathname);
} }

View File

@ -26,7 +26,7 @@
{% for user in allUser %} {% for user in allUser %}
{% if not user.role_anonymous() or config.config_anonbrowse %} {% if not user.role_anonymous() or config.config_anonbrowse %}
<tr> <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.email}}</td>
<td>{{user.kindle_mail}}</td> <td>{{user.kindle_mail}}</td>
<td>{{user.downloads.count()}}</td> <td>{{user.downloads.count()}}</td>

View File

@ -47,13 +47,13 @@
{{ text_table_row('title', _('Enter Title'),_('Title'), true, true) }} {{ text_table_row('title', _('Enter Title'),_('Title'), true, true) }}
{{ text_table_row('sort', _('Enter Title Sort'),_('Title Sort'), false, 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('author_sort', _('Enter Author Sort'),_('Author Sort'), false, true) }}
{{ text_table_row('authors', _('Enter Authors'),_('Authors'), true) }} {{ text_table_row('authors', _('Enter Authors'),_('Authors'), true, true) }}
{{ text_table_row('tags', _('Enter Categories'),_('Categories'), false, false) }} {{ text_table_row('tags', _('Enter Categories'),_('Categories'), false, true) }}
{{ text_table_row('series', _('Enter Series'),_('Series'), false, false) }} {{ 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> <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--> <!--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()%} {% if g.user.role_delete_books() and g.user.role_edit()%}
<th data-align="right" data-formatter="EbookActions" data-switchable="false">{{_('Delete')}}</th> <th data-align="right" data-formatter="EbookActions" data-switchable="false">{{_('Delete')}}</th>
{% endif %} {% endif %}

View File

@ -12,7 +12,7 @@
<label for="mail_server_type">{{_('Choose Server Type')}}</label> <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"> <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="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> </select>
</div> </div>
<div data-related="email-settings-1"> <div data-related="email-settings-1">
@ -20,7 +20,7 @@
{% if content.mail_gmail_token == {} %} {% 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> <button type="submit" id="gmail_server" name="gmail" value="submit" class="btn btn-default">{{_('Setup Gmail Account as E-Mail Server')}}</button>
{% else %} {% 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 %} {% endif %}
</div> </div>
</div> </div>
@ -66,7 +66,7 @@
{% if feature_support['gmail'] %} {% if feature_support['gmail'] %}
</div> </div>
{% endif %} {% 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> </form>
{% if g.allow_registration %} {% if g.allow_registration %}
<div class="col-md-10 col-lg-6"> <div class="col-md-10 col-lg-6">

View File

@ -60,12 +60,12 @@
<a id="fullscreen" class="icon-resize-full">Fullscreen</a> <a id="fullscreen" class="icon-resize-full">Fullscreen</a>
</div> </div>
<div id="progress" role="progressbar" class="loading"> <div id="progress" role="progressbar" class="loading">
<div class="bar-load"> <div class="bar-load from-left">
<div class="text load"> <div class="text load">
Loading... Loading...
</div> </div>
</div> </div>
<div class="bar-read"> <div class="bar-read from-left">
<div class="text page"></div> <div class="text page"></div>
</div> </div>
</div> </div>

View File

@ -129,7 +129,7 @@
<div class="col-sm-12"> <div class="col-sm-12">
<div id="user_submit" class="btn btn-default">{{_('Save')}}</div> <div id="user_submit" class="btn btn-default">{{_('Save')}}</div>
{% if not profile %} {% 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 %} {% endif %}
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %} {% 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> <div class="btn btn-danger" id="btndeluser" data-value="{{ content.id }}" data-remote="false" >{{_('Delete User')}}</div>

View File

@ -24,15 +24,17 @@
data-column="{{value.get(array_field)}}" data-column="{{value.get(array_field)}}"
data-formatter="checkboxFormatter"> data-formatter="checkboxFormatter">
<div class="form-check"> <div class="form-check">
<div>
<label> <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> </label>
</div> </div>
<div class="form-check"> <div>
<label> <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> </label>
</div> </div>
</div>
{{show_text}} {{show_text}}
</th> </th>
{%- endmacro %} {%- endmacro %}
@ -48,14 +50,13 @@
data-editable-source={{url}} data-editable-source={{url}}
{% if validate %}data-edit-validate="{{ _('This Field is Required') }}"{% endif %}> {% if validate %}data-edit-validate="{{ _('This Field is Required') }}"{% endif %}>
<div> <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> <option value="all">{{ _('Show All') }}</option>
{% for language in languages %} {% for language in languages %}
<option value="{{language.lang_code}}">{{language.name}}</option> <option value="{{language.lang_code}}">{{language.name}}</option>
{% endfor %} {% endfor %}
</select> </select>
</div><br> </div>
{{ show_text }} {{ show_text }}
</th> </th>
{%- endmacro %} {%- endmacro %}
@ -71,13 +72,13 @@
data-editable-source={{url}} data-editable-source={{url}}
{% if validate %}data-edit-validate="{{ _('This Field is Required') }}"{% endif %}> {% if validate %}data-edit-validate="{{ _('This Field is Required') }}"{% endif %}>
<div> <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> <option value="None">{{_('Select...')}}</option>
{% for translation in translations %} {% for translation in translations %}
<option value="{{translation}}">{{translation.display_name|capitalize}}</option> <option value="{{translation}}">{{translation.display_name|capitalize}}</option>
{% endfor %} {% endfor %}
</select> </select>
</div><br> </div>
{{ show_text }} {{ show_text }}
</th> </th>
{%- endmacro %} {%- endmacro %}
@ -137,6 +138,9 @@
</tr> </tr>
</thead> </thead>
</table> </table>
<div class="errorlink">
<div class="btn btn-default" data-back="{{ url_for('admin.admin') }}" id="back">{{_('Back')}}</div>
</div>
{% endblock %} {% endblock %}
{% block modal %} {% block modal %}
{{ delete_confirm_modal() }} {{ delete_confirm_modal() }}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -75,8 +75,9 @@ def load_user_from_auth_header(header_val):
basic_username = basic_password = '' # nosec basic_username = basic_password = '' # nosec
try: try:
header_val = base64.b64decode(header_val).decode('utf-8') header_val = base64.b64decode(header_val).decode('utf-8')
basic_username = header_val.split(':')[0] # Users with colon are invalid: rfc7617 page 4
basic_password = header_val.split(':')[1] basic_username = header_val.split(':', 1)[0]
basic_password = header_val.split(':', 1)[1]
except (TypeError, UnicodeDecodeError, binascii.Error): except (TypeError, UnicodeDecodeError, binascii.Error):
pass pass
user = _fetch_user_by_name(basic_username) user = _fetch_user_by_name(basic_username)

View File

@ -760,10 +760,26 @@ def list_books():
sort = request.args.get("sort", "id") sort = request.args.get("sort", "id")
order = request.args.get("order", "").lower() order = request.args.get("order", "").lower()
state = None state = None
join = tuple()
if sort == "state": if sort == "state":
state = json.loads(request.args.get("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)] order = [text(sort + " " + order)]
elif not state: elif not state:
order = [db.Books.timestamp.desc()] 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() books = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).all()
entries = calibre_db.get_checkbox_sorted(books, state, off, limit,order) entries = calibre_db.get_checkbox_sorted(books, state, off, limit,order)
elif search: 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: 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 entry in entries:
for index in range(0, len(entry.languages)): for index in range(0, len(entry.languages)):

File diff suppressed because it is too large Load Diff