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

This commit is contained in:
bodybybuddha 2018-08-28 09:56:08 -04:00
commit 3ec000ad85
42 changed files with 37648 additions and 3392 deletions

View File

@ -26,6 +26,7 @@ except ImportError:
pass pass
import web import web
import server import server
import random
try: try:
import unidecode import unidecode
@ -76,10 +77,27 @@ def make_mobi(book_id, calibrepath, user_id, kindle_mail):
def send_test_mail(kindle_mail, user_name): def send_test_mail(kindle_mail, user_name):
global_WorkerThread.add_email(_(u'Calibre-web test email'),None, None, ub.get_mail_settings(), global_WorkerThread.add_email(_(u'Calibre-Web test e-mail'),None, None, ub.get_mail_settings(),
kindle_mail, user_name, _(u"Test E-Mail")) kindle_mail, user_name, _(u"Test e-mail"))
return return
# Send registration email or password reset email, depending on parameter resend (False means welcome email)
def send_registration_mail(e_mail, user_name, default_password, resend=False):
text = "Hello %s!\r\n" % user_name
if not resend:
text += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n"
text += "Please log in to your account using the following informations:\r\n"
text += "User name: %s\n" % user_name
text += "Password: %s\r\n" % default_password
text += "Don't forget to change your password after first login.\r\n"
text += "Sincerely\r\n\r\n"
text += "Your Calibre-Web team"
global_WorkerThread.add_email(_(u'Get Started with Calibre-Web'),None, None, ub.get_mail_settings(),
e_mail, user_name, _(u"Registration e-mail for user: %s" % user_name),text)
return
# Files are processed in the following order/priority: # Files are processed in the following order/priority:
# 1: If Mobi file is exisiting, it's directly send to kindle email, # 1: If Mobi file is exisiting, it's directly send to kindle email,
# 2: If Epub file is exisiting, it's converted and send to kindle email # 2: If Epub file is exisiting, it's converted and send to kindle email
@ -99,7 +117,7 @@ def send_mail(book_id, kindle_mail, calibrepath, user_id):
formats["pdf"] = entry.name + ".pdf" formats["pdf"] = entry.name + ".pdf"
if len(formats) == 0: if len(formats) == 0:
return _(u"Could not find any formats suitable for sending by email") return _(u"Could not find any formats suitable for sending by e-mail")
if 'mobi' in formats: if 'mobi' in formats:
result = formats['mobi'] result = formats['mobi']
@ -109,7 +127,7 @@ def send_mail(book_id, kindle_mail, calibrepath, user_id):
elif 'pdf' in formats: elif 'pdf' in formats:
result = formats['pdf'] # worker.get_attachment() result = formats['pdf'] # worker.get_attachment()
else: else:
return _(u"Could not find any formats suitable for sending by email") return _(u"Could not find any formats suitable for sending by e-mail")
if result: if result:
global_WorkerThread.add_email(_(u"Send to Kindle"), book.path, result, ub.get_mail_settings(), global_WorkerThread.add_email(_(u"Send to Kindle"), book.path, result, ub.get_mail_settings(),
kindle_mail, user_id, _(u"E-Mail: %s" % book.title)) kindle_mail, user_id, _(u"E-Mail: %s" % book.title))
@ -271,6 +289,11 @@ def delete_book_gdrive(book, book_format):
error =_(u'Book path %s not found on Google Drive' % book.path) # file not found error =_(u'Book path %s not found on Google Drive' % book.path) # file not found
return error return error
def generate_random_password():
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
passlen = 8
return "".join(random.sample(s,passlen ))
################################## External interface ################################## External interface
def update_dir_stucture(book_id, calibrepath): def update_dir_stucture(book_id, calibrepath):

View File

@ -69,7 +69,7 @@ class server:
self.wsgiserver.close(True) self.wsgiserver.close(True)
if self.restart == True: if self.restart == True:
web.app.logger.info("Performing restart of Calibre-web") web.app.logger.info("Performing restart of Calibre-Web")
web.helper.global_WorkerThread.stop() web.helper.global_WorkerThread.stop()
if os.name == 'nt': if os.name == 'nt':
arguments = ["\"" + sys.executable + "\""] arguments = ["\"" + sys.executable + "\""]
@ -79,7 +79,7 @@ class server:
else: else:
os.execl(sys.executable, sys.executable, *sys.argv) os.execl(sys.executable, sys.executable, *sys.argv)
else: else:
web.app.logger.info("Performing shutdown of Calibre-web") web.app.logger.info("Performing shutdown of Calibre-Web")
web.helper.global_WorkerThread.stop() web.helper.global_WorkerThread.stop()
sys.exit(0) sys.exit(0)

View File

@ -0,0 +1,663 @@
/*! X-editable - v1.5.3
* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
* http://github.com/vitalets/x-editable
* Copyright (c) 2015 Vitaliy Potapov; Licensed MIT */
.editableform {
margin-bottom: 0; /* overwrites bootstrap margin */
}
.editableform .control-group {
margin-bottom: 0; /* overwrites bootstrap margin */
white-space: nowrap; /* prevent wrapping buttons on new line */
line-height: 20px; /* overwriting bootstrap line-height. See #133 */
}
/*
BS3 width:1005 for inputs breaks editable form in popup
See: https://github.com/vitalets/x-editable/issues/393
*/
.editableform .form-control {
width: auto;
}
.editable-buttons {
display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */
vertical-align: top;
margin-left: 7px;
/* inline-block emulation for IE7*/
zoom: 1;
*display: inline;
}
.editable-buttons.editable-buttons-bottom {
display: block;
margin-top: 7px;
margin-left: 0;
}
.editable-input {
vertical-align: top;
display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */
width: auto; /* bootstrap-responsive has width: 100% that breakes layout */
white-space: normal; /* reset white-space decalred in parent*/
/* display-inline emulation for IE7*/
zoom: 1;
*display: inline;
}
.editable-buttons .editable-cancel {
margin-left: 7px;
}
/*for jquery-ui buttons need set height to look more pretty*/
.editable-buttons button.ui-button-icon-only {
height: 24px;
width: 30px;
}
.editableform-loading {
background: url('../img/loading.gif') center center no-repeat;
height: 25px;
width: auto;
min-width: 25px;
}
.editable-inline .editableform-loading {
background-position: left 5px;
}
.editable-error-block {
max-width: 300px;
margin: 5px 0 0 0;
width: auto;
white-space: normal;
}
/*add padding for jquery ui*/
.editable-error-block.ui-state-error {
padding: 3px;
}
.editable-error {
color: red;
}
/* ---- For specific types ---- */
.editableform .editable-date {
padding: 0;
margin: 0;
float: left;
}
/* move datepicker icon to center of add-on button. See https://github.com/vitalets/x-editable/issues/183 */
.editable-inline .add-on .icon-th {
margin-top: 3px;
margin-left: 1px;
}
/* checklist vertical alignment */
.editable-checklist label input[type="checkbox"],
.editable-checklist label span {
vertical-align: middle;
margin: 0;
}
.editable-checklist label {
white-space: nowrap;
}
/* set exact width of textarea to fit buttons toolbar */
.editable-wysihtml5 {
width: 566px;
height: 250px;
}
/* clear button shown as link in date inputs */
.editable-clear {
clear: both;
font-size: 0.9em;
text-decoration: none;
text-align: right;
}
/* IOS-style clear button for text inputs */
.editable-clear-x {
background: url('../img/clear.png') center center no-repeat;
display: block;
width: 13px;
height: 13px;
position: absolute;
opacity: 0.6;
z-index: 100;
top: 50%;
right: 6px;
margin-top: -6px;
}
.editable-clear-x:hover {
opacity: 1;
}
.editable-pre-wrapped {
white-space: pre-wrap;
}
.editable-container.editable-popup {
max-width: none !important; /* without this rule poshytip/tooltip does not stretch */
}
.editable-container.popover {
width: auto; /* without this rule popover does not stretch */
}
.editable-container.editable-inline {
display: inline-block;
vertical-align: middle;
width: auto;
/* inline-block emulation for IE7*/
zoom: 1;
*display: inline;
}
.editable-container.ui-widget {
font-size: inherit; /* jqueryui widget font 1.1em too big, overwrite it */
z-index: 9990; /* should be less than select2 dropdown z-index to close dropdown first when click */
}
.editable-click,
a.editable-click,
a.editable-click:hover {
text-decoration: none;
border-bottom: dashed 1px #0088cc;
}
.editable-click.editable-disabled,
a.editable-click.editable-disabled,
a.editable-click.editable-disabled:hover {
color: #585858;
cursor: default;
border-bottom: none;
}
.editable-empty, .editable-empty:hover, .editable-empty:focus{
font-style: italic;
color: #DD1144;
/* border-bottom: none; */
text-decoration: none;
}
.editable-unsaved {
font-weight: bold;
}
.editable-unsaved:after {
/* content: '*'*/
}
.editable-bg-transition {
-webkit-transition: background-color 1400ms ease-out;
-moz-transition: background-color 1400ms ease-out;
-o-transition: background-color 1400ms ease-out;
-ms-transition: background-color 1400ms ease-out;
transition: background-color 1400ms ease-out;
}
/*see https://github.com/vitalets/x-editable/issues/139 */
.form-horizontal .editable
{
padding-top: 5px;
display:inline-block;
}
/*!
* Datepicker for Bootstrap
*
* Copyright 2012 Stefan Petre
* Improvements by Andrew Rowls
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
*/
.datepicker {
padding: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
direction: ltr;
/*.dow {
border-top: 1px solid #ddd !important;
}*/
}
.datepicker-inline {
width: 220px;
}
.datepicker.datepicker-rtl {
direction: rtl;
}
.datepicker.datepicker-rtl table tr td span {
float: right;
}
.datepicker-dropdown {
top: 0;
left: 0;
}
.datepicker-dropdown:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #ccc;
border-bottom-color: rgba(0, 0, 0, 0.2);
position: absolute;
top: -7px;
left: 6px;
}
.datepicker-dropdown:after {
content: '';
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #ffffff;
position: absolute;
top: -6px;
left: 7px;
}
.datepicker > div {
display: none;
}
.datepicker.days div.datepicker-days {
display: block;
}
.datepicker.months div.datepicker-months {
display: block;
}
.datepicker.years div.datepicker-years {
display: block;
}
.datepicker table {
margin: 0;
}
.datepicker td,
.datepicker th {
text-align: center;
width: 20px;
height: 20px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
border: none;
}
.table-striped .datepicker table tr td,
.table-striped .datepicker table tr th {
background-color: transparent;
}
.datepicker table tr td.day:hover {
background: #eeeeee;
cursor: pointer;
}
.datepicker table tr td.old,
.datepicker table tr td.new {
color: #999999;
}
.datepicker table tr td.disabled,
.datepicker table tr td.disabled:hover {
background: none;
color: #999999;
cursor: default;
}
.datepicker table tr td.today,
.datepicker table tr td.today:hover,
.datepicker table tr td.today.disabled,
.datepicker table tr td.today.disabled:hover {
background-color: #fde19a;
background-image: -moz-linear-gradient(top, #fdd49a, #fdf59a);
background-image: -ms-linear-gradient(top, #fdd49a, #fdf59a);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fdd49a), to(#fdf59a));
background-image: -webkit-linear-gradient(top, #fdd49a, #fdf59a);
background-image: -o-linear-gradient(top, #fdd49a, #fdf59a);
background-image: linear-gradient(top, #fdd49a, #fdf59a);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0);
border-color: #fdf59a #fdf59a #fbed50;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #000;
}
.datepicker table tr td.today:hover,
.datepicker table tr td.today:hover:hover,
.datepicker table tr td.today.disabled:hover,
.datepicker table tr td.today.disabled:hover:hover,
.datepicker table tr td.today:active,
.datepicker table tr td.today:hover:active,
.datepicker table tr td.today.disabled:active,
.datepicker table tr td.today.disabled:hover:active,
.datepicker table tr td.today.active,
.datepicker table tr td.today:hover.active,
.datepicker table tr td.today.disabled.active,
.datepicker table tr td.today.disabled:hover.active,
.datepicker table tr td.today.disabled,
.datepicker table tr td.today:hover.disabled,
.datepicker table tr td.today.disabled.disabled,
.datepicker table tr td.today.disabled:hover.disabled,
.datepicker table tr td.today[disabled],
.datepicker table tr td.today:hover[disabled],
.datepicker table tr td.today.disabled[disabled],
.datepicker table tr td.today.disabled:hover[disabled] {
background-color: #fdf59a;
}
.datepicker table tr td.today:active,
.datepicker table tr td.today:hover:active,
.datepicker table tr td.today.disabled:active,
.datepicker table tr td.today.disabled:hover:active,
.datepicker table tr td.today.active,
.datepicker table tr td.today:hover.active,
.datepicker table tr td.today.disabled.active,
.datepicker table tr td.today.disabled:hover.active {
background-color: #fbf069 \9;
}
.datepicker table tr td.today:hover:hover {
color: #000;
}
.datepicker table tr td.today.active:hover {
color: #fff;
}
.datepicker table tr td.range,
.datepicker table tr td.range:hover,
.datepicker table tr td.range.disabled,
.datepicker table tr td.range.disabled:hover {
background: #eeeeee;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
}
.datepicker table tr td.range.today,
.datepicker table tr td.range.today:hover,
.datepicker table tr td.range.today.disabled,
.datepicker table tr td.range.today.disabled:hover {
background-color: #f3d17a;
background-image: -moz-linear-gradient(top, #f3c17a, #f3e97a);
background-image: -ms-linear-gradient(top, #f3c17a, #f3e97a);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f3c17a), to(#f3e97a));
background-image: -webkit-linear-gradient(top, #f3c17a, #f3e97a);
background-image: -o-linear-gradient(top, #f3c17a, #f3e97a);
background-image: linear-gradient(top, #f3c17a, #f3e97a);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0);
border-color: #f3e97a #f3e97a #edde34;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
}
.datepicker table tr td.range.today:hover,
.datepicker table tr td.range.today:hover:hover,
.datepicker table tr td.range.today.disabled:hover,
.datepicker table tr td.range.today.disabled:hover:hover,
.datepicker table tr td.range.today:active,
.datepicker table tr td.range.today:hover:active,
.datepicker table tr td.range.today.disabled:active,
.datepicker table tr td.range.today.disabled:hover:active,
.datepicker table tr td.range.today.active,
.datepicker table tr td.range.today:hover.active,
.datepicker table tr td.range.today.disabled.active,
.datepicker table tr td.range.today.disabled:hover.active,
.datepicker table tr td.range.today.disabled,
.datepicker table tr td.range.today:hover.disabled,
.datepicker table tr td.range.today.disabled.disabled,
.datepicker table tr td.range.today.disabled:hover.disabled,
.datepicker table tr td.range.today[disabled],
.datepicker table tr td.range.today:hover[disabled],
.datepicker table tr td.range.today.disabled[disabled],
.datepicker table tr td.range.today.disabled:hover[disabled] {
background-color: #f3e97a;
}
.datepicker table tr td.range.today:active,
.datepicker table tr td.range.today:hover:active,
.datepicker table tr td.range.today.disabled:active,
.datepicker table tr td.range.today.disabled:hover:active,
.datepicker table tr td.range.today.active,
.datepicker table tr td.range.today:hover.active,
.datepicker table tr td.range.today.disabled.active,
.datepicker table tr td.range.today.disabled:hover.active {
background-color: #efe24b \9;
}
.datepicker table tr td.selected,
.datepicker table tr td.selected:hover,
.datepicker table tr td.selected.disabled,
.datepicker table tr td.selected.disabled:hover {
background-color: #9e9e9e;
background-image: -moz-linear-gradient(top, #b3b3b3, #808080);
background-image: -ms-linear-gradient(top, #b3b3b3, #808080);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#b3b3b3), to(#808080));
background-image: -webkit-linear-gradient(top, #b3b3b3, #808080);
background-image: -o-linear-gradient(top, #b3b3b3, #808080);
background-image: linear-gradient(top, #b3b3b3, #808080);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0);
border-color: #808080 #808080 #595959;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.selected:hover,
.datepicker table tr td.selected:hover:hover,
.datepicker table tr td.selected.disabled:hover,
.datepicker table tr td.selected.disabled:hover:hover,
.datepicker table tr td.selected:active,
.datepicker table tr td.selected:hover:active,
.datepicker table tr td.selected.disabled:active,
.datepicker table tr td.selected.disabled:hover:active,
.datepicker table tr td.selected.active,
.datepicker table tr td.selected:hover.active,
.datepicker table tr td.selected.disabled.active,
.datepicker table tr td.selected.disabled:hover.active,
.datepicker table tr td.selected.disabled,
.datepicker table tr td.selected:hover.disabled,
.datepicker table tr td.selected.disabled.disabled,
.datepicker table tr td.selected.disabled:hover.disabled,
.datepicker table tr td.selected[disabled],
.datepicker table tr td.selected:hover[disabled],
.datepicker table tr td.selected.disabled[disabled],
.datepicker table tr td.selected.disabled:hover[disabled] {
background-color: #808080;
}
.datepicker table tr td.selected:active,
.datepicker table tr td.selected:hover:active,
.datepicker table tr td.selected.disabled:active,
.datepicker table tr td.selected.disabled:hover:active,
.datepicker table tr td.selected.active,
.datepicker table tr td.selected:hover.active,
.datepicker table tr td.selected.disabled.active,
.datepicker table tr td.selected.disabled:hover.active {
background-color: #666666 \9;
}
.datepicker table tr td.active,
.datepicker table tr td.active:hover,
.datepicker table tr td.active.disabled,
.datepicker table tr td.active.disabled:hover {
background-color: #006dcc;
background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
background-image: -o-linear-gradient(top, #0088cc, #0044cc);
background-image: linear-gradient(top, #0088cc, #0044cc);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
border-color: #0044cc #0044cc #002a80;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.active:hover,
.datepicker table tr td.active:hover:hover,
.datepicker table tr td.active.disabled:hover,
.datepicker table tr td.active.disabled:hover:hover,
.datepicker table tr td.active:active,
.datepicker table tr td.active:hover:active,
.datepicker table tr td.active.disabled:active,
.datepicker table tr td.active.disabled:hover:active,
.datepicker table tr td.active.active,
.datepicker table tr td.active:hover.active,
.datepicker table tr td.active.disabled.active,
.datepicker table tr td.active.disabled:hover.active,
.datepicker table tr td.active.disabled,
.datepicker table tr td.active:hover.disabled,
.datepicker table tr td.active.disabled.disabled,
.datepicker table tr td.active.disabled:hover.disabled,
.datepicker table tr td.active[disabled],
.datepicker table tr td.active:hover[disabled],
.datepicker table tr td.active.disabled[disabled],
.datepicker table tr td.active.disabled:hover[disabled] {
background-color: #0044cc;
}
.datepicker table tr td.active:active,
.datepicker table tr td.active:hover:active,
.datepicker table tr td.active.disabled:active,
.datepicker table tr td.active.disabled:hover:active,
.datepicker table tr td.active.active,
.datepicker table tr td.active:hover.active,
.datepicker table tr td.active.disabled.active,
.datepicker table tr td.active.disabled:hover.active {
background-color: #003399 \9;
}
.datepicker table tr td span {
display: block;
width: 23%;
height: 54px;
line-height: 54px;
float: left;
margin: 1%;
cursor: pointer;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.datepicker table tr td span:hover {
background: #eeeeee;
}
.datepicker table tr td span.disabled,
.datepicker table tr td span.disabled:hover {
background: none;
color: #999999;
cursor: default;
}
.datepicker table tr td span.active,
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active.disabled,
.datepicker table tr td span.active.disabled:hover {
background-color: #006dcc;
background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
background-image: -o-linear-gradient(top, #0088cc, #0044cc);
background-image: linear-gradient(top, #0088cc, #0044cc);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
border-color: #0044cc #0044cc #002a80;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active:hover:hover,
.datepicker table tr td span.active.disabled:hover,
.datepicker table tr td span.active.disabled:hover:hover,
.datepicker table tr td span.active:active,
.datepicker table tr td span.active:hover:active,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.active,
.datepicker table tr td span.active:hover.active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active.disabled:hover.active,
.datepicker table tr td span.active.disabled,
.datepicker table tr td span.active:hover.disabled,
.datepicker table tr td span.active.disabled.disabled,
.datepicker table tr td span.active.disabled:hover.disabled,
.datepicker table tr td span.active[disabled],
.datepicker table tr td span.active:hover[disabled],
.datepicker table tr td span.active.disabled[disabled],
.datepicker table tr td span.active.disabled:hover[disabled] {
background-color: #0044cc;
}
.datepicker table tr td span.active:active,
.datepicker table tr td span.active:hover:active,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.active,
.datepicker table tr td span.active:hover.active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active.disabled:hover.active {
background-color: #003399 \9;
}
.datepicker table tr td span.old,
.datepicker table tr td span.new {
color: #999999;
}
.datepicker th.datepicker-switch {
width: 145px;
}
.datepicker thead tr:first-child th,
.datepicker tfoot tr th {
cursor: pointer;
}
.datepicker thead tr:first-child th:hover,
.datepicker tfoot tr th:hover {
background: #eeeeee;
}
.datepicker .cw {
font-size: 10px;
width: 12px;
padding: 0 2px 0 5px;
vertical-align: middle;
}
.datepicker thead tr:first-child th.cw {
cursor: default;
background-color: transparent;
}
.input-append.date .add-on i,
.input-prepend.date .add-on i {
display: block;
cursor: pointer;
width: 16px;
height: 16px;
}
.input-daterange input {
text-align: center;
}
.input-daterange input:first-child {
-webkit-border-radius: 3px 0 0 3px;
-moz-border-radius: 3px 0 0 3px;
border-radius: 3px 0 0 3px;
}
.input-daterange input:last-child {
-webkit-border-radius: 0 3px 3px 0;
-moz-border-radius: 0 3px 3px 0;
border-radius: 0 3px 3px 0;
}
.input-daterange .add-on {
display: inline-block;
width: auto;
min-width: 16px;
height: 18px;
padding: 4px 5px;
font-weight: normal;
line-height: 18px;
text-align: center;
text-shadow: 0 1px 0 #ffffff;
vertical-align: middle;
background-color: #eeeeee;
border: 1px solid #ccc;
margin-left: -5px;
margin-right: -5px;
}

View File

@ -94,7 +94,13 @@ input.pill:not(:checked) + label .glyphicon {
.upload-format-input-text {display: initial;} .upload-format-input-text {display: initial;}
#btn-upload-format {display: none;} #btn-upload-format {display: none;}
.upload-format-input-text {display: initial;} .upload-cover-input-text {display: initial;}
#btn-upload-format {display: none;} #btn-upload-cover {display: none;}
.panel-title > a { text-decoration: none;} .panel-title > a { text-decoration: none;}
.editable-buttons { display:inline-block; margin-left: 7px ;}
.editable-input { display:inline-block;}
.editable-cancel { margin-bottom: 0px !important; margin-left: 7px !important;}
.editable-submit { margin-bottom: 0px !important;}

View File

@ -246,3 +246,11 @@ $("#btn-upload-format").on("change", function () {
} // Remove c:\fake at beginning from localhost chrome } // Remove c:\fake at beginning from localhost chrome
$("#upload-format").html(filename); $("#upload-format").html(filename);
}); });
$("#btn-upload-cover").on("change", function () {
var filename = $(this).val();
if (filename.substring(3, 11) === "fakepath") {
filename = filename.substring(12);
} // Remove c:\fake at beginning from localhost chrome
$("#upload-cover").html(filename);
});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,7 @@
/*
* bootstrap-table - v1.12.1 - 2018-03-12
* https://github.com/wenzhixin/bootstrap-table
* Copyright (c) 2018 zhixin wen
* Licensed MIT License
*/
!function(a){"use strict";a.extend(a.fn.bootstrapTable.defaults,{editable:!0,onEditableInit:function(){return!1},onEditableSave:function(){return!1},onEditableShown:function(){return!1},onEditableHidden:function(){return!1}}),a.extend(a.fn.bootstrapTable.Constructor.EVENTS,{"editable-init.bs.table":"onEditableInit","editable-save.bs.table":"onEditableSave","editable-shown.bs.table":"onEditableShown","editable-hidden.bs.table":"onEditableHidden"});var b=a.fn.bootstrapTable.Constructor,c=b.prototype.initTable,d=b.prototype.initBody;b.prototype.initTable=function(){var b=this;c.apply(this,Array.prototype.slice.apply(arguments)),this.options.editable&&a.each(this.columns,function(c,d){if(d.editable){var e={},f=[],g="editable-",h=function(a,b){var c=a.replace(/([A-Z])/g,function(a){return"-"+a.toLowerCase()});if(c.slice(0,g.length)==g){var d=c.replace(g,"data-");e[d]=b}};a.each(b.options,h),d.formatter=d.formatter||function(a){return a},d._formatter=d._formatter?d._formatter:d.formatter,d.formatter=function(c,g,i){var j=d._formatter?d._formatter(c,g,i):c;a.each(d,h),a.each(e,function(a,b){f.push(" "+a+'="'+b+'"')});var k=!1;return d.editable.hasOwnProperty("noeditFormatter")&&(k=d.editable.noeditFormatter(c,g,i)),k===!1?['<a href="javascript:void(0)"',' data-name="'+d.field+'"',' data-pk="'+g[b.options.idField]+'"',' data-value="'+j+'"',f.join(""),"></a>"].join(""):k}}})},b.prototype.initBody=function(){var b=this;d.apply(this,Array.prototype.slice.apply(arguments)),this.options.editable&&(a.each(this.columns,function(c,d){d.editable&&(b.$body.find('a[data-name="'+d.field+'"]').editable(d.editable).off("save").on("save",function(c,e){var f=b.getData(),g=a(this).parents("tr[data-index]").data("index"),h=f[g],i=h[d.field];a(this).data("value",e.submitValue),h[d.field]=e.submitValue,b.trigger("editable-save",d.field,h,i,a(this)),b.resetFooter()}),b.$body.find('a[data-name="'+d.field+'"]').editable(d.editable).off("shown").on("shown",function(c,e){var f=b.getData(),g=a(this).parents("tr[data-index]").data("index"),h=f[g];b.trigger("editable-shown",d.field,h,a(this),e)}),b.$body.find('a[data-name="'+d.field+'"]').editable(d.editable).off("hidden").on("hidden",function(c,e){var f=b.getData(),g=a(this).parents("tr[data-index]").data("index"),h=f[g];b.trigger("editable-hidden",d.field,h,a(this),e)}))}),this.trigger("editable-init"))}}(jQuery);

57
cps/static/js/table.js Normal file
View File

@ -0,0 +1,57 @@
$(function() {
$("#domain_submit").click(function(event){
event.preventDefault();
$("#domain_add").ajaxForm();
$(this).closest("form").submit();
$.ajax({
method:"get",
url: window.location.pathname + "/../../ajax/domainlist",
async: true,
timeout: 900,
success:function(data){
$('#domain-table').bootstrapTable("load", data);
}
});
});
$('#domain-table').bootstrapTable({
formatNoMatches: function () {
return '';
},
striped: false
});
$("#btndeletedomain").click(function() {
//get data-id attribute of the clicked element
var domainId = $(this).data('domainId');
$.ajax({
method:"post",
url: window.location.pathname + "/../../ajax/deletedomain",
data: {"domainid":domainId}
});
$('#DeleteDomain').modal('hide');
$.ajax({
method:"get",
url: window.location.pathname + "/../../ajax/domainlist",
async: true,
timeout: 900,
success:function(data){
$('#domain-table').bootstrapTable("load", data);
}
});
});
//triggered when modal is about to be shown
$('#DeleteDomain').on('show.bs.modal', function(e) {
//get data-id attribute of the clicked element and store in button
var domainId = $(e.relatedTarget).data('domain-id');
$(e.currentTarget).find("#btndeletedomain").data('domainId',domainId);
});
});
function TableActions (value, row, index) {
return [
'<a class="danger remove" data-toggle="modal" data-target="#DeleteDomain" data-domain-id="'+row.id+'" title="Remove">',
'<i class="glyphicon glyphicon-trash"></i>',
'</a>'
].join('');
}

View File

@ -2,7 +2,6 @@
{% block body %} {% block body %}
<div class="discover"> <div class="discover">
<h2>{{_('User list')}}</h2> <h2>{{_('User list')}}</h2>
<div id="divLoading"></div>
<table class="table table-striped" id="table_user"> <table class="table table-striped" id="table_user">
<tr> <tr>
<th>{{_('Nickname')}}</th> <th>{{_('Nickname')}}</th>
@ -13,7 +12,6 @@
<th>{{_('Download')}}</th> <th>{{_('Download')}}</th>
<th>{{_('Upload')}}</th> <th>{{_('Upload')}}</th>
<th>{{_('Edit')}}</th> <th>{{_('Edit')}}</th>
<th>{{_('Passwd')}}</th>
</tr> </tr>
{% for user in content %} {% for user in content %}
{% if not user.role_anonymous() or config.config_anonbrowse %} {% if not user.role_anonymous() or config.config_anonbrowse %}
@ -26,20 +24,18 @@
<td>{% if user.role_download() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td> <td>{% if user.role_download() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
<td>{% if user.role_upload() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td> <td>{% if user.role_upload() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
<td>{% if user.role_edit() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td> <td>{% if user.role_edit() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
<td>{% if user.role_passwd() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
</tr> </tr>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</table> </table>
<div class="btn btn-default" id="admin_new_user"><a href="{{url_for('new_user')}}">{{_('Add new user')}}</a></div> <div class="btn btn-default" id="admin_new_user"><a href="{{url_for('new_user')}}">{{_('Add new user')}}</a></div>
<h2>{{_('SMTP mail settings')}}</h2> <h2>{{_('SMTP e-mail server settings')}}</h2>
<table class="table table-striped" id="table_email"> <table class="table table-striped" id="table_email">
<tr> <tr>
<th>{{_('SMTP hostname')}}</th> <th>{{_('SMTP hostname')}}</th>
<th>{{_('SMTP port')}}</th> <th>{{_('SMTP port')}}</th>
<th>{{_('SSL')}}</th> <th>{{_('SSL')}}</th>
<th>{{_('SMTP login')}}</th> <th>{{_('SMTP login')}}</th>
<th>{{_('SMTP password')}}</th>
<th>{{_('From mail')}}</th> <th>{{_('From mail')}}</th>
</tr> </tr>
<tr> <tr>
@ -47,44 +43,56 @@
<td>{{email.mail_port}}</td> <td>{{email.mail_port}}</td>
<td>{% if email.mail_use_ssl %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td> <td>{% if email.mail_use_ssl %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
<td>{{email.mail_login}}</td> <td>{{email.mail_login}}</td>
<td>********</td>
<td>{{email.mail_from}}</td> <td>{{email.mail_from}}</td>
</table> </table>
<div class="btn btn-default" id="admin_edit_email"><a href="{{url_for('edit_mailsettings')}}">{{_('Change SMTP settings')}}</a></div> <div class="btn btn-default" id="admin_edit_email"><a href="{{url_for('edit_mailsettings')}}">{{_('Change SMTP settings')}}</a></div>
<div id="container">
<h2>{{_('Configuration')}}</h2> <h2>{{_('Configuration')}}</h2>
<table class="table table-striped" id="table_configuration"> <div class="col-xs-12 col-sm-6">
<tr> <div class="Row">
<th>{{_('Calibre DB dir')}}</th> <div class="col-xs-6 col-sm-6">{{_('Calibre DB dir')}}</div>
<th>{{_('Log Level')}}</th> <div class="col-xs-6 col-sm-6">{{config.config_calibre_dir}}</div>
<th>{{_('Port')}}</th> </div>
<th>{{_('Books per page')}}</th> <div class="Row">
<th>{{_('Uploading')}}</th> <div class="col-xs-6 col-sm-6">{{_('Log Level')}}</div>
<th>{{_('Public registration')}}</th> <div class="col-xs-6 col-sm-6">{{config.get_Log_Level()}}</div>
<th>{{_('Anonymous browsing')}}</th> </div>
<th>{{_('Remote Login')}}</th> <div class="Row">
</tr> <div class="col-xs-6 col-sm-6">{{_('Port')}}</div>
<tr> <div class="col-xs-6 col-sm-6">{{config.config_port}}</div>
<td>{{config.config_calibre_dir}}</td> </div>
<td>{{config.get_Log_Level()}}</td> </div>
<td>{{config.config_port}}</td> <div class="col-xs-12 col-sm-6">
<td>{{config.config_books_per_page}}</td> <div class="Row">
<td>{% if config.config_uploading %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td> <div class="col-xs-6 col-sm-7">{{_('Books per page')}}</div>
<td>{% if config.config_public_reg %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td> <div class="col-xs-6 col-sm-5">{{config.config_books_per_page}}</div>
<td>{% if config.config_anonbrowse %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td> </div>
<td>{% if config.config_remote_login %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td> <div class="Row">
</table> <div class="col-xs-6 col-sm-7">{{_('Uploading')}}</div>
<div class="col-xs-6 col-sm-5">{% if config.config_uploading %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
</div>
<div class="Row">
<div class="col-xs-6 col-sm-7">{{_('Public registration')}}</div>
<div class="col-xs-6 col-sm-5">{% if config.config_public_reg %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
</div>
<div class="Row">
<div class="col-xs-6 col-sm-7">{{_('Remote Login')}}</div>
<div class="col-xs-6 col-sm-5">{% if config.config_remote_login %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
</div>
</div>
</div>
<div class="col-xs-12 col-sm-12">
<p></p>
<div class="btn btn-default"><a href="{{url_for('configuration')}}">{{_('Basic Configuration')}}</a></div> <div class="btn btn-default"><a href="{{url_for('configuration')}}">{{_('Basic Configuration')}}</a></div>
<div class="btn btn-default"><a href="{{url_for('view_configuration')}}">{{_('UI Configuration')}}</a></div> <div class="btn btn-default"><a href="{{url_for('view_configuration')}}">{{_('UI Configuration')}}</a></div>
</div>
<h2>{{_('Administration')}}</h2> <h2>{{_('Administration')}}</h2>
<div>{{_('Current commit timestamp')}}: <span>{{commit}} </span></div> <div>{{_('Current commit timestamp')}}: <span>{{commit}} </span></div>
<div class="hidden" id="update_info">{{_('Newest commit timestamp')}}: <span></span></div> <div class="hidden" id="update_info">{{_('Newest commit timestamp')}}: <span></span></div>
<p></p> <p></p>
<div class="btn btn-default" id="restart_database">{{_('Reconnect to Calibre DB')}}</div> <div class="btn btn-default" id="restart_database">{{_('Reconnect to Calibre DB')}}</div>
<div class="btn btn-default" data-toggle="modal" data-target="#RestartDialog">{{_('Restart Calibre-web')}}</div> <div class="btn btn-default" data-toggle="modal" data-target="#RestartDialog">{{_('Restart Calibre-Web')}}</div>
<div class="btn btn-default" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-web')}}</div> <div class="btn btn-default" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-Web')}}</div>
<div class="btn btn-default" id="check_for_update">{{_('Check for update')}}</div> <div class="btn btn-default" id="check_for_update">{{_('Check for update')}}</div>
<div class="btn btn-default hidden" id="perform_update" data-toggle="modal" data-target="#UpdateprogressDialog">{{_('Perform Update')}}</div> <div class="btn btn-default hidden" id="perform_update" data-toggle="modal" data-target="#UpdateprogressDialog">{{_('Perform Update')}}</div>
</div> </div>
@ -95,7 +103,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header bg-info"></div> <div class="modal-header bg-info"></div>
<div class="modal-body text-center"> <div class="modal-body text-center">
<p>{{_('Do you really want to restart Calibre-web?')}}</p> <p>{{_('Do you really want to restart Calibre-Web?')}}</p>
<div id="spinner" class="spinner" style="display:none;"> <div id="spinner" class="spinner" style="display:none;">
<img id="img-spinner" src="/static/css/images/loading-icon.gif"/> <img id="img-spinner" src="/static/css/images/loading-icon.gif"/>
</div> </div>
@ -113,7 +121,7 @@
<div class="modal-header bg-info"> <div class="modal-header bg-info">
</div> </div>
<div class="modal-body text-center"> <div class="modal-body text-center">
<p>{{_('Do you really want to stop Calibre-web?')}}</p> <p>{{_('Do you really want to stop Calibre-Web?')}}</p>
<button type="button" class="btn btn-default" id="shutdown" data-dismiss="modal">{{_('Ok')}}</button> <button type="button" class="btn btn-default" id="shutdown" data-dismiss="modal">{{_('Ok')}}</button>
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Back')}}</button> <button type="button" class="btn btn-default" data-dismiss="modal">{{_('Back')}}</button>
</div> </div>

View File

@ -60,7 +60,11 @@
<label for="cover_url">{{_('Cover URL (jpg, cover is downloaded and stored in database, field is afterwards empty again)')}}</label> <label for="cover_url">{{_('Cover URL (jpg, cover is downloaded and stored in database, field is afterwards empty again)')}}</label>
<input type="text" class="form-control" name="cover_url" id="cover_url" value=""> <input type="text" class="form-control" name="cover_url" id="cover_url" value="">
</div> </div>
<div class="form-group" aria-label="Upload cover from local drive">
<label class="btn btn-default btn-file" for="btn-upload-cover">{{ _('Upload Cover from local drive') }}</label>
<div class="upload-cover-input-text" id="upload-cover"></div>
<input id="btn-upload-cover" name="btn-upload-cover" type="file">
</div>
<div class="form-group"> <div class="form-group">
<label for="pubdate">{{_('Publishing date')}}</label> <label for="pubdate">{{_('Publishing date')}}</label>
<div style="position: relative"> <div style="position: relative">
@ -70,7 +74,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="languages">{{_('Language')}}</label> <label for="languages">{{_('Language')}}</label>
<input type="text" class="form-control typeahead" name="languages" id="languages" value="{% for language in book.languages %}{{language.language_name.strip()}}, {% endfor %}"> <input type="text" class="form-control typeahead" name="languages" id="languages" value="{% for language in book.languages %}{{language.language_name.strip()}}{% endfor %}">
</div> </div>
{% if cc|length > 0 %} {% if cc|length > 0 %}
@ -170,7 +174,7 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header bg-danger text-center"> <div class="modal-header bg-danger text-center">
<span>{{_('Are really you sure?')}}</span> <span>{{_('Are you really sure?')}}</span>
</div> </div>
<div class="modal-body text-center"> <div class="modal-body text-center">
<span>{{_('Book will be deleted from Calibre database')}}</span> <span>{{_('Book will be deleted from Calibre database')}}</span>
@ -255,7 +259,7 @@
var i18nMsg = { var i18nMsg = {
'loading': {{_('Loading...')|safe|tojson}}, 'loading': {{_('Loading...')|safe|tojson}},
'search_error': {{_('Search error!')|safe|tojson}}, 'search_error': {{_('Search error!')|safe|tojson}},
'no_result': {{_('No Result! Please try anonther keyword.')|safe|tojson}}, 'no_result': {{_('No Result(s) found! Please try aonther keyword.')|safe|tojson}},
'author': {{_('Author')|safe|tojson}}, 'author': {{_('Author')|safe|tojson}},
'publisher': {{_('Publisher')|safe|tojson}}, 'publisher': {{_('Publisher')|safe|tojson}},
'description': {{_('Description')|safe|tojson}}, 'description': {{_('Description')|safe|tojson}},

View File

@ -1,4 +1,8 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block header %}
<link href="{{ url_for('static', filename='css/libs/bootstrap-table.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/libs/bootstrap-editable.css') }}" rel="stylesheet">
{% endblock %}
{% block body %} {% block body %}
<div class="discover"> <div class="discover">
<h1>{{title}}</h1> <h1>{{title}}</h1>
@ -35,6 +39,52 @@
<button type="submit" name="test" value="test" class="btn btn-default">{{_('Save settings and send Test E-Mail')}}</button> <button type="submit" name="test" value="test" class="btn btn-default">{{_('Save settings and send Test E-Mail')}}</button>
<a href="{{ url_for('admin') }}" id="back" class="btn btn-default">{{_('Back')}}</a> <a href="{{ url_for('admin') }}" id="back" class="btn btn-default">{{_('Back')}}</a>
</form> </form>
{% if g.allow_registration %}
<h2>{{_('Allowed domains for registering')}}</h2>
<table class="table table-no-bordered" id="domain-table" data-url="{{url_for('list_domain')}}" data-id-field="id" data-show-header="false" data-editable-mode="inline">
<thead>
<tr>
<th data-field="domain" id="domain" data-editable-type="text" data-editable-url="{{ url_for('edit_domain')}}" data-editable="true" data-editable-title="{{_('Enter domainname')}}"></th>
<th data-field="id" id="id" data-visible="false"></th>
<th data-align="right" data-formatter="TableActions"></th>
</tr>
</thead>
</table>
<form id="domain_add" action="{{ url_for('add_domain')}}" method="POST">
<div class="form-group required">
<label for="domainname">{{_('Add Domain')}}</label>
<input type="text" class="form-control" name="domainname" id="domainname" >
</div>
<button id="domain_submit" class="btn btn-default">{{_('Add')}}</button>
</form>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}
{% block modal %}
{% if g.allow_registration %}
<div id="DeleteDomain" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header bg-danger">
</div>
<div class="modal-body text-center">
<p>{{_('Do you really want to delete this domain rule?')}}</p>
<button type="button" class="btn btn-danger" id="btndeletedomain" >{{_('Delete')}}</button>
<!--a href="{{ url_for('delete_domain')}}" class="btn btn-danger">{{_('Delete')}}</a-->
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Back')}}</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block js %}
{% if g.allow_registration %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
{% endif %}
{% endblock %}

View File

@ -124,7 +124,7 @@
{% if g.user.show_sorted() %} {% if g.user.show_sorted() %}
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-sort-by-attributes"></span> {{_('Sorted Books')}} <span class="glyphicon glyphicon-sort-by-attributes"></span>{{_('Sorted Books')}}
<span class="caret"></span> <span class="caret"></span>
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
@ -136,44 +136,44 @@
</li> </li>
{%endif%} {%endif%}
{% if g.user.show_hot_books() %} {% if g.user.show_hot_books() %}
<li id="nav_hot" {% if page == 'hot' %}class="active"{% endif %}><a href="{{url_for('hot_books')}}"><span class="glyphicon glyphicon-fire"></span> {{_('Hot Books')}}</a></li> <li id="nav_hot" {% if page == 'hot' %}class="active"{% endif %}><a href="{{url_for('hot_books')}}"><span class="glyphicon glyphicon-fire"></span>{{_('Hot Books')}}</a></li>
{%endif%} {%endif%}
{% if g.user.show_best_rated_books() %} {% if g.user.show_best_rated_books() %}
<li {% if page == 'rated' %}class="active"{% endif %}><a href="{{url_for('best_rated_books')}}"><span class="glyphicon glyphicon-star"></span> {{_('Best rated Books')}}</a></li> <li {% if page == 'rated' %}class="active"{% endif %}><a href="{{url_for('best_rated_books')}}"><span class="glyphicon glyphicon-star"></span>{{_('Best rated Books')}}</a></li>
{%endif%} {%endif%}
{% if g.user.show_read_and_unread() %} {% if g.user.show_read_and_unread() %}
{% if not g.user.is_anonymous %} {% if not g.user.is_anonymous %}
<li {% if page == 'read' %}class="active"{% endif %}><a href="{{url_for('read_books')}}"><span class="glyphicon glyphicon-eye-open"></span> {{_('Read Books')}}</a></li> <li {% if page == 'read' %}class="active"{% endif %}><a href="{{url_for('read_books')}}"><span class="glyphicon glyphicon-eye-open"></span>{{_('Read Books')}}</a></li>
{%endif%} {%endif%}
<li {% if page == 'read' %}class="active"{% endif %}><a href="{{url_for('unread_books')}}"><span class="glyphicon glyphicon-eye-close"></span> {{_('Unread Books')}}</a></li> <li {% if page == 'read' %}class="active"{% endif %}><a href="{{url_for('unread_books')}}"><span class="glyphicon glyphicon-eye-close"></span>{{_('Unread Books')}}</a></li>
{%endif%} {%endif%}
{% if g.user.show_random_books() %} {% if g.user.show_random_books() %}
<li id="nav_rand" {% if page == 'discover' %}class="active"{% endif %}><a href="{{url_for('discover')}}"><span class="glyphicon glyphicon-random"></span> {{_('Discover')}}</a></li> <li id="nav_rand" {% if page == 'discover' %}class="active"{% endif %}><a href="{{url_for('discover')}}"><span class="glyphicon glyphicon-random"></span>{{_('Discover')}}</a></li>
{%endif%} {%endif%}
{% if g.user.show_category() %} {% if g.user.show_category() %}
<li id="nav_cat" {% if page == 'discover' %}class="category"{% endif %}><a href="{{url_for('category_list')}}"><span class="glyphicon glyphicon-inbox"></span> {{_('Categories')}}</a></li> <li id="nav_cat" {% if page == 'discover' %}class="category"{% endif %}><a href="{{url_for('category_list')}}"><span class="glyphicon glyphicon-inbox"></span>{{_('Categories')}}</a></li>
{%endif%} {%endif%}
{% if g.user.show_series() %} {% if g.user.show_series() %}
<li id="nav_serie" {% if page == 'series' %}class="active"{% endif %}><a href="{{url_for('series_list')}}"><span class="glyphicon glyphicon-bookmark"></span> {{_('Series')}}</a></li> <li id="nav_serie" {% if page == 'series' %}class="active"{% endif %}><a href="{{url_for('series_list')}}"><span class="glyphicon glyphicon-bookmark"></span>{{_('Series')}}</a></li>
{%endif%} {%endif%}
{% if g.user.show_author() %} {% if g.user.show_author() %}
<li id="nav_author" {% if page == 'author' %}class="active"{% endif %}><a href="{{url_for('author_list')}}"><span class="glyphicon glyphicon-user"></span> {{_('Authors')}}</a></li> <li id="nav_author" {% if page == 'author' %}class="active"{% endif %}><a href="{{url_for('author_list')}}"><span class="glyphicon glyphicon-user"></span>{{_('Authors')}}</a></li>
{%endif%} {%endif%}
{% if g.user.filter_language() == 'all' and g.user.show_language() %} {% if g.user.filter_language() == 'all' and g.user.show_language() %}
<li id="nav_lang" {% if page == 'language' %}class="active"{% endif %}><a href="{{url_for('language_overview')}}"><span class="glyphicon glyphicon-flag"></span> {{_('Languages')}} </a></li> <li id="nav_lang" {% if page == 'language' %}class="active"{% endif %}><a href="{{url_for('language_overview')}}"><span class="glyphicon glyphicon-flag"></span>{{_('Languages')}} </a></li>
{%endif%} {%endif%}
{% if g.user.is_authenticated or g.user.is_anonymous %} {% if g.user.is_authenticated or g.user.is_anonymous %}
<li class="nav-head hidden-xs">{{_('Public Shelves')}}</li> <li class="nav-head hidden-xs">{{_('Public Shelves')}}</li>
{% for shelf in g.public_shelfes %} {% for shelf in g.public_shelfes %}
<li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list"></span> {{shelf.name}}</a></li> <li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list"></span>{{shelf.name}}</a></li>
{% endfor %} {% endfor %}
<li class="nav-head hidden-xs">{{_('Your Shelves')}}</li> <li class="nav-head hidden-xs">{{_('Your Shelves')}}</li>
{% for shelf in g.user.shelf %} {% for shelf in g.user.shelf %}
<li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list"></span> {{shelf.name}}</a></li> <li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list"></span>{{shelf.name}}</a></li>
{% endfor %} {% endfor %}
{% if not g.user.is_anonymous %} {% if not g.user.is_anonymous %}
<li id="nav_createshelf" class="create-shelf"><a href="{{url_for('create_shelf')}}">{{_('Create a Shelf')}}</a></li> <li id="nav_createshelf" class="create-shelf"><a href="{{url_for('create_shelf')}}">{{_('Create a Shelf')}}</a></li>
<li id="nav_about" {% if page == 'stat' %}class="active"{% endif %}><a href="{{url_for('stats')}}"><span class="glyphicon glyphicon-info-sign"></span> {{_('About')}}</a></li> <li id="nav_about" {% if page == 'stat' %}class="active"{% endif %}><a href="{{url_for('stats')}}"><span class="glyphicon glyphicon-info-sign"></span>{{_('About')}}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@ -2,7 +2,7 @@
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<LongName>{{instance}}</LongName> <LongName>{{instance}}</LongName>
<ShortName>{{instance}}</ShortName> <ShortName>{{instance}}</ShortName>
<Description>{{_('Calibre Web ebook catalog')}}</Description> <Description>{{_('Calibre-Web ebook catalog')}}</Description>
<Developer>Janeczku</Developer> <Developer>Janeczku</Developer>
<Contact>https://github.com/janeczku/calibre-web</Contact> <Contact>https://github.com/janeczku/calibre-web</Contact>
<Url type="text/html" <Url type="text/html"

View File

@ -7,10 +7,10 @@
<label for="nickname">{{_('Username')}}</label> <label for="nickname">{{_('Username')}}</label>
<input type="text" class="form-control" id="nickname" name="nickname" placeholder="{{_('Choose a username')}}" required> <input type="text" class="form-control" id="nickname" name="nickname" placeholder="{{_('Choose a username')}}" required>
</div> </div>
<div class="form-group required"> <!--div class="form-group required">
<label for="password">{{_('Password')}}</label> <label for="password">{{_('Password')}}</label>
<input type="password" class="form-control" id="password" name="password" placeholder="{{_('Choose a password')}}" required> <input type="password" class="form-control" id="password" name="password" placeholder="{{_('Choose a password')}}" required>
</div> </div-->
<div class="form-group required"> <div class="form-group required">
<label for="email">{{_('Email address')}}</label> <label for="email">{{_('Email address')}}</label>
<input type="email" class="form-control" id="email" name="email" placeholder="{{_('Your email address')}}" required> <input type="email" class="form-control" id="email" name="email" placeholder="{{_('Your email address')}}" required>

View File

@ -2,7 +2,7 @@
{% block body %} {% block body %}
<h3>{{_('About')}}</h3> <h3>{{_('About')}}</h3>
<p>{{instance}} powered by <p>{{instance}} powered by
<a href="https://github.com/janeczku/calibre-web" title="Calibre-Web">Calibre Web</a>. <a href="https://github.com/janeczku/calibre-web" title="Calibre-Web">Calibre-Web</a>.
</p> </p>
<h3>{{_('Calibre library statistics')}}</h3> <h3>{{_('Calibre library statistics')}}</h3>
<table id="stats" class="table"> <table id="stats" class="table">

View File

@ -28,6 +28,7 @@
{% block js %} {% block js %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
<script> <script>
// ToDo: Move to js file
$('#table').bootstrapTable({ $('#table').bootstrapTable({
formatNoMatches: function () { formatNoMatches: function () {
return ''; return '';

View File

@ -14,10 +14,16 @@
<input type="email" class="form-control" name="email" id="email" value="{{ content.email if content.email != None }}" autocomplete="off"> <input type="email" class="form-control" name="email" id="email" value="{{ content.email if content.email != None }}" autocomplete="off">
</div> </div>
{% if ( g.user and g.user.role_passwd() or g.user.role_admin() ) and not content.role_anonymous() %} {% if ( g.user and g.user.role_passwd() or g.user.role_admin() ) and not content.role_anonymous() %}
<div class="form-group"> {% if g.user and g.user.role_admin() and g.allow_registration and not new_user and not profile %}
<label for="password">{{_('Password')}}</label> <div class="btn btn-default" id="resend_password"><a href="{{url_for('reset_password', user_id = content.id) }}">{{_('Reset user Password')}}</a></div>
<input type="password" class="form-control" name="password" id="password" value="" autocomplete="off"> {% else %}
</div> <div class="form-group">
<label for="password">{{_('Password')}}</label>
<input type="password" class="form-control" name="password" id="password" value="" autocomplete="off">
</div>
{% endif %}
{% endif %} {% endif %}
<div class="form-group"> <div class="form-group">
<label for="kindle_mail">{{_('Kindle E-Mail')}}</label> <label for="kindle_mail">{{_('Kindle E-Mail')}}</label>

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

Binary file not shown.

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

@ -149,7 +149,7 @@ class UserBase:
return '<User %r>' % self.nickname return '<User %r>' % self.nickname
# Baseclass for Users in Calibre-web, settings which are depending on certain users are stored here. It is derived from # Baseclass for Users in Calibre-Web, settings which are depending on certain users are stored here. It is derived from
# User Base (all access methods are declared there) # User Base (all access methods are declared there)
class User(UserBase, Base): class User(UserBase, Base):
__tablename__ = 'user' __tablename__ = 'user'
@ -216,7 +216,7 @@ class Shelf(Base):
return '<Shelf %r>' % self.name return '<Shelf %r>' % self.name
# Baseclass representing Relationship between books and Shelfs in Calibre-web in app.db (N:M) # Baseclass representing Relationship between books and Shelfs in Calibre-Web in app.db (N:M)
class BookShelf(Base): class BookShelf(Base):
__tablename__ = 'book_shelf_link' __tablename__ = 'book_shelf_link'
@ -259,6 +259,17 @@ class Downloads(Base):
def __repr__(self): def __repr__(self):
return '<Download %r' % self.book_id return '<Download %r' % self.book_id
# Baseclass representing allowed domains for registration
class Registration(Base):
__tablename__ = 'registration'
id = Column(Integer, primary_key=True)
domain = Column(String)
def __repr__(self):
return u"<Registration('{0}')>".format(self.domain)
# Baseclass for representing settings in app.db with email server settings and Calibre database settings # Baseclass for representing settings in app.db with email server settings and Calibre database settings
# (application settings) # (application settings)
@ -276,7 +287,7 @@ class Settings(Base):
config_port = Column(Integer, default=DEFAULT_PORT) config_port = Column(Integer, default=DEFAULT_PORT)
config_certfile = Column(String) config_certfile = Column(String)
config_keyfile = Column(String) config_keyfile = Column(String)
config_calibre_web_title = Column(String, default=u'Calibre-web') config_calibre_web_title = Column(String, default=u'Calibre-Web')
config_books_per_page = Column(Integer, default=60) config_books_per_page = Column(Integer, default=60)
config_random_books = Column(Integer, default=4) config_random_books = Column(Integer, default=4)
config_read_column = Column(Integer, default=0) config_read_column = Column(Integer, default=0)
@ -516,35 +527,18 @@ class Config:
# everywhere to curent should work. Migration is done by checking if relevant coloums are existing, and than adding # everywhere to curent should work. Migration is done by checking if relevant coloums are existing, and than adding
# rows with SQL commands # rows with SQL commands
def migrate_Database(): def migrate_Database():
if not engine.dialect.has_table(engine.connect(), "book_read_link"):
ReadBook.__table__.create(bind=engine)
if not engine.dialect.has_table(engine.connect(), "bookmark"): if not engine.dialect.has_table(engine.connect(), "bookmark"):
Bookmark.__table__.create(bind=engine) Bookmark.__table__.create(bind=engine)
if not engine.dialect.has_table(engine.connect(), "registration"):
try: ReadBook.__table__.create(bind=engine)
session.query(exists().where(User.locale)).scalar()
session.commit()
except exc.OperationalError: # Database is not compatible, some rows are missing
conn = engine.connect() conn = engine.connect()
conn.execute("ALTER TABLE user ADD column locale String(2) DEFAULT 'en'") conn.execute("insert into registration (domain) values('%.%')")
conn.execute("ALTER TABLE user ADD column default_language String(3) DEFAULT 'all'")
session.commit() session.commit()
try: # Handle table exists, but no content
session.query(exists().where(Settings.config_calibre_dir)).scalar() cnt = session.query(Registration).count()
session.commit() if not cnt:
except exc.OperationalError: # Database is not compatible, some rows are missing
conn = engine.connect() conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_calibre_dir` String") conn.execute("insert into registration (domain) values('%.%')")
conn.execute("ALTER TABLE Settings ADD column `config_port` INTEGER DEFAULT 8083")
conn.execute("ALTER TABLE Settings ADD column `config_calibre_web_title` String DEFAULT 'Calibre-web'")
conn.execute("ALTER TABLE Settings ADD column `config_books_per_page` INTEGER DEFAULT 60")
conn.execute("ALTER TABLE Settings ADD column `config_random_books` INTEGER DEFAULT 4")
conn.execute("ALTER TABLE Settings ADD column `config_title_regex` String DEFAULT "
"'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+'")
conn.execute("ALTER TABLE Settings ADD column `config_log_level` SmallInteger DEFAULT " + str(logging.INFO))
conn.execute("ALTER TABLE Settings ADD column `config_uploading` SmallInteger DEFAULT 0")
conn.execute("ALTER TABLE Settings ADD column `config_anonbrowse` SmallInteger DEFAULT 0")
conn.execute("ALTER TABLE Settings ADD column `config_public_reg` SmallInteger DEFAULT 0")
session.commit() session.commit()
try: try:
session.query(exists().where(Settings.config_use_google_drive)).scalar() session.query(exists().where(Settings.config_use_google_drive)).scalar()
@ -553,6 +547,7 @@ def migrate_Database():
conn.execute("ALTER TABLE Settings ADD column `config_use_google_drive` INTEGER DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_use_google_drive` INTEGER DEFAULT 0")
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_folder` String DEFAULT ''") conn.execute("ALTER TABLE Settings ADD column `config_google_drive_folder` String DEFAULT ''")
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_watch_changes_response` String DEFAULT ''") conn.execute("ALTER TABLE Settings ADD column `config_google_drive_watch_changes_response` String DEFAULT ''")
session.commit()
try: try:
session.query(exists().where(Settings.config_columns_to_ignore)).scalar() session.query(exists().where(Settings.config_columns_to_ignore)).scalar()
except exc.OperationalError: except exc.OperationalError:
@ -561,14 +556,12 @@ def migrate_Database():
session.commit() session.commit()
try: try:
session.query(exists().where(Settings.config_default_role)).scalar() session.query(exists().where(Settings.config_default_role)).scalar()
session.commit()
except exc.OperationalError: # Database is not compatible, some rows are missing except exc.OperationalError: # Database is not compatible, some rows are missing
conn = engine.connect() conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_default_role` SmallInteger DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_default_role` SmallInteger DEFAULT 0")
session.commit() session.commit()
try: try:
session.query(exists().where(BookShelf.order)).scalar() session.query(exists().where(BookShelf.order)).scalar()
session.commit()
except exc.OperationalError: # Database is not compatible, some rows are missing except exc.OperationalError: # Database is not compatible, some rows are missing
conn = engine.connect() conn = engine.connect()
conn.execute("ALTER TABLE book_shelf_link ADD column 'order' INTEGER DEFAULT 1") conn.execute("ALTER TABLE book_shelf_link ADD column 'order' INTEGER DEFAULT 1")
@ -576,7 +569,6 @@ def migrate_Database():
try: try:
create = False create = False
session.query(exists().where(User.sidebar_view)).scalar() session.query(exists().where(User.sidebar_view)).scalar()
session.commit()
except exc.OperationalError: # Database is not compatible, some rows are missing except exc.OperationalError: # Database is not compatible, some rows are missing
conn = engine.connect() conn = engine.connect()
conn.execute("ALTER TABLE user ADD column `sidebar_view` Integer DEFAULT 1") conn.execute("ALTER TABLE user ADD column `sidebar_view` Integer DEFAULT 1")
@ -606,6 +598,7 @@ def migrate_Database():
except exc.OperationalError: except exc.OperationalError:
conn = engine.connect() conn = engine.connect()
conn.execute("ALTER TABLE user ADD column `theme` INTEGER DEFAULT 0") conn.execute("ALTER TABLE user ADD column `theme` INTEGER DEFAULT 0")
session.commit()
if session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() is None: if session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() is None:
create_anonymous_user() create_anonymous_user()
try: try:
@ -627,21 +620,18 @@ def migrate_Database():
conn.execute("ALTER TABLE Settings ADD column `config_mature_content_tags` String DEFAULT ''") conn.execute("ALTER TABLE Settings ADD column `config_mature_content_tags` String DEFAULT ''")
try: try:
session.query(exists().where(Settings.config_default_show)).scalar() session.query(exists().where(Settings.config_default_show)).scalar()
session.commit()
except exc.OperationalError: # Database is not compatible, some rows are missing except exc.OperationalError: # Database is not compatible, some rows are missing
conn = engine.connect() conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_default_show` SmallInteger DEFAULT 2047") conn.execute("ALTER TABLE Settings ADD column `config_default_show` SmallInteger DEFAULT 2047")
session.commit() session.commit()
try: try:
session.query(exists().where(Settings.config_logfile)).scalar() session.query(exists().where(Settings.config_logfile)).scalar()
session.commit()
except exc.OperationalError: # Database is not compatible, some rows are missing except exc.OperationalError: # Database is not compatible, some rows are missing
conn = engine.connect() conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_logfile` String DEFAULT ''") conn.execute("ALTER TABLE Settings ADD column `config_logfile` String DEFAULT ''")
session.commit() session.commit()
try: try:
session.query(exists().where(Settings.config_certfile)).scalar() session.query(exists().where(Settings.config_certfile)).scalar()
session.commit()
except exc.OperationalError: # Database is not compatible, some rows are missing except exc.OperationalError: # Database is not compatible, some rows are missing
conn = engine.connect() conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_certfile` String DEFAULT ''") conn.execute("ALTER TABLE Settings ADD column `config_certfile` String DEFAULT ''")
@ -649,14 +639,12 @@ def migrate_Database():
session.commit() session.commit()
try: try:
session.query(exists().where(Settings.config_read_column)).scalar() session.query(exists().where(Settings.config_read_column)).scalar()
session.commit()
except exc.OperationalError: # Database is not compatible, some rows are missing except exc.OperationalError: # Database is not compatible, some rows are missing
conn = engine.connect() conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_read_column` INTEGER DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_read_column` INTEGER DEFAULT 0")
session.commit() session.commit()
try: try:
session.query(exists().where(Settings.config_ebookconverter)).scalar() session.query(exists().where(Settings.config_ebookconverter)).scalar()
session.commit()
except exc.OperationalError: # Database is not compatible, some rows are missing except exc.OperationalError: # Database is not compatible, some rows are missing
conn = engine.connect() conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_ebookconverter` INTEGER DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_ebookconverter` INTEGER DEFAULT 0")

View File

@ -2,9 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
try: try:
from googleapiclient.errors import HttpError from googleapiclient.errors import HttpError
# gdrive_support = True
except ImportError: except ImportError:
# gdrive_support = False
pass pass
try: try:
@ -87,6 +85,7 @@ except ImportError:
import time import time
import server import server
import random
current_milli_time = lambda: int(round(time.time() * 1000)) current_milli_time = lambda: int(round(time.time() * 1000))
@ -94,7 +93,8 @@ current_milli_time = lambda: int(round(time.time() * 1000))
# Global variables # Global variables
gdrive_watch_callback_token = 'target=calibreweb-watch_files' gdrive_watch_callback_token = 'target=calibreweb-watch_files'
ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx', 'fb2']) ALLOWED_EXTENSIONS = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx',
'fb2'}
def md5(fname): def md5(fname):
hash_md5 = hashlib.md5() hash_md5 = hashlib.md5()
@ -136,9 +136,9 @@ class ReverseProxied(object):
scheme = environ.get('HTTP_X_SCHEME', '') scheme = environ.get('HTTP_X_SCHEME', '')
if scheme: if scheme:
environ['wsgi.url_scheme'] = scheme environ['wsgi.url_scheme'] = scheme
server = environ.get('HTTP_X_FORWARDED_SERVER', '') servr = environ.get('HTTP_X_FORWARDED_SERVER', '')
if server: if servr:
environ['HTTP_HOST'] = server environ['HTTP_HOST'] = servr
return self.app(environ, start_response) return self.app(environ, start_response)
@ -185,8 +185,8 @@ db.setup_db()
def is_gdrive_ready(): def is_gdrive_ready():
return os.path.exists(os.path.join(config.get_main_dir,'settings.yaml')) and \ return os.path.exists(os.path.join(config.get_main_dir, 'settings.yaml')) and \
os.path.exists(os.path.join(config.get_main_dir,'gdrive_credentials')) os.path.exists(os.path.join(config.get_main_dir, 'gdrive_credentials'))
@babel.localeselector @babel.localeselector
@ -287,10 +287,10 @@ class Pagination(object):
def has_next(self): def has_next(self):
return self.page < self.pages return self.page < self.pages
# right_edge: last right_edges count of all pages are shown as number, means, if 10 pages are paginated -> 9,10 shown # right_edge: last right_edges count of all pages are shown as number, means, if 10 pages are paginated -> 9,10 shwn
# left_edge: first left_edges count of all pages are shown as number -> 1,2 shown # left_edge: first left_edges count of all pages are shown as number -> 1,2 shwn
# left_current: left_current count below current page are shown as number, means if current page 5 -> 3,4 shown # left_current: left_current count below current page are shown as number, means if current page 5 -> 3,4 shwn
# left_current: right_current count above current page are shown as number, means if current page 5 -> 6,7 shown # left_current: right_current count above current page are shown as number, means if current page 5 -> 6,7 shwn
def iter_pages(self, left_edge=2, left_current=2, def iter_pages(self, left_edge=2, left_current=2,
right_current=4, right_edge=2): right_current=4, right_edge=2):
last = 0 last = 0
@ -321,7 +321,7 @@ def remote_login_required(f):
return f(*args, **kwargs) return f(*args, **kwargs)
if request.is_xhr: if request.is_xhr:
data = {'status': 'error', 'message': 'Forbidden'} data = {'status': 'error', 'message': 'Forbidden'}
response = make_response(json.dumps(data, ensure_ascii=false)) response = make_response(json.dumps(data, ensure_ascii=False))
response.headers["Content-Type"] = "application/json; charset=utf-8" response.headers["Content-Type"] = "application/json; charset=utf-8"
return response, 403 return response, 403
abort(403) abort(403)
@ -338,9 +338,10 @@ def url_for_other_page(page):
args['page'] = page args['page'] = page
return url_for(request.endpoint, **args) return url_for(request.endpoint, **args)
# shortentitles to at longest nchar, shorten longer words if necessary # shortentitles to at longest nchar, shorten longer words if necessary
@app.template_filter('shortentitle') @app.template_filter('shortentitle')
def shortentitle_filter(s,nchar=20): def shortentitle_filter(s, nchar=20):
text = s.split() text = s.split()
res = "" # result res = "" # result
suml = 0 # overall length suml = 0 # overall length
@ -471,7 +472,7 @@ def common_filters():
# Creates for all stored languages a translated speaking name in the array for the UI # Creates for all stored languages a translated speaking name in the array for the UI
def speaking_language(languages = None): def speaking_language(languages=None):
if not languages: if not languages:
languages = db.session.query(db.Languages).all() languages = db.session.query(db.Languages).all()
for lang in languages: for lang in languages:
@ -486,17 +487,17 @@ def speaking_language(languages = None):
# Fill indexpage with all requested data from database # Fill indexpage with all requested data from database
def fill_indexpage(page, database, db_filter, order, *join): def fill_indexpage(page, database, db_filter, order, *join):
if current_user.show_detail_random(): if current_user.show_detail_random():
random = db.session.query(db.Books).filter(common_filters())\ randm = db.session.query(db.Books).filter(common_filters())\
.order_by(func.random()).limit(config.config_random_books) .order_by(func.random()).limit(config.config_random_books)
else: else:
random = false randm = false()
off = int(int(config.config_books_per_page) * (page - 1)) off = int(int(config.config_books_per_page) * (page - 1))
pagination = Pagination(page, config.config_books_per_page, pagination = Pagination(page, config.config_books_per_page,
len(db.session.query(database) len(db.session.query(database)
.filter(db_filter).filter(common_filters()).all())) .filter(db_filter).filter(common_filters()).all()))
entries = db.session.query(database).join(*join,isouter=True).filter(db_filter)\ entries = db.session.query(database).join(*join,isouter=True).filter(db_filter)\
.filter(common_filters()).order_by(*order).offset(off).limit(config.config_books_per_page).all() .filter(common_filters()).order_by(*order).offset(off).limit(config.config_books_per_page).all()
return entries, random, pagination return entries, randm, pagination
# Modifies different Database objects, first check if elements have to be added to database, than check # Modifies different Database objects, first check if elements have to be added to database, than check
@ -893,6 +894,73 @@ def get_email_status_json():
return response return response
# checks if domain is in database (including wildcards)
# example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name;
# from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/
def check_valid_domain(domain_text):
# result = session.query(Notification).from_statement(text(sql)).params(id=5).all()
#ToDo: check possible SQL injection
domain_text = domain_text.split('@',1)[-1].lower()
sql = "SELECT * FROM registration WHERE '%s' LIKE domain;" % domain_text
result = ub.session.query(ub.Registration).from_statement(text(sql)).all()
return len(result)
''' POST /post
{
name: 'username', //name of field (column in db)
pk: 1 //primary key (record id)
value: 'superuser!' //new value
}'''
@app.route("/ajax/editdomain", methods=['POST'])
@login_required
@admin_required
def edit_domain():
vals = request.form.to_dict()
answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first()
# domain_name = request.args.get('domain')
answer.domain = vals['value'].replace('*','%').replace('?','_').lower()
ub.session.commit()
return ""
@app.route("/ajax/adddomain", methods=['POST'])
@login_required
@admin_required
def add_domain():
domain_name = request.form.to_dict()['domainname'].replace('*','%').replace('?','_').lower()
check = ub.session.query(ub.Registration).filter(ub.Registration.domain == domain_name).first()
if not check:
new_domain = ub.Registration(domain=domain_name)
ub.session.add(new_domain)
ub.session.commit()
return ""
@app.route("/ajax/deletedomain", methods=['POST'])
@login_required
@admin_required
def delete_domain():
domain_id = request.form.to_dict()['domainid'].replace('*','%').replace('?','_').lower()
ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete()
ub.session.commit()
# If last domain was deleted, add all domains by default
if not ub.session.query(ub.Registration).count():
new_domain = ub.Registration(domain="%.%")
ub.session.add(new_domain)
ub.session.commit()
return ""
@app.route("/ajax/domainlist")
@login_required
@admin_required
def list_domain():
answer = ub.session.query(ub.Registration).all()
json_dumps = json.dumps([{"domain":r.domain.replace('%','*').replace('_','?'),"id":r.id} for r in answer])
js=json.dumps(json_dumps.replace('"', "'")).lstrip('"').strip('"')
response = make_response(js.replace("'",'"'))
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@app.route("/get_authors_json", methods=['GET', 'POST']) @app.route("/get_authors_json", methods=['GET', 'POST'])
@login_required_if_no_ano @login_required_if_no_ano
def get_authors_json(): def get_authors_json():
@ -1076,7 +1144,7 @@ def hot_books(page):
random = db.session.query(db.Books).filter(common_filters())\ random = db.session.query(db.Books).filter(common_filters())\
.order_by(func.random()).limit(config.config_random_books) .order_by(func.random()).limit(config.config_random_books)
else: else:
random = false random = false()
off = int(int(config.config_books_per_page) * (page - 1)) off = int(int(config.config_books_per_page) * (page - 1))
all_books = ub.session.query(ub.Downloads, ub.func.count(ub.Downloads.book_id)).order_by( all_books = ub.session.query(ub.Downloads, ub.func.count(ub.Downloads.book_id)).order_by(
ub.func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id) ub.func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id)
@ -1996,30 +2064,38 @@ def register():
if request.method == "POST": if request.method == "POST":
to_save = request.form.to_dict() to_save = request.form.to_dict()
if not to_save["nickname"] or not to_save["email"] or not to_save["password"]: if not to_save["nickname"] or not to_save["email"]:
flash(_(u"Please fill out all fields!"), category="error") flash(_(u"Please fill out all fields!"), category="error")
return render_title_template('register.html', title=_(u"register"), page="register") return render_title_template('register.html', title=_(u"register"), page="register")
existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower()).first() existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower()).first()
existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"]).first() existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()).first()
if not existing_user and not existing_email: if not existing_user and not existing_email:
content = ub.User() content = ub.User()
content.password = generate_password_hash(to_save["password"]) # content.password = generate_password_hash(to_save["password"])
content.nickname = to_save["nickname"] if check_valid_domain(to_save["email"]):
content.email = to_save["email"] content.nickname = to_save["nickname"]
content.role = config.config_default_role content.email = to_save["email"]
content.sidebar_view = config.config_default_show password = helper.generate_random_password()
try: content.password = generate_password_hash(password)
ub.session.add(content) content.role = config.config_default_role
ub.session.commit() content.sidebar_view = config.config_default_show
except Exception: try:
ub.session.rollback() ub.session.add(content)
flash(_(u"An unknown error occured. Please try again later."), category="error") ub.session.commit()
helper.send_registration_mail(to_save["email"],to_save["nickname"], password)
except Exception:
ub.session.rollback()
flash(_(u"An unknown error occurred. Please try again later."), category="error")
return render_title_template('register.html', title=_(u"register"), page="register")
else:
flash(_(u"Your e-mail is not allowed to register"), category="error")
app.logger.info('Registering failed for user "' + to_save['nickname'] + '" e-mail adress: ' + to_save["email"])
return render_title_template('register.html', title=_(u"register"), page="register") return render_title_template('register.html', title=_(u"register"), page="register")
flash("Your account has been created. Please login.", category="success") flash(_(u"Confirmation e-mail was send to your e-mail account."), category="success")
return redirect(url_for('login')) return redirect(url_for('login'))
else: else:
flash(_(u"This username or email address is already in use."), category="error") flash(_(u"This username or e-mail address is already in use."), category="error")
return render_title_template('register.html', title=_(u"register"), page="register") return render_title_template('register.html', title=_(u"register"), page="register")
return render_title_template('register.html', title=_(u"register"), page="register") return render_title_template('register.html', title=_(u"register"), page="register")
@ -2134,7 +2210,7 @@ def token_verified():
data['status'] = 'success' data['status'] = 'success'
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success") flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success")
response = make_response(json.dumps(data, ensure_ascii=false)) response = make_response(json.dumps(data, ensure_ascii=False))
response.headers["Content-Type"] = "application/json; charset=utf-8" response.headers["Content-Type"] = "application/json; charset=utf-8"
return response return response
@ -2156,7 +2232,7 @@ def send_to_kindle(book_id):
else: else:
flash(_(u"There was an error sending this book: %(res)s", res=result), category="error") flash(_(u"There was an error sending this book: %(res)s", res=result), category="error")
else: else:
flash(_(u"Please configure your kindle email address first..."), category="error") flash(_(u"Please configure your kindle e-mail address first..."), category="error")
return redirect(request.environ["HTTP_REFERER"]) return redirect(request.environ["HTTP_REFERER"])
@ -2363,7 +2439,7 @@ def edit_shelf(shelf_id):
@login_required @login_required
def delete_shelf(shelf_id): def delete_shelf(shelf_id):
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
deleted = false deleted = None
if current_user.role_admin(): if current_user.role_admin():
deleted = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).delete() deleted = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).delete()
else: else:
@ -2456,17 +2532,21 @@ def profile():
to_save = request.form.to_dict() to_save = request.form.to_dict()
content.random_books = 0 content.random_books = 0
if current_user.role_passwd() or current_user.role_admin(): if current_user.role_passwd() or current_user.role_admin():
if to_save["password"]: if "password" in to_save:
content.password = generate_password_hash(to_save["password"]) content.password = generate_password_hash(to_save["password"])
if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail: if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail:
content.kindle_mail = to_save["kindle_mail"] content.kindle_mail = to_save["kindle_mail"]
if to_save["email"] and to_save["email"] != content.email: if to_save["email"] and to_save["email"] != content.email:
if config.config_public_reg and not check_valid_domain(to_save["email"]):
flash(_(u"E-mail is not from valid domain"), category="error")
return render_title_template("user_edit.html", content=content, downloads=downloads,
title=_(u"%(name)s's profile", name=current_user.nickname))
content.email = to_save["email"] content.email = to_save["email"]
if "show_random" in to_save and to_save["show_random"] == "on": if "show_random" in to_save and to_save["show_random"] == "on":
content.random_books = 1 content.random_books = 1
if "default_language" in to_save: if "default_language" in to_save:
content.default_language = to_save["default_language"] content.default_language = to_save["default_language"]
if to_save["locale"]: if "locale" in to_save:
content.locale = to_save["locale"] content.locale = to_save["locale"]
content.sidebar_view = 0 content.sidebar_view = 0
if "show_random" in to_save: if "show_random" in to_save:
@ -2499,7 +2579,7 @@ def profile():
ub.session.commit() ub.session.commit()
except IntegrityError: except IntegrityError:
ub.session.rollback() ub.session.rollback()
flash(_(u"Found an existing account for this email address."), category="error") flash(_(u"Found an existing account for this e-mail address."), category="error")
return render_title_template("user_edit.html", content=content, downloads=downloads, return render_title_template("user_edit.html", content=content, downloads=downloads,
title=_(u"%(name)s's profile", name=current_user.nickname)) title=_(u"%(name)s's profile", name=current_user.nickname))
flash(_(u"Profile updated"), category="success") flash(_(u"Profile updated"), category="success")
@ -2606,7 +2686,7 @@ def view_configuration():
if "show_mature_content" in to_save: if "show_mature_content" in to_save:
content.config_default_show = content.config_default_show + ub.MATURE_CONTENT content.config_default_show = content.config_default_show + ub.MATURE_CONTENT
ub.session.commit() ub.session.commit()
flash(_(u"Calibre-web configuration updated"), category="success") flash(_(u"Calibre-Web configuration updated"), category="success")
config.loadSettings() config.loadSettings()
if reboot_required: if reboot_required:
# db.engine.dispose() # ToDo verify correct # db.engine.dispose() # ToDo verify correct
@ -2634,6 +2714,7 @@ def configuration_helper(origin):
gdriveError=None gdriveError=None
db_change = False db_change = False
success = False success = False
filedata = None
if gdriveutils.gdrive_support == False: if gdriveutils.gdrive_support == False:
gdriveError = _('Import of optional Google Drive requirements missing') gdriveError = _('Import of optional Google Drive requirements missing')
else: else:
@ -2644,7 +2725,6 @@ def configuration_helper(origin):
filedata=json.load(settings) filedata=json.load(settings)
if not 'web' in filedata: if not 'web' in filedata:
gdriveError = _('client_secrets.json is not configured for web application') gdriveError = _('client_secrets.json is not configured for web application')
filedata = None
if request.method == "POST": if request.method == "POST":
to_save = request.form.to_dict() to_save = request.form.to_dict()
content = ub.session.query(ub.Settings).first() # type: ub.Settings content = ub.session.query(ub.Settings).first() # type: ub.Settings
@ -2768,7 +2848,7 @@ def configuration_helper(origin):
db.session.close() db.session.close()
db.engine.dispose() db.engine.dispose()
ub.session.commit() ub.session.commit()
flash(_(u"Calibre-web configuration updated"), category="success") flash(_(u"Calibre-Web configuration updated"), category="success")
config.loadSettings() config.loadSettings()
app.logger.setLevel(config.config_log_level) app.logger.setLevel(config.config_log_level)
logging.getLogger("book_formats").setLevel(config.config_log_level) logging.getLogger("book_formats").setLevel(config.config_log_level)
@ -2793,10 +2873,10 @@ def configuration_helper(origin):
app.logger.info('Reboot required, restarting') app.logger.info('Reboot required, restarting')
if origin: if origin:
success = True success = True
if is_gdrive_ready() and gdriveutils.gdrive_support == True: if is_gdrive_ready() and gdriveutils.gdrive_support == True and config.config_use_google_drive == True:
gdrivefolders=gdriveutils.listRootFolders() gdrivefolders=gdriveutils.listRootFolders()
else: else:
gdrivefolders=None gdrivefolders=list()
return render_title_template("config_edit.html", origin=origin, success=success, content=config, return render_title_template("config_edit.html", origin=origin, success=success, content=config,
show_authenticate_google_drive=not is_gdrive_ready(), gdrive=gdriveutils.gdrive_support, show_authenticate_google_drive=not is_gdrive_ready(), gdrive=gdriveutils.gdrive_support,
gdriveError=gdriveError, gdrivefolders=gdrivefolders, gdriveError=gdriveError, gdrivefolders=gdrivefolders,
@ -2812,13 +2892,6 @@ def new_user():
translations = [LC('en')] + babel.list_translations() translations = [LC('en')] + babel.list_translations()
if request.method == "POST": if request.method == "POST":
to_save = request.form.to_dict() to_save = request.form.to_dict()
if not to_save["nickname"] or not to_save["email"] or not to_save["password"]:
flash(_(u"Please fill out all fields!"), category="error")
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
title=_(u"Add new user"))
content.password = generate_password_hash(to_save["password"])
content.nickname = to_save["nickname"]
content.email = to_save["email"]
content.default_language = to_save["default_language"] content.default_language = to_save["default_language"]
content.mature_content = "show_mature_content" in to_save content.mature_content = "show_mature_content" in to_save
content.theme = int(to_save["theme"]) content.theme = int(to_save["theme"])
@ -2858,6 +2931,18 @@ def new_user():
content.role = content.role + ub.ROLE_PASSWD content.role = content.role + ub.ROLE_PASSWD
if "edit_shelf_role" in to_save: if "edit_shelf_role" in to_save:
content.role = content.role + ub.ROLE_EDIT_SHELFS content.role = content.role + ub.ROLE_EDIT_SHELFS
if not to_save["nickname"] or not to_save["email"] or not to_save["password"]:
flash(_(u"Please fill out all fields!"), category="error")
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
title=_(u"Add new user"))
content.password = generate_password_hash(to_save["password"])
content.nickname = to_save["nickname"]
if config.config_public_reg and not check_valid_domain(to_save["email"]):
flash(_(u"E-mail is not from valid domain"), category="error")
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
title=_(u"Add new user"))
else:
content.email = to_save["email"]
try: try:
ub.session.add(content) ub.session.add(content)
ub.session.commit() ub.session.commit()
@ -2865,7 +2950,7 @@ def new_user():
return redirect(url_for('admin')) return redirect(url_for('admin'))
except IntegrityError: except IntegrityError:
ub.session.rollback() ub.session.rollback()
flash(_(u"Found an existing account for this email address or nickname."), category="error") flash(_(u"Found an existing account for this e-mail address or nickname."), category="error")
else: else:
content.role = config.config_default_role content.role = config.config_default_role
content.sidebar_view = config.config_default_show content.sidebar_view = config.config_default_show
@ -2889,22 +2974,23 @@ def edit_mailsettings():
content.mail_use_ssl = int(to_save["mail_use_ssl"]) content.mail_use_ssl = int(to_save["mail_use_ssl"])
try: try:
ub.session.commit() ub.session.commit()
flash(_(u"Mail settings updated"), category="success") flash(_(u"E-mail server settings updated"), category="success")
except Exception as e: except Exception as e:
flash(e, category="error") flash(e, category="error")
if "test" in to_save and to_save["test"]: if "test" in to_save and to_save["test"]:
if current_user.kindle_mail: if current_user.kindle_mail:
result = helper.send_test_mail(current_user.kindle_mail, current_user.nickname) result = helper.send_test_mail(current_user.kindle_mail, current_user.nickname)
if result is None: if result is None:
flash(_(u"Test E-Mail successfully send to %(kindlemail)s", kindlemail=current_user.kindle_mail), flash(_(u"Test e-mail successfully send to %(kindlemail)s", kindlemail=current_user.kindle_mail),
category="success") category="success")
else: else:
flash(_(u"There was an error sending the Test E-Mail: %(res)s", res=result), category="error") flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error")
else: else:
flash(_(u"Please configure your kindle email address first..."), category="error") flash(_(u"Please configure your kindle e-mail address first..."), category="error")
else: else:
flash(_(u"E-Mail settings updated"), category="success") flash(_(u"E-mail server settings updated"), category="success")
return render_title_template("email_edit.html", content=content, title=_(u"Edit mail settings"), page="mailset") return render_title_template("email_edit.html", content=content, title=_(u"Edit e-mail server settings"),
page="mailset")
@app.route("/admin/user/<int:user_id>", methods=["GET", "POST"]) @app.route("/admin/user/<int:user_id>", methods=["GET", "POST"])
@ -2916,9 +3002,9 @@ def edit_user(user_id):
languages = speaking_language() languages = speaking_language()
translations = babel.list_translations() + [LC('en')] translations = babel.list_translations() + [LC('en')]
for book in content.downloads: for book in content.downloads:
downloadBook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() downloadbook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
if downloadBook: if downloadbook:
downloads.append(db.session.query(db.Books).filter(db.Books.id == book.book_id).first()) downloads.append(downloadbook)
else: else:
ub.delete_download(book.book_id) ub.delete_download(book.book_id)
# ub.session.query(ub.Downloads).filter(book.book_id == ub.Downloads.book_id).delete() # ub.session.query(ub.Downloads).filter(book.book_id == ub.Downloads.book_id).delete()
@ -3046,6 +3132,26 @@ def edit_user(user_id):
nick=content.nickname), page="edituser") nick=content.nickname), page="edituser")
@app.route("/admin/resetpassword/<int:user_id>")
@login_required
@admin_required
def reset_password(user_id):
if not config.config_public_reg:
abort(404)
if current_user is not None and current_user.is_authenticated:
existing_user = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
password = helper.generate_random_password()
existing_user.password = generate_password_hash(password)
try:
ub.session.commit()
helper.send_registration_mail(existing_user.email, existing_user.nickname, password, True)
flash(_(u"Password for user %s reset" % existing_user.nickname), category="success")
except Exception:
ub.session.rollback()
flash(_(u"An unknown error occurred. Please try again later."), category="error")
return redirect(url_for('admin'))
@app.route("/admin/book/<int:book_id>", methods=['GET', 'POST']) @app.route("/admin/book/<int:book_id>", methods=['GET', 'POST'])
@login_required_if_no_ano @login_required_if_no_ano
@edit_required @edit_required
@ -3062,14 +3168,14 @@ def edit_book(book_id):
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error") flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
return redirect(url_for("index")) return redirect(url_for("index"))
for index in range(0, len(book.languages)): for indx in range(0, len(book.languages)):
try: try:
book.languages[index].language_name = LC.parse(book.languages[index].lang_code).get_language_name( book.languages[indx].language_name = LC.parse(book.languages[indx].lang_code).get_language_name(
get_locale()) get_locale())
except Exception: except Exception:
book.languages[index].language_name = _(isoLanguages.get(part3=book.languages[index].lang_code).name) book.languages[indx].language_name = _(isoLanguages.get(part3=book.languages[indx].lang_code).name)
for author in book.authors: for authr in book.authors:
author_names.append(author.name.replace('|', ',')) author_names.append(authr.name.replace('|', ','))
#Option for showing convertbook button #Option for showing convertbook button
if config.config_ebookconverter == 2: if config.config_ebookconverter == 2:
@ -3097,7 +3203,7 @@ def edit_book(book_id):
conversion_formats=allowed_conversion_formats) conversion_formats=allowed_conversion_formats)
# Update book # Update book
edited_books_id = set() edited_books_id = {}
# Check and handle Uploaded file # Check and handle Uploaded file
if 'btn-upload-format' in request.files: if 'btn-upload-format' in request.files:
@ -3107,7 +3213,8 @@ def edit_book(book_id):
if '.' in requested_file.filename: if '.' in requested_file.filename:
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
if file_ext not in ALLOWED_EXTENSIONS: if file_ext not in ALLOWED_EXTENSIONS:
flash(_('File extension "%s" is not allowed to be uploaded to this server' % file_ext), category="error") flash(_('File extension "%s" is not allowed to be uploaded to this server' % file_ext),
category="error")
return redirect(url_for('show_book', book_id=book.id)) return redirect(url_for('show_book', book_id=book.id))
else: else:
flash(_('File to be uploaded must have an extension'), category="error") flash(_('File to be uploaded must have an extension'), category="error")
@ -3143,10 +3250,36 @@ def edit_book(book_id):
db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort)
# Queue uploader info # Queue uploader info
uploadText=_(u"File format %s added to %s" % (file_ext.upper(),book.title)) uploadText=_(u"File format %s added to %s" % (file_ext.upper(), book.title))
helper.global_WorkerThread.add_upload(current_user.nickname, helper.global_WorkerThread.add_upload(current_user.nickname,
"<a href=\""+ url_for('show_book', book_id=book.id) +"\">"+ uploadText + "</a>") "<a href=\"" + url_for('show_book', book_id=book.id) + "\">" + uploadText + "</a>")
if 'btn-upload-cover' in request.files:
requested_file = request.files['btn-upload-cover']
# check for empty request
if requested_file.filename != '':
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
# file_name = book.path.rsplit('/', 1)[-1]
filepath = os.path.normpath(os.path.join(config.config_calibre_dir, book.path))
saved_filename = os.path.join(filepath, 'cover.' + file_ext)
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
if not os.path.exists(filepath):
try:
os.makedirs(filepath)
except OSError:
flash(_(u"Failed to create path for cover %s (Permission denied)." % filepath), category="error")
return redirect(url_for('show_book', book_id=book.id))
try:
requested_file.save(saved_filename)
# im=Image.open(saved_filename)
book.has_cover = 1
except OSError:
flash(_(u"Failed to store cover-file %s." % saved_filename), category="error")
return redirect(url_for('show_book', book_id=book.id))
except IOError:
flash(_(u"Cover-file is not a valid image file" % saved_filename), category="error")
return redirect(url_for('show_book', book_id=book.id))
to_save = request.form.to_dict() to_save = request.form.to_dict()
try: try:
@ -3316,13 +3449,14 @@ def edit_book(book_id):
else: else:
input_tags = to_save[cc_string].split(',') input_tags = to_save[cc_string].split(',')
input_tags = list(map(lambda it: it.strip(), input_tags)) input_tags = list(map(lambda it: it.strip(), input_tags))
modify_database_object(input_tags, getattr(book, cc_string), db.cc_classes[c.id], db.session, 'custom') modify_database_object(input_tags, getattr(book, cc_string), db.cc_classes[c.id], db.session,
'custom')
db.session.commit() db.session.commit()
if config.config_use_google_drive: if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal() gdriveutils.updateGdriveCalibreFromLocal()
author_names = [] author_names = []
for author in book.authors: for authr in book.authors:
author_names.append(author.name) author_names.append(authr.name)
if "detail_view" in to_save: if "detail_view" in to_save:
return redirect(url_for('show_book', book_id=book.id)) return redirect(url_for('show_book', book_id=book.id))
else: else:
@ -3358,9 +3492,7 @@ def upload():
if file_ext not in ALLOWED_EXTENSIONS: if file_ext not in ALLOWED_EXTENSIONS:
flash( flash(
_('File extension "%s" is not allowed to be uploaded to this server' % _('File extension "%s" is not allowed to be uploaded to this server' %
file_ext), file_ext), category="error")
category="error"
)
return redirect(url_for('index')) return redirect(url_for('index'))
else: else:
flash(_('File to be uploaded must have an extension'), category="error") flash(_('File to be uploaded must have an extension'), category="error")
@ -3369,12 +3501,12 @@ def upload():
# extract metadata from file # extract metadata from file
meta = uploader.upload(requested_file) meta = uploader.upload(requested_file)
title = meta.title title = meta.title
author = meta.author authr = meta.author
tags = meta.tags tags = meta.tags
series = meta.series series = meta.series
series_index = meta.series_id series_index = meta.series_id
title_dir = helper.get_valid_filename(title) title_dir = helper.get_valid_filename(title)
author_dir = helper.get_valid_filename(author) author_dir = helper.get_valid_filename(authr)
filepath = os.path.join(config.config_calibre_dir, author_dir, title_dir) filepath = os.path.join(config.config_calibre_dir, author_dir, title_dir)
saved_filename = os.path.join(filepath, title_dir + meta.extension.lower()) saved_filename = os.path.join(filepath, title_dir + meta.extension.lower())
@ -3403,11 +3535,11 @@ def upload():
move(meta.cover, os.path.join(filepath, "cover.jpg")) move(meta.cover, os.path.join(filepath, "cover.jpg"))
# handle authors # handle authors
is_author = db.session.query(db.Authors).filter(db.Authors.name == author).first() is_author = db.session.query(db.Authors).filter(db.Authors.name == authr).first()
if is_author: if is_author:
db_author = is_author db_author = is_author
else: else:
db_author = db.Authors(author, helper.get_sorted_author(author), "") db_author = db.Authors(authr, helper.get_sorted_author(authr), "")
db.session.add(db_author) db.session.add(db_author)
# handle series # handle series

View File

@ -218,7 +218,7 @@ class WorkerThread(threading.Thread):
if(filename): if(filename):
self.add_email(_(u'Send to Kindle'), self.queue[self.current]['path'], filename, self.add_email(_(u'Send to Kindle'), self.queue[self.current]['path'], filename,
self.queue[self.current]['settings'], self.queue[self.current]['kindle'], self.queue[self.current]['settings'], self.queue[self.current]['kindle'],
self.UIqueue[self.current]['user'], _(u"E-Mail: %s" % self.queue[self.current]['title'])) self.UIqueue[self.current]['user'], _(u"E-mail: %s" % self.queue[self.current]['title']))
def convert(self): def convert(self):
error_message = None error_message = None
@ -326,7 +326,8 @@ class WorkerThread(threading.Thread):
addLock.release() addLock.release()
def add_email(self, subject, filepath, attachment, settings, recipient, user_name, typ): def add_email(self, subject, filepath, attachment, settings, recipient, user_name, typ,
text=_(u'This e-mail has been sent via Calibre-Web.')):
# if more than 20 entries in the list, clean the list # if more than 20 entries in the list, clean the list
addLock = threading.Lock() addLock = threading.Lock()
addLock.acquire() addLock.acquire()
@ -335,7 +336,7 @@ class WorkerThread(threading.Thread):
# progress, runtime, and status = 0 # progress, runtime, and status = 0
self.queue.append({'subject':subject, 'attachment':attachment, 'filepath':filepath, self.queue.append({'subject':subject, 'attachment':attachment, 'filepath':filepath,
'settings':settings, 'recipent':recipient, 'starttime': 0, 'settings':settings, 'recipent':recipient, 'starttime': 0,
'status': STAT_WAITING, 'typ': TASK_EMAIL}) 'status': STAT_WAITING, 'typ': TASK_EMAIL, 'text':text})
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'type': typ, self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'type': typ,
'runtime': '0 s', 'status': _('Waiting'),'id': self.id }) 'runtime': '0 s', 'status': _('Waiting'),'id': self.id })
self.id += 1 self.id += 1
@ -369,7 +370,7 @@ class WorkerThread(threading.Thread):
msg['Subject'] = self.queue[self.current]['subject'] msg['Subject'] = self.queue[self.current]['subject']
msg['Message-Id'] = make_msgid('calibre-web') msg['Message-Id'] = make_msgid('calibre-web')
msg['Date'] = formatdate(localtime=True) msg['Date'] = formatdate(localtime=True)
text = _(u'This email has been sent via calibre web.') text = self.queue[self.current]['text']
msg.attach(MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8')) msg.attach(MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8'))
if obj['attachment']: if obj['attachment']:
result = get_attachment(obj['filepath'], obj['attachment']) result = get_attachment(obj['filepath'], obj['attachment'])

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
# About # About
Calibre Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing [Calibre](https://calibre-ebook.com) database. Calibre-Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing [Calibre](https://calibre-ebook.com) database.
*This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.* *This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.*
@ -12,7 +12,7 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d
- full graphical setup - full graphical setup
- User management with fine grained per-user permissions - User management with fine grained per-user permissions
- Admin interface - Admin interface
- User Interface in dutch, english, french, german, italian, japanese, polish, russian, simplified chinese, spanish - User Interface in dutch, english, french, german, italian, japanese, khmer, polish, russian, simplified chinese, spanish
- OPDS feed for eBook reader apps - OPDS feed for eBook reader apps
- Filter and search by titles, authors, tags, series and language - Filter and search by titles, authors, tags, series and language
- Create custom book collection (shelves) - Create custom book collection (shelves)
@ -98,7 +98,7 @@ The Drive API should now be setup and ready to use, so we need to integrate it i
8. Google Drive should now be connected and be used to get images and download Epubs. The metadata.db is stored in the calibre library location 8. Google Drive should now be connected and be used to get images and download Epubs. The metadata.db is stored in the calibre library location
### Optional ### Optional
If your calibre web is using https, it is possible to add a "watch" to the drive. This will inform us if the metadata.db file is updated and allow us to update our calibre library accordingly. If your Calibre-Web is using https, it is possible to add a "watch" to the drive. This will inform us if the metadata.db file is updated and allow us to update our calibre library accordingly.
Additionally the public adress your server uses (e.g.https://example.com) has to be verified in the Google developer console. After this is done, please wait a few minutes. Additionally the public adress your server uses (e.g.https://example.com) has to be verified in the Google developer console. After this is done, please wait a few minutes.
9. Open config page 9. Open config page
@ -123,7 +123,7 @@ Pre-built Docker images based on Alpine Linux are available in these Docker Hub
Reverse proxy configuration examples for apache and nginx to use Calibre-Web: Reverse proxy configuration examples for apache and nginx to use Calibre-Web:
nginx configuration for a local server listening on port 8080, mapping calibre web to /calibre: nginx configuration for a local server listening on port 8080, mapping Calibre-Web to /calibre:
``` ```
http { http {
@ -148,7 +148,7 @@ http {
proxy_redirect http://$host/ https://$host:12345/; proxy_redirect http://$host/ https://$host:12345/;
``` ```
Apache 2.4 configuration for a local server listening on port 443, mapping calibre web to /calibre-web: Apache 2.4 configuration for a local server listening on port 443, mapping Calibre-Web to /calibre-web:
The following modules have to be activated: headers, proxy, rewrite. The following modules have to be activated: headers, proxy, rewrite.
``` ```