Commit 251a77c8 authored by Ozzie Isaacs's avatar Ozzie Isaacs

Merge branch 'master' into Development

Improved packaging support
parents 33a0a4c1 7b7494b8
...@@ -10,6 +10,7 @@ env/ ...@@ -10,6 +10,7 @@ env/
venv/ venv/
eggs/ eggs/
dist/ dist/
executable/
build/ build/
vendor/ vendor/
.eggs/ .eggs/
......
...@@ -48,6 +48,11 @@ try: ...@@ -48,6 +48,11 @@ try:
except ImportError: except ImportError:
flask_danceVersion = None flask_danceVersion = None
try:
from greenlet import __version__ as greenlet_Version
except ImportError:
greenlet_Version = None
from . import services from . import services
about = flask.Blueprint('about', __name__) about = flask.Blueprint('about', __name__)
...@@ -77,7 +82,8 @@ _VERSIONS = OrderedDict( ...@@ -77,7 +82,8 @@ _VERSIONS = OrderedDict(
python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None, python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None,
Goodreads = u'installed' if bool(services.goodreads_support) else None, Goodreads = u'installed' if bool(services.goodreads_support) else None,
jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None, jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None,
flask_dance = flask_danceVersion flask_dance = flask_danceVersion,
greenlet = greenlet_Version
) )
_VERSIONS.update(uploader.get_versions()) _VERSIONS.update(uploader.get_versions())
......
...@@ -56,7 +56,8 @@ log = logger.create() ...@@ -56,7 +56,8 @@ log = logger.create()
feature_support = { feature_support = {
'ldap': bool(services.ldap), 'ldap': bool(services.ldap),
'goodreads': bool(services.goodreads_support), 'goodreads': bool(services.goodreads_support),
'kobo': bool(services.kobo) 'kobo': bool(services.kobo),
'updater': constants.UPDATER_AVAILABLE
} }
try: try:
...@@ -1335,8 +1336,11 @@ def download_debug(): ...@@ -1335,8 +1336,11 @@ def download_debug():
@login_required @login_required
@admin_required @admin_required
def get_update_status(): def get_update_status():
log.info(u"Update status requested") if feature_support['updater']:
return updater_thread.get_available_updates(request.method, locale=get_locale()) log.info(u"Update status requested")
return updater_thread.get_available_updates(request.method, locale=get_locale())
else:
return ''
@admi.route("/get_updater_status", methods=['GET', 'POST']) @admi.route("/get_updater_status", methods=['GET', 'POST'])
...@@ -1344,35 +1348,37 @@ def get_update_status(): ...@@ -1344,35 +1348,37 @@ def get_update_status():
@admin_required @admin_required
def get_updater_status(): def get_updater_status():
status = {} status = {}
if request.method == "POST": if feature_support['updater']:
commit = request.form.to_dict() if request.method == "POST":
if "start" in commit and commit['start'] == 'True': commit = request.form.to_dict()
text = { if "start" in commit and commit['start'] == 'True':
"1": _(u'Requesting update package'), text = {
"2": _(u'Downloading update package'), "1": _(u'Requesting update package'),
"3": _(u'Unzipping update package'), "2": _(u'Downloading update package'),
"4": _(u'Replacing files'), "3": _(u'Unzipping update package'),
"5": _(u'Database connections are closed'), "4": _(u'Replacing files'),
"6": _(u'Stopping server'), "5": _(u'Database connections are closed'),
"7": _(u'Update finished, please press okay and reload page'), "6": _(u'Stopping server'),
"8": _(u'Update failed:') + u' ' + _(u'HTTP Error'), "7": _(u'Update finished, please press okay and reload page'),
"9": _(u'Update failed:') + u' ' + _(u'Connection error'), "8": _(u'Update failed:') + u' ' + _(u'HTTP Error'),
"10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'), "9": _(u'Update failed:') + u' ' + _(u'Connection error'),
"11": _(u'Update failed:') + u' ' + _(u'General error'), "10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'),
"12": _(u'Update failed:') + u' ' + _(u'Update File Could Not be Saved in Temp Dir') "11": _(u'Update failed:') + u' ' + _(u'General error'),
} "12": _(u'Update failed:') + u' ' + _(u'Update File Could Not be Saved in Temp Dir')
status['text'] = text }
updater_thread.status = 0 status['text'] = text
updater_thread.resume() updater_thread.status = 0
status['status'] = updater_thread.get_update_status() updater_thread.resume()
elif request.method == "GET": status['status'] = updater_thread.get_update_status()
try: elif request.method == "GET":
status['status'] = updater_thread.get_update_status() try:
if status['status'] == -1: status['status'] = updater_thread.get_update_status()
status['status'] = 7 if status['status'] == -1:
except Exception: status['status'] = 7
status['status'] = 11 except Exception:
return json.dumps(status) status['status'] = 11
return json.dumps(status)
return ''
@admi.route('/import_ldap_users') @admi.route('/import_ldap_users')
......
...@@ -23,6 +23,7 @@ from collections import namedtuple ...@@ -23,6 +23,7 @@ from collections import namedtuple
# if installed via pip this variable is set to true # if installed via pip this variable is set to true
HOME_CONFIG = False HOME_CONFIG = False
UPDATER_AVAILABLE = True
# Base dir is parent of current file, necessary if called from different folder # Base dir is parent of current file, necessary if called from different folder
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
...@@ -131,7 +132,7 @@ def selected_roles(dictionary): ...@@ -131,7 +132,7 @@ def selected_roles(dictionary):
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
'series_id, languages') 'series_id, languages')
STABLE_VERSION = {'version': '0.6.11 Beta'} STABLE_VERSION = {'version': '0.6.12 Beta'}
NIGHTLY_VERSION = {} NIGHTLY_VERSION = {}
NIGHTLY_VERSION[0] = '$Format:%H$' NIGHTLY_VERSION[0] = '$Format:%H$'
......
...@@ -36,7 +36,7 @@ def get_sidebar_config(kwargs=None): ...@@ -36,7 +36,7 @@ def get_sidebar_config(kwargs=None):
else: else:
content = 'conf' in kwargs content = 'conf' in kwargs
sidebar = list() sidebar = list()
sidebar.append({"glyph": "glyphicon-book", "text": _('Recently Added'), "link": 'web.index', "id": "new", sidebar.append({"glyph": "glyphicon-book", "text": _('Books'), "link": 'web.index', "id": "new",
"visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root", "visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root",
"show_text": _('Show recent books'), "config_show":False}) "show_text": _('Show recent books'), "config_show":False})
sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot", sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot",
......
...@@ -22,6 +22,7 @@ import os ...@@ -22,6 +22,7 @@ import os
import errno import errno
import signal import signal
import socket import socket
import subprocess
try: try:
from gevent.pywsgi import WSGIServer from gevent.pywsgi import WSGIServer
...@@ -136,6 +137,64 @@ class WebServer(object): ...@@ -136,6 +137,64 @@ class WebServer(object):
return sock, _readable_listen_address(*address) return sock, _readable_listen_address(*address)
def _get_args_for_reloading(self):
"""Determine how the script was executed, and return the args needed
to execute it again in a new process.
Code from https://github.com/pyload/pyload. Author GammaC0de, voulter
"""
rv = [sys.executable]
py_script = sys.argv[0]
args = sys.argv[1:]
# Need to look at main module to determine how it was executed.
__main__ = sys.modules["__main__"]
# The value of __package__ indicates how Python was called. It may
# not exist if a setuptools script is installed as an egg. It may be
# set incorrectly for entry points created with pip on Windows.
if getattr(__main__, "__package__", None) is None or (
os.name == "nt"
and __main__.__package__ == ""
and not os.path.exists(py_script)
and os.path.exists(f"{py_script}.exe")
):
# Executed a file, like "python app.py".
py_script = os.path.abspath(py_script)
if os.name == "nt":
# Windows entry points have ".exe" extension and should be
# called directly.
if not os.path.exists(py_script) and os.path.exists(f"{py_script}.exe"):
py_script += ".exe"
if (
os.path.splitext(sys.executable)[1] == ".exe"
and os.path.splitext(py_script)[1] == ".exe"
):
rv.pop(0)
rv.append(py_script)
else:
# Executed a module, like "python -m module".
if sys.argv[0] == "-m":
args = sys.argv
else:
if os.path.isfile(py_script):
# Rewritten by Python from "-m script" to "/path/to/script.py".
py_module = __main__.__package__
name = os.path.splitext(os.path.basename(py_script))[0]
if name != "__main__":
py_module += f".{name}"
else:
# Incorrectly rewritten by pydevd debugger from "-m script" to "script".
py_module = py_script
rv.extend(("-m", py_module.lstrip(".")))
rv.extend(args)
return rv
def _start_gevent(self): def _start_gevent(self):
ssl_args = self.ssl_args or {} ssl_args = self.ssl_args or {}
...@@ -199,11 +258,8 @@ class WebServer(object): ...@@ -199,11 +258,8 @@ class WebServer(object):
return True return True
log.info("Performing restart of Calibre-Web") log.info("Performing restart of Calibre-Web")
arguments = list(sys.argv) args = self._get_args_for_reloading()
arguments.insert(0, sys.executable) subprocess.call(args, close_fds=True)
if os.name == 'nt':
arguments = ["\"%s\"" % a for a in arguments]
os.execv(sys.executable, arguments)
return True return True
def _killServer(self, __, ___): def _killServer(self, __, ___):
......
...@@ -256,13 +256,11 @@ $(function() { ...@@ -256,13 +256,11 @@ $(function() {
$("#h4").addClass("hidden"); $("#h4").addClass("hidden");
}); });
function startTable(type, user_id) { function startTable(type, user_id) {
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
var path = src.substring(0, src.lastIndexOf("/"));
$("#restrict-elements-table").bootstrapTable({ $("#restrict-elements-table").bootstrapTable({
formatNoMatches: function () { formatNoMatches: function () {
return ""; return "";
}, },
url: path + "/../../ajax/listrestriction/" + type + "/" + user_id, url: getPath() + "/ajax/listrestriction/" + type + "/" + user_id,
rowStyle: function(row) { rowStyle: function(row) {
// console.log('Reihe :' + row + " Index :" + index); // console.log('Reihe :' + row + " Index :" + index);
if (row.id.charAt(0) === "a") { if (row.id.charAt(0) === "a") {
...@@ -276,13 +274,13 @@ $(function() { ...@@ -276,13 +274,13 @@ $(function() {
$.ajax ({ $.ajax ({
type: "Post", type: "Post",
data: "id=" + row.id + "&type=" + row.type + "&Element=" + encodeURIComponent(row.Element), data: "id=" + row.id + "&type=" + row.type + "&Element=" + encodeURIComponent(row.Element),
url: path + "/../../ajax/deleterestriction/" + type + "/" + user_id, url: getPath() + "/ajax/deleterestriction/" + type + "/" + user_id,
async: true, async: true,
timeout: 900, timeout: 900,
success:function() { success:function() {
$.ajax({ $.ajax({
method:"get", method:"get",
url: path + "/../../ajax/listrestriction/" + type + "/" + user_id, url: getPath() + "/ajax/listrestriction/" + type + "/" + user_id,
async: true, async: true,
timeout: 900, timeout: 900,
success:function(data) { success:function(data) {
...@@ -298,7 +296,7 @@ $(function() { ...@@ -298,7 +296,7 @@ $(function() {
$("#restrict-elements-table").removeClass("table-hover"); $("#restrict-elements-table").removeClass("table-hover");
$("#restrict-elements-table").on("editable-save.bs.table", function (e, field, row) { $("#restrict-elements-table").on("editable-save.bs.table", function (e, field, row) {
$.ajax({ $.ajax({
url: path + "/../../ajax/editrestriction/" + type + "/" + user_id, url: getPath() + "/ajax/editrestriction/" + type + "/" + user_id,
type: "Post", type: "Post",
data: row data: row
}); });
...@@ -306,13 +304,13 @@ $(function() { ...@@ -306,13 +304,13 @@ $(function() {
$("[id^=submit_]").click(function() { $("[id^=submit_]").click(function() {
$(this)[0].blur(); $(this)[0].blur();
$.ajax({ $.ajax({
url: path + "/../../ajax/addrestriction/" + type + "/" + user_id, url: getPath() + "/ajax/addrestriction/" + type + "/" + user_id,
type: "Post", type: "Post",
data: $(this).closest("form").serialize() + "&" + $(this)[0].name + "=", data: $(this).closest("form").serialize() + "&" + $(this)[0].name + "=",
success: function () { success: function () {
$.ajax ({ $.ajax ({
method:"get", method:"get",
url: path + "/../../ajax/listrestriction/" + type + "/" + user_id, url: getPath() + "/ajax/listrestriction/" + type + "/" + user_id,
async: true, async: true,
timeout: 900, timeout: 900,
success:function(data) { success:function(data) {
......
...@@ -171,9 +171,11 @@ ...@@ -171,9 +171,11 @@
</tbody> </tbody>
</table> </table>
{% if feature_support['updater'] %}
<div class="hidden" id="update_error"> <span>{{update_error}}</span></div> <div class="hidden" id="update_error"> <span>{{update_error}}</span></div>
<div class="btn btn-primary" id="check_for_update">{{_('Check for Update')}}</div> <div class="btn btn-primary" id="check_for_update">{{_('Check for Update')}}</div>
<div class="btn btn-primary hidden" id="perform_update" data-toggle="modal" data-target="#StatusDialog">{{_('Perform Update')}}</div> <div class="btn btn-primary hidden" id="perform_update" data-toggle="modal" data-target="#StatusDialog">{{_('Perform Update')}}</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -264,7 +264,7 @@ class Updater(threading.Thread): ...@@ -264,7 +264,7 @@ class Updater(threading.Thread):
# log_from_thread("Delete file " + item_path) # log_from_thread("Delete file " + item_path)
os.remove(item_path) os.remove(item_path)
except OSError: except OSError:
logger.debug("Could not remove: %s", item_path) log.debug("Could not remove: %s", item_path)
shutil.rmtree(source, ignore_errors=True) shutil.rmtree(source, ignore_errors=True)
def is_venv(self): def is_venv(self):
......
This diff is collapsed.
# GDrive Integration # GDrive Integration
google-api-python-client>=1.7.11,<1.8.0 google-api-python-client>=1.7.11,<1.13.0
gevent>=1.2.1,<20.6.0 gevent>20.6.0,<21.2.0
greenlet>=0.4.12,<0.4.17 greenlet>=0.4.17,<1.1.0
httplib2>=0.9.2,<0.18.0 httplib2>=0.9.2,<0.18.0
oauth2client>=4.0.0,<4.1.4 oauth2client>=4.0.0,<4.1.4
uritemplate>=3.0.0,<3.1.0 uritemplate>=3.0.0,<3.1.0
...@@ -17,7 +17,7 @@ goodreads>=0.3.2,<0.4.0 ...@@ -17,7 +17,7 @@ goodreads>=0.3.2,<0.4.0
python-Levenshtein>=0.12.0,<0.13.0 python-Levenshtein>=0.12.0,<0.13.0
# ldap login # ldap login
python-ldap>=3.0.0,<3.3.0 python-ldap>=3.0.0,<3.4.0
Flask-SimpleLDAP>=1.4.0,<1.5.0 Flask-SimpleLDAP>=1.4.0,<1.5.0
#oauth #oauth
...@@ -25,7 +25,7 @@ Flask-Dance>=1.4.0,<3.1.0 ...@@ -25,7 +25,7 @@ Flask-Dance>=1.4.0,<3.1.0
SQLAlchemy-Utils>=0.33.5,<0.37.0 SQLAlchemy-Utils>=0.33.5,<0.37.0
# extracting metadata # extracting metadata
lxml>=3.8.0,<4.6.0 lxml>=3.8.0,<4.7.0
rarfile>=2.7 rarfile>=2.7
# other # other
......
...@@ -37,20 +37,20 @@ ...@@ -37,20 +37,20 @@
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;"> <div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;">
<p class='text-justify attribute'><strong>Start Time: </strong>2021-01-30 17:56:37</p> <p class='text-justify attribute'><strong>Start Time: </strong>2021-02-01 19:02:39</p>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3"> <div class="col-xs-6 col-md-6 col-sm-offset-3">
<p class='text-justify attribute'><strong>Stop Time: </strong>2021-01-30 20:23:25</p> <p class='text-justify attribute'><strong>Stop Time: </strong>2021-02-01 21:32:03</p>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3"> <div class="col-xs-6 col-md-6 col-sm-offset-3">
<p class='text-justify attribute'><strong>Duration: </strong>1h 59 min</p> <p class='text-justify attribute'><strong>Duration: </strong>2h 0 min</p>
</div> </div>
</div> </div>
</div> </div>
...@@ -802,11 +802,11 @@ ...@@ -802,11 +802,11 @@
<tr id="su" class="failClass"> <tr id="su" class="skipClass">
<td>TestEditBooks</td> <td>TestEditBooks</td>
<td class="text-center">33</td> <td class="text-center">33</td>
<td class="text-center">30</td> <td class="text-center">31</td>
<td class="text-center">1</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">2</td> <td class="text-center">2</td>
<td class="text-center"> <td class="text-center">
...@@ -977,33 +977,11 @@ ...@@ -977,33 +977,11 @@
<tr id="ft10.17" class="none bg-danger"> <tr id='pt10.17' class='hiddenRow bg-success'>
<td> <td>
<div class='testcase'>TestEditBooks - test_edit_title</div> <div class='testcase'>TestEditBooks - test_edit_title</div>
</td> </td>
<td colspan='6'> <td colspan='6' align='center'>PASS</td>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft10.17')">FAIL</a>
</div>
<!--css div popup start-->
<div id="div_ft10.17" class="popup_window test_output" style="display:none;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus='this.blur();'
onclick='document.getElementById('div_ft10.17').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File "/home/ozzie/Development/calibre-web-test/test/test_edit_books.py", line 131, in test_edit_title
self.assertEqual(ele.text, u'Very long extra super turbo cool title without any issue of ...')
AssertionError: 'Very[33 chars]e without any issue of displaying including ö utf-8 characters' != 'Very[33 chars]e without any issue of ...'
- Very long extra super turbo cool title without any issue of displaying including ö utf-8 characters
+ Very long extra super turbo cool title without any issue of ...</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr> </tr>
...@@ -1215,8 +1193,8 @@ AssertionError: 'Very[33 chars]e without any issue of displaying including ö ut ...@@ -1215,8 +1193,8 @@ AssertionError: 'Very[33 chars]e without any issue of displaying including ö ut
<tr id="su" class="failClass"> <tr id="su" class="failClass">
<td>TestEditBooksOnGdrive</td> <td>TestEditBooksOnGdrive</td>
<td class="text-center">20</td> <td class="text-center">20</td>
<td class="text-center">18</td> <td class="text-center">19</td>
<td class="text-center">2</td> <td class="text-center">1</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center"> <td class="text-center">
...@@ -1361,33 +1339,11 @@ AssertionError: 'Very[33 chars]e without any issue of displaying including ö ut ...@@ -1361,33 +1339,11 @@ AssertionError: 'Very[33 chars]e without any issue of displaying including ö ut
<tr id="ft12.16" class="none bg-danger"> <tr id='pt12.16' class='hiddenRow bg-success'>
<td> <td>
<div class='testcase'>TestEditBooksOnGdrive - test_edit_title</div> <div class='testcase'>TestEditBooksOnGdrive - test_edit_title</div>
</td> </td>
<td colspan='6'> <td colspan='6' align='center'>PASS</td>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft12.16')">FAIL</a>
</div>
<!--css div popup start-->
<div id="div_ft12.16" class="popup_window test_output" style="display:none;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus='this.blur();'
onclick='document.getElementById('div_ft12.16').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File "/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py", line 227, in test_edit_title
self.assertEqual(ele.text, u'Very long extra super turbo cool title without any issue of ...')
AssertionError: 'Very[33 chars]e without any issue of displaying including ö utf-8 characters' != 'Very[33 chars]e without any issue of ...'
- Very long extra super turbo cool title without any issue of displaying including ö utf-8 characters
+ Very long extra super turbo cool title without any issue of ...</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr> </tr>
...@@ -1438,7 +1394,7 @@ AssertionError: 'Very[33 chars]e without any issue of displaying including ö ut ...@@ -1438,7 +1394,7 @@ AssertionError: 'Very[33 chars]e without any issue of displaying including ö ut
<pre class="text-left">Traceback (most recent call last): <pre class="text-left">Traceback (most recent call last):
File "/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py", line 847, in test_watch_metadata File "/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py", line 847, in test_watch_metadata
self.assertNotIn('series', book) self.assertNotIn('series', book)
AssertionError: 'series' unexpectedly found in {'reader': ['epub'], 'title': 'testbook', 'author': ['John Döe'], 'rating': 0, 'languages': ['English'], 'identifier': [], 'cover': '/cover/5?edit=f84d9deb-4c69-48d6-9f16-94ad43b3e3fa', 'tag': [], 'publisher': ['Randomhäus'], 'comment': '\n', 'add_shelf': [], 'del_shelf': [], 'edit_enable': True, 'kindle': None, 'kindlebtn': None, 'download': ['EPUB (6.7 kB)'], 'read': False, 'archived': False, 'series_all': 'Book 1.0 of test', 'series_index': '1.0', 'series': 'test', 'cust_columns': []}</pre> AssertionError: 'series' unexpectedly found in {'reader': ['epub'], 'title': 'testbook', 'author': ['John Döe'], 'rating': 0, 'languages': ['English'], 'identifier': [], 'cover': '/cover/5?edit=175d2a21-2733-49b6-9fcf-3ef9aee1670b', 'tag': [], 'publisher': ['Randomhäus'], 'comment': '\n', 'add_shelf': [], 'del_shelf': [], 'edit_enable': True, 'kindle': None, 'kindlebtn': None, 'download': ['EPUB (6.7 kB)'], 'read': False, 'archived': False, 'series_all': 'Book 1.0 of test', 'series_index': '1.0', 'series': 'test', 'cust_columns': []}</pre>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
...@@ -3443,8 +3399,8 @@ AssertionError: 'series' unexpectedly found in {'reader': ['epub'], 'title': 'te ...@@ -3443,8 +3399,8 @@ AssertionError: 'series' unexpectedly found in {'reader': ['epub'], 'title': 'te
<tr id='total_row' class="text-center bg-grey"> <tr id='total_row' class="text-center bg-grey">
<td>Total</td> <td>Total</td>
<td>299</td> <td>299</td>
<td>289</td> <td>291</td>
<td>3</td> <td>1</td>
<td>0</td> <td>0</td>
<td>7</td> <td>7</td>
<td>&nbsp;</td> <td>&nbsp;</td>
...@@ -3528,7 +3484,7 @@ AssertionError: 'series' unexpectedly found in {'reader': ['epub'], 'title': 'te ...@@ -3528,7 +3484,7 @@ AssertionError: 'series' unexpectedly found in {'reader': ['epub'], 'title': 'te
<tr> <tr>
<th>Jinja2</th> <th>Jinja2</th>
<td>2.11.2</td> <td>2.11.3</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
...@@ -3540,7 +3496,7 @@ AssertionError: 'series' unexpectedly found in {'reader': ['epub'], 'title': 'te ...@@ -3540,7 +3496,7 @@ AssertionError: 'series' unexpectedly found in {'reader': ['epub'], 'title': 'te
<tr> <tr>
<th>pytz</th> <th>pytz</th>
<td>2020.5</td> <td>2021.1</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
...@@ -3804,7 +3760,7 @@ AssertionError: 'series' unexpectedly found in {'reader': ['epub'], 'title': 'te ...@@ -3804,7 +3760,7 @@ AssertionError: 'series' unexpectedly found in {'reader': ['epub'], 'title': 'te
</div> </div>
<script> <script>
drawCircle(289, 3, 0, 7); drawCircle(291, 1, 0, 7);
</script> </script>
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment