diff --git a/.gitignore b/.gitignore index ff08f874..eb398718 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ gdrive_credentials client_secrets.json gmail.json /.key + +pages/ diff --git a/cps/admin.py b/cps/admin.py index 20e901e3..022acc8e 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -1705,7 +1705,7 @@ def _db_configuration_update_helper(): return _db_configuration_result('{}'.format(ex), gdrive_error) if db_change or not db_valid or not config.db_configured \ - or config.config_calibre_dir != to_save["config_calibre_dir"]: + or config.config_calibre_dir != to_save["config_calibre_dir"]: if not os.path.exists(metadata_db) or not to_save['config_calibre_dir']: return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdrive_error) else: @@ -1728,6 +1728,9 @@ def _db_configuration_update_helper(): calibre_db.update_config(config) if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK): flash(_("DB is not Writeable"), category="warning") + _config_string(to_save, "config_calibre_split_dir") + config.config_calibre_split = to_save.get('config_calibre_split', 0) == "on" + calibre_db.update_config(config) config.save() return _db_configuration_result(None, gdrive_error) diff --git a/cps/config_sql.py b/cps/config_sql.py index 21644ccd..f6c0991c 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -69,6 +69,8 @@ class _Settings(_Base): config_calibre_dir = Column(String) config_calibre_uuid = Column(String) + config_calibre_split = Column(Boolean, default=False) + config_calibre_split_dir = Column(String) config_port = Column(Integer, default=constants.DEFAULT_PORT) config_external_port = Column(Integer, default=constants.DEFAULT_PORT) config_certfile = Column(String) @@ -389,6 +391,9 @@ class ConfigSQL(object): self.db_configured = False self.save() + def get_book_path(self): + return self.config_calibre_split_dir if self.config_calibre_split_dir else self.config_calibre_dir + def store_calibre_uuid(self, calibre_db, Library_table): try: calibre_uuid = calibre_db.session.query(Library_table).one_or_none() diff --git a/cps/editbooks.py b/cps/editbooks.py index 723f72a3..e9bec58a 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -135,7 +135,7 @@ def edit_book(book_id): edited_books_id = book.id modify_date = True title_author_error = helper.update_dir_structure(edited_books_id, - config.config_calibre_dir, + config.get_book_path(), input_authors[0], renamed_author=renamed) if title_author_error: @@ -280,7 +280,7 @@ def upload(): meta.extension.lower()) else: error = helper.update_dir_structure(book_id, - config.config_calibre_dir, + config.get_book_path(), input_authors[0], meta.file_path, title_dir + meta.extension.lower(), @@ -330,7 +330,7 @@ def convert_bookformat(book_id): return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to) - rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(), + rtn = helper.convert_book_format(book_id, config.get_book_path(), book_format_from.upper(), book_format_to.upper(), current_user.name) if rtn is None: @@ -400,7 +400,7 @@ def edit_list_book(param): elif param == 'title': sort_param = book.sort if handle_title_on_edit(book, vals.get('value', "")): - rename_error = helper.update_dir_structure(book.id, config.config_calibre_dir) + rename_error = helper.update_dir_structure(book.id, config.get_book_path()) if not rename_error: ret = Response(json.dumps({'success': True, 'newValue': book.title}), mimetype='application/json') @@ -418,7 +418,7 @@ def edit_list_book(param): mimetype='application/json') elif param == 'authors': input_authors, __, renamed = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true") - rename_error = helper.update_dir_structure(book.id, config.config_calibre_dir, input_authors[0], + rename_error = helper.update_dir_structure(book.id, config.get_book_path(), input_authors[0], renamed_author=renamed) if not rename_error: ret = Response(json.dumps({ @@ -522,10 +522,10 @@ def merge_list_book(): for element in from_book.data: if element.format not in to_file: # create new data entry with: book_id, book_format, uncompressed_size, name - filepath_new = os.path.normpath(os.path.join(config.config_calibre_dir, + filepath_new = os.path.normpath(os.path.join(config.get_book_path(), to_book.path, to_name + "." + element.format.lower())) - filepath_old = os.path.normpath(os.path.join(config.config_calibre_dir, + filepath_old = os.path.normpath(os.path.join(config.get_book_path(), from_book.path, element.name + "." + element.format.lower())) copyfile(filepath_old, filepath_new) @@ -565,7 +565,7 @@ def table_xchange_author_title(): if edited_books_id: # toDo: Handle error - edit_error = helper.update_dir_structure(edited_books_id, config.config_calibre_dir, input_authors[0], + edit_error = helper.update_dir_structure(edited_books_id, config.get_book_path(), input_authors[0], renamed_author=renamed) if modify_date: book.last_modified = datetime.utcnow() @@ -762,7 +762,7 @@ def move_coverfile(meta, db_book): cover_file = meta.cover else: cover_file = os.path.join(constants.STATIC_DIR, 'generic_cover.jpg') - new_cover_path = os.path.join(config.config_calibre_dir, db_book.path) + new_cover_path = os.path.join(config.get_book_path(), db_book.path) try: os.makedirs(new_cover_path, exist_ok=True) copyfile(cover_file, os.path.join(new_cover_path, "cover.jpg")) @@ -848,7 +848,7 @@ def delete_book_from_table(book_id, book_format, json_response): book = calibre_db.get_book(book_id) if book: try: - result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper()) + result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper()) if not result: if json_response: return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id), @@ -1184,7 +1184,7 @@ def upload_single_file(file_request, book, book_id): return False file_name = book.path.rsplit('/', 1)[-1] - filepath = os.path.normpath(os.path.join(config.config_calibre_dir, book.path)) + filepath = os.path.normpath(os.path.join(config.get_book_path(), book.path)) saved_filename = os.path.join(filepath, file_name + '.' + file_ext) # check if file path exists, otherwise create it, copy file to calibre path and delete temp file diff --git a/cps/editpage.py b/cps/editpage.py new file mode 100644 index 00000000..1942bfd2 --- /dev/null +++ b/cps/editpage.py @@ -0,0 +1,108 @@ +import os +import flask +from flask import Blueprint, Flask, abort, request +from functools import wraps +from pathlib import Path +from flask_login import current_user, login_required +from werkzeug.exceptions import NotFound + +from .render_template import render_title_template +from . import logger, config, ub +from .constants import CONFIG_DIR as _CONFIG_DIR + +log = logger.create() + +editpage = Blueprint('editpage', __name__) + +def edit_required(f): + @wraps(f) + def inner(*args, **kwargs): + if current_user.role_edit() or current_user.role_admin(): + return f(*args, **kwargs) + abort(403) + + return inner + +def _get_checkbox(dictionary, field, default): + new_value = dictionary.get(field, default) + convertor = lambda y: y == "on" + new_value = convertor(new_value) + + return new_value + +@editpage.route("/admin/page/", methods=["GET", "POST"]) +@login_required +@edit_required +def edit_page(file): + doc = "" + title = "" + name = "" + icon = "file" + is_enabled = True + order = 0 + position = "0" + + page = ub.session.query(ub.Page).filter(ub.Page.id == file).first() + + try: + title = page.title + name = page.name + icon = page.icon + is_enabled = page.is_enabled + order = page.order + position = page.position + except AttributeError: + if file != "new": + abort(404) + + if request.method == "POST": + to_save = request.form.to_dict() + title = to_save.get("title", "").strip() + name = to_save.get("name", "").strip() + icon = to_save.get("icon", "").strip() + position = to_save.get("position", "").strip() + order = int(to_save.get("order", 0)) + content = to_save.get("content", "").strip() + is_enabled = _get_checkbox(to_save, "is_enabled", True) + + if page: + page.title = title + page.name = name + page.icon = icon + page.is_enabled = is_enabled + page.order = order + page.position = position + ub.session_commit("Page edited {}".format(file)) + else: + new_page = ub.Page(title=title, name=name, icon=icon, is_enabled=is_enabled, order=order, position=position) + ub.session.add(new_page) + ub.session_commit("Page added {}".format(file)) + + if (file == "new"): + file = str(new_page.id) + dir_config_path = os.path.join(_CONFIG_DIR, 'pages') + file_name = Path(name + '.md') + file_path = dir_config_path / file_name + os.makedirs(dir_config_path, exist_ok=True) + + try: + with open(file_path, 'w') as f: + f.write(content) + f.close() + except Exception as ex: + log.error(ex) + + if file != "new": + try: + dir_config_path = Path(_CONFIG_DIR) / 'pages' + file_path = dir_config_path / f"{name}.md" + + with open(file_path, 'r') as f: + doc = f.read() + except NotFound: + log.error("'%s' was accessed but file doesn't exists." % file) + + else: + doc = "## New file\n\nInformation" + + return render_title_template("edit_page.html", title=title, name=name, icon=icon, is_enabled=is_enabled, order=order, position=position, content=doc, file=file) diff --git a/cps/epub.py b/cps/epub.py index 50adba59..b45f3e51 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -48,7 +48,8 @@ def get_epub_layout(book, book_data): 'n': 'urn:oasis:names:tc:opendocument:xmlns:container', 'pkg': 'http://www.idpf.org/2007/opf', } - file_path = os.path.normpath(os.path.join(config.config_calibre_dir, book.path, book_data.name + "." + book_data.format.lower())) + file_path = os.path.normpath(os.path.join(config.get_book_path(), + book.path, book_data.name + "." + book_data.format.lower())) try: epubZip = zipfile.ZipFile(file_path) diff --git a/cps/helper.py b/cps/helper.py index 0c526d01..bd219594 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -781,7 +781,7 @@ def get_book_cover_internal(book, resolution=None): # Send the book cover from the Calibre directory else: - cover_file_path = os.path.join(config.config_calibre_dir, book.path) + cover_file_path = os.path.join(config.get_book_path(), book.path) if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")): return send_from_directory(cover_file_path, "cover.jpg") else: @@ -934,7 +934,7 @@ def save_cover(img, book_path): else: return False, message else: - return save_cover_from_filestorage(os.path.join(config.config_calibre_dir, book_path), "cover.jpg", img) + return save_cover_from_filestorage(os.path.join(config.get_book_path(), book_path), "cover.jpg", img) def do_download_file(book, book_format, client, data, headers): @@ -947,7 +947,7 @@ def do_download_file(book, book_format, client, data, headers): else: abort(404) else: - filename = os.path.join(config.config_calibre_dir, book.path) + filename = os.path.join(config.get_book_path(), book.path) if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)): # ToDo: improve error handling log.error('File not found: %s', os.path.join(filename, data.name + "." + book_format)) diff --git a/cps/kobo.py b/cps/kobo.py index 76530797..63741a62 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -205,7 +205,7 @@ def HandleSyncRequest(): for book in books: formats = [data.format for data in book.Books.data] if 'KEPUB' not in formats and config.config_kepubifypath and 'EPUB' in formats: - helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name) + helper.convert_book_format(book.Books.id, config.get_book_path(), 'EPUB', 'KEPUB', current_user.name) kobo_reading_state = get_or_create_reading_state(book.Books.id) entitlement = { diff --git a/cps/listpages.py b/cps/listpages.py new file mode 100644 index 00000000..8016f5ee --- /dev/null +++ b/cps/listpages.py @@ -0,0 +1,28 @@ +import flask +import json +from flask import Blueprint, jsonify, make_response,abort +from flask_login import current_user, login_required +from functools import wraps +from flask_babel import gettext as _ + +from .render_template import render_title_template +from . import ub, db + +listpages = Blueprint('listpages', __name__) + +def edit_required(f): + @wraps(f) + def inner(*args, **kwargs): + if current_user.role_edit() or current_user.role_admin(): + return f(*args, **kwargs) + abort(403) + + return inner + +@listpages.route("/admin/pages/", methods=["GET"]) +@login_required +@edit_required +def show_list(): + pages = ub.session.query(ub.Page).order_by(ub.Page.position).order_by(ub.Page.order).all() + + return render_title_template('list_pages.html', title=_("Pages List"), page="book_table", pages=pages) diff --git a/cps/main.py b/cps/main.py index 286b2b27..ad990bb7 100644 --- a/cps/main.py +++ b/cps/main.py @@ -36,6 +36,9 @@ def main(): from .gdrive import gdrive from .editbooks import editbook from .about import about + from .page import page + from .listpages import listpages + from .editpage import editpage from .search import search from .search_metadata import meta from .shelf import shelf @@ -65,6 +68,9 @@ def main(): limiter.limit("3/minute",key_func=request_username)(opds) app.register_blueprint(jinjia) app.register_blueprint(about) + app.register_blueprint(page) + app.register_blueprint(listpages) + app.register_blueprint(editpage) app.register_blueprint(shelf) app.register_blueprint(admi) app.register_blueprint(remotelogin) diff --git a/cps/opds.py b/cps/opds.py index 4067712f..b13b0570 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -502,7 +502,7 @@ def render_element_index(database_column, linked_table, folder): entries = entries.join(linked_table).join(db.Books) entries = entries.filter(calibre_db.common_filters()).group_by(func.upper(func.substr(database_column, 1, 1))).all() elements = [] - if off == 0: + if off == 0 and entries: elements.append({'id': "00", 'name': _("All")}) shift = 1 for entry in entries[ diff --git a/cps/page.py b/cps/page.py new file mode 100644 index 00000000..b3e42faa --- /dev/null +++ b/cps/page.py @@ -0,0 +1,38 @@ +import os +import flask +import markdown +from flask import abort +from pathlib import Path +from flask_babel import gettext as _ +from werkzeug.exceptions import NotFound + +from . import logger, config, ub +from .render_template import render_title_template +from .constants import CONFIG_DIR as _CONFIG_DIR + +page = flask.Blueprint('page', __name__) + +log = logger.create() + +@page.route('/page/', methods=['GET']) +def get_page(file): + page = ub.session.query(ub.Page)\ + .filter(ub.Page.name == file)\ + .filter(ub.Page.is_enabled)\ + .first() + + if not page: + log.error(f"'{file}' was accessed but is not enabled or it's not in database.") + abort(404) + + try: + dir_config_path = Path(_CONFIG_DIR) / 'pages' + file_path = dir_config_path / f"{file}.md" + with open(file_path, 'r') as f: + temp_md = f.read() + body = markdown.markdown(temp_md) + + return render_title_template('page.html', body=body, title=page.title, page=page.name) + except NotFound: + log.error("'%s' was accessed but file doesn't exists." % file) + abort(404) diff --git a/cps/render_template.py b/cps/render_template.py index 68b46459..0f6e68ef 100644 --- a/cps/render_template.py +++ b/cps/render_template.py @@ -104,14 +104,24 @@ def get_sidebar_config(kwargs=None): g.shelves_access = ub.session.query(ub.Shelf).filter( or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all() - return sidebar, simple + top_pages = ub.session.query(ub.Page)\ + .filter(ub.Page.position == "1")\ + .filter(ub.Page.is_enabled)\ + .order_by(ub.Page.order) + bottom_pages = ub.session.query(ub.Page)\ + .filter(ub.Page.position == "0")\ + .filter(ub.Page.is_enabled)\ + .order_by(ub.Page.order) + + return sidebar, simple, top_pages, bottom_pages # Returns the template for rendering and includes the instance name def render_title_template(*args, **kwargs): - sidebar, simple = get_sidebar_config(kwargs) + sidebar, simple, top_pages, bottom_pages = get_sidebar_config(kwargs) try: return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, simple=simple, + top_pages=top_pages, bottom_pages=bottom_pages, accept=constants.EXTENSIONS_UPLOAD, *args, **kwargs) except PermissionError: diff --git a/cps/static/js/kthoom.js b/cps/static/js/kthoom.js index 67b18fc1..b7ed6c8b 100644 --- a/cps/static/js/kthoom.js +++ b/cps/static/js/kthoom.js @@ -179,8 +179,9 @@ kthoom.ImageFile = function(file) { }; function updateDirectionButtons(){ - var left, right = 1; - if (currentImage == 0 ) { + var left = 1; + var right = 1; + if (currentImage <= 0 ) { if (settings.direction === 0) { left = 0; } else { diff --git a/cps/tasks/convert.py b/cps/tasks/convert.py old mode 100755 new mode 100644 index df6ae104..7b2c8718 --- a/cps/tasks/convert.py +++ b/cps/tasks/convert.py @@ -62,11 +62,11 @@ class TaskConvert(CalibreTask): df = gdriveutils.getFileFromEbooksFolder(cur_book.path, data.name + "." + self.settings['old_book_format'].lower()) if df: - datafile = os.path.join(config.config_calibre_dir, + datafile = os.path.join(config.get_book_path(), cur_book.path, data.name + "." + self.settings['old_book_format'].lower()) - if not os.path.exists(os.path.join(config.config_calibre_dir, cur_book.path)): - os.makedirs(os.path.join(config.config_calibre_dir, cur_book.path)) + if not os.path.exists(os.path.join(config.get_book_path(), cur_book.path)): + os.makedirs(os.path.join(config.get_book_path(), cur_book.path)) df.GetContentFile(datafile) worker_db.session.close() else: diff --git a/cps/tasks/mail.py b/cps/tasks/mail.py old mode 100755 new mode 100644 index a305b623..36133ccf --- a/cps/tasks/mail.py +++ b/cps/tasks/mail.py @@ -239,7 +239,7 @@ class TaskEmail(CalibreTask): @classmethod def _get_attachment(cls, book_path, filename): """Get file as MIMEBase message""" - calibre_path = config.config_calibre_dir + calibre_path = config.get_book_path() if config.config_use_google_drive: df = gdriveutils.getFileFromEbooksFolder(book_path, filename) if df: diff --git a/cps/tasks/metadata_backup.py b/cps/tasks/metadata_backup.py index 1751feeb..45015ccf 100644 --- a/cps/tasks/metadata_backup.py +++ b/cps/tasks/metadata_backup.py @@ -114,7 +114,7 @@ class TaskBackupMetadata(CalibreTask): True) else: # ToDo: Handle book folder not found or not readable - book_metadata_filepath = os.path.join(config.config_calibre_dir, book.path, 'metadata.opf') + book_metadata_filepath = os.path.join(config.get_book_path(), book.path, 'metadata.opf') # prepare finalize everything and output doc = etree.ElementTree(package) try: diff --git a/cps/tasks/thumbnail.py b/cps/tasks/thumbnail.py index 6d11fe97..dd9ee1e0 100644 --- a/cps/tasks/thumbnail.py +++ b/cps/tasks/thumbnail.py @@ -209,7 +209,7 @@ class TaskGenerateCoverThumbnails(CalibreTask): if stream is not None: stream.close() else: - book_cover_filepath = os.path.join(config.config_calibre_dir, book.path, 'cover.jpg') + book_cover_filepath = os.path.join(config.get_book_path(), book.path, 'cover.jpg') if not os.path.isfile(book_cover_filepath): raise Exception('Book cover file not found') @@ -404,7 +404,7 @@ class TaskGenerateSeriesThumbnails(CalibreTask): if stream is not None: stream.close() - book_cover_filepath = os.path.join(config.config_calibre_dir, book.path, 'cover.jpg') + book_cover_filepath = os.path.join(config.get_book_path(), book.path, 'cover.jpg') if not os.path.isfile(book_cover_filepath): raise Exception('Book cover file not found') diff --git a/cps/templates/admin.html b/cps/templates/admin.html index 4cb69e21..761254f3 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -161,6 +161,7 @@ {{_('Edit Calibre Database Configuration')}} {{_('Edit Basic Configuration')}} {{_('Edit UI Configuration')}} + {{_('List Pages')}} {% if feature_support['scheduler'] %} diff --git a/cps/templates/config_db.html b/cps/templates/config_db.html index 0090bd95..6e54d97c 100644 --- a/cps/templates/config_db.html +++ b/cps/templates/config_db.html @@ -16,6 +16,18 @@ +
+ + +
+
+
+ + + + +
+
{% if feature_support['gdrive'] %}
diff --git a/cps/templates/edit_page.html b/cps/templates/edit_page.html new file mode 100644 index 00000000..e612a133 --- /dev/null +++ b/cps/templates/edit_page.html @@ -0,0 +1,45 @@ +{% extends "layout.html" %} +{% block body %} +
+
{{_('Back')}}
+

{{_('Edit page')}}

+
+ +
+ + +
+
+ + +
+
+ + + {{_('Icons list')}} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + {{_('Cancel')}} +
+
+{% endblock %} diff --git a/cps/templates/layout.html b/cps/templates/layout.html index 1bee1c1d..c4716615 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -142,6 +142,9 @@
@@ -1976,12 +1976,12 @@ IndexError: list index out of range - + TestEditBooksOnGdrive 18 - 17 + 18 + 0 0 - 1 0 Detail @@ -2125,31 +2125,11 @@ IndexError: list index out of range - +
TestEditBooksOnGdrive - test_edit_title
- -
- ERROR -
- - - - + PASS @@ -3092,15 +3072,15 @@ KeyError: 'title' - + TestLogin + 18 17 - 17 - 0 + 1 0 0 - Detail + Detail @@ -3241,7 +3221,36 @@ KeyError: 'title' - + + +
TestLogin - test_proxy_login_multi_user
+ + +
+ FAIL +
+ + + + + + + + +
TestLogin - test_proxy_login_opds
@@ -3250,7 +3259,7 @@ KeyError: 'title' - +
TestLogin - test_robots
@@ -3623,21 +3632,30 @@ KeyError: 'title' - + TestReader - 6 - 5 - 1 + 8 + 7 0 0 + 1 - Detail + Detail - + + +
TestReader - test_cb7_reader
+ + SKIP + + + + +
TestReader - test_comic_MACOS_files
@@ -3646,7 +3664,7 @@ KeyError: 'title' - +
TestReader - test_comic_reader
@@ -3655,7 +3673,7 @@ KeyError: 'title' - +
TestReader - test_epub_reader
@@ -3664,7 +3682,7 @@ KeyError: 'title' - +
TestReader - test_pdf_reader
@@ -3673,42 +3691,25 @@ KeyError: 'title' - + -
TestReader - test_sound_listener
+
TestReader - test_single_file_comic
- -
- FAIL -
- - - + PASS + + + + + + +
TestReader - test_sound_listener
+ PASS - +
TestReader - test_txt_reader
@@ -4048,7 +4049,7 @@ AssertionError: '0:02' != '0:01' - TestThumbnailsEnv + TestSocket 1 1 0 @@ -4062,6 +4063,54 @@ AssertionError: '0:02' != '0:01' + +
TestSocket - test_socket_communication
+ + PASS + + + + + + + TestSystemdActivation + 1 + 1 + 0 + 0 + 0 + + Detail + + + + + + + +
TestSystemdActivation - test_systemd_activation
+ + PASS + + + + + + + TestThumbnailsEnv + 1 + 1 + 0 + 0 + 0 + + Detail + + + + + +
TestThumbnailsEnv - test_cover_cache_env_on_database_change
@@ -4071,21 +4120,21 @@ AssertionError: '0:02' != '0:01' - + TestThumbnails 8 - 6 - 1 + 7 + 0 0 1 - Detail + Detail - +
TestThumbnails - test_cache_non_writable
@@ -4094,7 +4143,7 @@ AssertionError: '0:02' != '0:01' - +
TestThumbnails - test_cache_of_deleted_book
@@ -4103,7 +4152,7 @@ AssertionError: '0:02' != '0:01' - +
TestThumbnails - test_cover_cache_on_database_change
@@ -4112,7 +4161,7 @@ AssertionError: '0:02' != '0:01' - +
TestThumbnails - test_cover_change_on_upload_new_cover
@@ -4121,7 +4170,7 @@ AssertionError: '0:02' != '0:01' - +
TestThumbnails - test_cover_for_series
@@ -4130,7 +4179,7 @@ AssertionError: '0:02' != '0:01' - +
TestThumbnails - test_cover_on_upload_book
@@ -4139,7 +4188,7 @@ AssertionError: '0:02' != '0:01' - +
TestThumbnails - test_remove_cover_from_cache
@@ -4148,31 +4197,11 @@ AssertionError: '0:02' != '0:01' - +
TestThumbnails - test_sideloaded_book
- -
- FAIL -
- - - - + PASS @@ -4186,13 +4215,13 @@ AssertionError: 0.029247650171179584 not greater than or equal to 0.04 0 1 - Detail + Detail - +
TestUpdater - test_check_update_nightly_errors
@@ -4201,7 +4230,7 @@ AssertionError: 0.029247650171179584 not greater than or equal to 0.04 - +
TestUpdater - test_check_update_nightly_request_errors
@@ -4210,7 +4239,7 @@ AssertionError: 0.029247650171179584 not greater than or equal to 0.04 - +
TestUpdater - test_check_update_stable_errors
@@ -4219,7 +4248,7 @@ AssertionError: 0.029247650171179584 not greater than or equal to 0.04 - +
TestUpdater - test_check_update_stable_versions
@@ -4228,7 +4257,7 @@ AssertionError: 0.029247650171179584 not greater than or equal to 0.04 - +
TestUpdater - test_perform_update
@@ -4237,7 +4266,7 @@ AssertionError: 0.029247650171179584 not greater than or equal to 0.04 - +
TestUpdater - test_perform_update_stable_errors
@@ -4246,19 +4275,19 @@ AssertionError: 0.029247650171179584 not greater than or equal to 0.04 - +
TestUpdater - test_perform_update_timeout
- SKIP + SKIP
-