mirror of
				https://github.com/janeczku/calibre-web
				synced 2025-11-04 01:03:02 +00:00 
			
		
		
		
	Merge branch 'Develop':
- Fix for new tornado version - bookmark for comic viewer - Bugfix for showing series containing only one book in list view containing having this book no series_index value set - updated requirements
This commit is contained in:
		@@ -33,7 +33,7 @@ from functools import wraps
 | 
			
		||||
from urllib.parse import urlparse
 | 
			
		||||
 | 
			
		||||
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
 | 
			
		||||
from flask import Markup
 | 
			
		||||
from markupsafe import Markup
 | 
			
		||||
from flask_login import login_required, current_user, logout_user
 | 
			
		||||
from flask_babel import gettext as _
 | 
			
		||||
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
 | 
			
		||||
 
 | 
			
		||||
@@ -663,7 +663,7 @@ class CalibreDB:
 | 
			
		||||
 | 
			
		||||
        cls.session_factory = scoped_session(sessionmaker(autocommit=False,
 | 
			
		||||
                                                          autoflush=True,
 | 
			
		||||
                                                          bind=cls.engine))
 | 
			
		||||
                                                          bind=cls.engine, future=True))
 | 
			
		||||
        for inst in cls.instances:
 | 
			
		||||
            inst.init_session()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -25,16 +25,15 @@ from datetime import datetime
 | 
			
		||||
import json
 | 
			
		||||
from shutil import copyfile
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
from markupsafe import escape  # dependency of flask
 | 
			
		||||
from markupsafe import escape, Markup  # dependency of flask
 | 
			
		||||
from functools import wraps
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    from lxml.html.clean import clean_html, Cleaner
 | 
			
		||||
except ImportError:
 | 
			
		||||
    clean_html = None
 | 
			
		||||
 | 
			
		||||
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
 | 
			
		||||
from flask import Blueprint, request, flash, redirect, url_for, abort, Response
 | 
			
		||||
from flask_babel import gettext as _
 | 
			
		||||
from flask_babel import lazy_gettext as N_
 | 
			
		||||
from flask_babel import get_locale
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								cps/kobo.py
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								cps/kobo.py
									
									
									
									
									
								
							@@ -166,12 +166,6 @@ def HandleSyncRequest():
 | 
			
		||||
    only_kobo_shelves = current_user.kobo_only_shelves_sync
 | 
			
		||||
 | 
			
		||||
    if only_kobo_shelves:
 | 
			
		||||
        #if sqlalchemy_version2:
 | 
			
		||||
        #    changed_entries = select(db.Books,
 | 
			
		||||
        #                             ub.ArchivedBook.last_modified,
 | 
			
		||||
        #                             ub.BookShelf.date_added,
 | 
			
		||||
        #                             ub.ArchivedBook.is_archived)
 | 
			
		||||
        #else:
 | 
			
		||||
        changed_entries = calibre_db.session.query(db.Books,
 | 
			
		||||
                                                   ub.ArchivedBook.last_modified,
 | 
			
		||||
                                                   ub.BookShelf.date_added,
 | 
			
		||||
@@ -192,9 +186,6 @@ def HandleSyncRequest():
 | 
			
		||||
                           .filter(ub.Shelf.kobo_sync)
 | 
			
		||||
                           .distinct())
 | 
			
		||||
    else:
 | 
			
		||||
        #if sqlalchemy_version2:
 | 
			
		||||
        #    changed_entries = select(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
 | 
			
		||||
        #else:
 | 
			
		||||
        changed_entries = calibre_db.session.query(db.Books,
 | 
			
		||||
                                                   ub.ArchivedBook.last_modified,
 | 
			
		||||
                                                   ub.ArchivedBook.is_archived)
 | 
			
		||||
@@ -209,9 +200,6 @@ def HandleSyncRequest():
 | 
			
		||||
                           .order_by(db.Books.id))
 | 
			
		||||
 | 
			
		||||
    reading_states_in_new_entitlements = []
 | 
			
		||||
    #if sqlalchemy_version2:
 | 
			
		||||
    #    books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT))
 | 
			
		||||
    #else:
 | 
			
		||||
    books = changed_entries.limit(SYNC_ITEM_LIMIT)
 | 
			
		||||
    log.debug("Books to Sync: {}".format(len(books.all())))
 | 
			
		||||
    for book in books:
 | 
			
		||||
@@ -255,13 +243,6 @@ def HandleSyncRequest():
 | 
			
		||||
        new_books_last_created = max(ts_created, new_books_last_created)
 | 
			
		||||
        kobo_sync_status.add_synced_books(book.Books.id)
 | 
			
		||||
 | 
			
		||||
    '''if sqlalchemy_version2:
 | 
			
		||||
        max_change = calibre_db.session.execute(changed_entries
 | 
			
		||||
                                                .filter(ub.ArchivedBook.is_archived)
 | 
			
		||||
                                                .filter(ub.ArchivedBook.user_id == current_user.id)
 | 
			
		||||
                                                .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\
 | 
			
		||||
            .columns(db.Books).first()
 | 
			
		||||
    else:'''
 | 
			
		||||
    max_change = changed_entries.filter(ub.ArchivedBook.is_archived)\
 | 
			
		||||
        .filter(ub.ArchivedBook.user_id == current_user.id) \
 | 
			
		||||
        .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first()
 | 
			
		||||
@@ -271,10 +252,6 @@ def HandleSyncRequest():
 | 
			
		||||
    new_archived_last_modified = max(new_archived_last_modified, max_change)
 | 
			
		||||
 | 
			
		||||
    # no. of books returned
 | 
			
		||||
    '''if sqlalchemy_version2:
 | 
			
		||||
        entries = calibre_db.session.execute(changed_entries).all()
 | 
			
		||||
        book_count = len(entries)
 | 
			
		||||
    else:'''
 | 
			
		||||
    book_count = changed_entries.count()
 | 
			
		||||
    # last entry:
 | 
			
		||||
    cont_sync = bool(book_count)
 | 
			
		||||
@@ -523,7 +500,7 @@ def get_metadata(book):
 | 
			
		||||
@requires_kobo_auth
 | 
			
		||||
# Creates a Shelf with the given items, and returns the shelf's uuid.
 | 
			
		||||
def HandleTagCreate():
 | 
			
		||||
    # catch delete requests, otherwise the are handled in the book delete handler
 | 
			
		||||
    # catch delete requests, otherwise they are handled in the book delete handler
 | 
			
		||||
    if request.method == "DELETE":
 | 
			
		||||
        abort(405)
 | 
			
		||||
    name, items = None, None
 | 
			
		||||
@@ -717,14 +694,6 @@ def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
 | 
			
		||||
            })
 | 
			
		||||
        extra_filters.append(ub.Shelf.kobo_sync)
 | 
			
		||||
 | 
			
		||||
    '''if sqlalchemy_version2:
 | 
			
		||||
        shelflist = ub.session.execute(select(ub.Shelf).outerjoin(ub.BookShelf).filter(
 | 
			
		||||
            or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
 | 
			
		||||
                func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),
 | 
			
		||||
            ub.Shelf.user_id == current_user.id,
 | 
			
		||||
            *extra_filters
 | 
			
		||||
        ).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())).columns(ub.Shelf)
 | 
			
		||||
    else:'''
 | 
			
		||||
    shelflist = ub.session.query(ub.Shelf).outerjoin(ub.BookShelf).filter(
 | 
			
		||||
        or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
 | 
			
		||||
            func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),
 | 
			
		||||
 
 | 
			
		||||
@@ -288,4 +288,7 @@ class WebServer(object):
 | 
			
		||||
            if _GEVENT:
 | 
			
		||||
                self.wsgiserver.close()
 | 
			
		||||
            else:
 | 
			
		||||
                self.wsgiserver.add_callback_from_signal(self.wsgiserver.stop)
 | 
			
		||||
                if restart:
 | 
			
		||||
                    self.wsgiserver.call_later(1.0, self.wsgiserver.stop)
 | 
			
		||||
                else:
 | 
			
		||||
                    self.wsgiserver.add_callback_from_signal(self.wsgiserver.stop)
 | 
			
		||||
 
 | 
			
		||||
@@ -19,10 +19,8 @@
 | 
			
		||||
 | 
			
		||||
import sys
 | 
			
		||||
from base64 import b64decode, b64encode
 | 
			
		||||
from jsonschema import validate, exceptions, __version__
 | 
			
		||||
from datetime import datetime, timezone
 | 
			
		||||
 | 
			
		||||
from urllib.parse import unquote
 | 
			
		||||
from jsonschema import validate, exceptions
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
from flask import json
 | 
			
		||||
from .. import logger
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,8 @@ var settings = {
 | 
			
		||||
    fitMode: kthoom.Key.B,
 | 
			
		||||
    theme: "light",
 | 
			
		||||
    direction: 0, // 0 = Left to Right, 1 = Right to Left
 | 
			
		||||
	scrollbar: 1, // 0 = Hide Scrollbar, 1 = Show Scrollbar
 | 
			
		||||
    nextPage: 0, // 0 = Reset to Top, 1 = Remember Position
 | 
			
		||||
	scrollbar: 1, // 0 = Hide Scrollbar, 1 = Show Scrollbar	
 | 
			
		||||
    pageDisplay: 0 // 0 = Single Page, 1 = Long Strip
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -131,8 +132,8 @@ var createURLFromArray = function(array, mimeType) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ((typeof URL !== "function" && typeof URL !== "object") ||
 | 
			
		||||
      typeof URL.createObjectURL !== "function") {
 | 
			
		||||
        throw "Browser support for Object URLs is missing";
 | 
			
		||||
        typeof URL.createObjectURL !== "function") {
 | 
			
		||||
            throw "Browser support for Object URLs is missing";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return URL.createObjectURL(blob);
 | 
			
		||||
@@ -177,12 +178,36 @@ kthoom.ImageFile = function(file) {
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function updateDirectionButtons(){
 | 
			
		||||
    $("#right").show();
 | 
			
		||||
    $("#left").show();
 | 
			
		||||
    if (currentImage == 0 ) {
 | 
			
		||||
        if (settings.direction === 0) {
 | 
			
		||||
            $("#right").show();
 | 
			
		||||
            $("#left").hide();
 | 
			
		||||
        } else {
 | 
			
		||||
            $("#left").show();
 | 
			
		||||
            $("#right").hide();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if ((currentImage + 1) >= Math.max(totalImages, imageFiles.length)) {
 | 
			
		||||
        if (settings.direction === 0) {
 | 
			
		||||
            $("#left").show();
 | 
			
		||||
            $("#right").hide();
 | 
			
		||||
        } else {
 | 
			
		||||
            $("#right").show();
 | 
			
		||||
            $("#left").hide();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
function initProgressClick() {
 | 
			
		||||
    $("#progress").click(function(e) {
 | 
			
		||||
        var offset = $(this).offset();
 | 
			
		||||
        var x = e.pageX - offset.left;
 | 
			
		||||
        var rate = settings.direction === 0 ? x / $(this).width() : 1 - x / $(this).width();
 | 
			
		||||
        currentImage = Math.max(1, Math.ceil(rate * totalImages)) - 1;
 | 
			
		||||
        updateDirectionButtons();
 | 
			
		||||
        setBookmark();
 | 
			
		||||
        updatePage();
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
@@ -222,6 +247,7 @@ function loadFromArrayBuffer(ab) {
 | 
			
		||||
                                    
 | 
			
		||||
                                    // display first page if we haven't yet
 | 
			
		||||
                                    if (imageFiles.length === currentImage + 1) {
 | 
			
		||||
                                        updateDirectionButtons();
 | 
			
		||||
                                        updatePage();
 | 
			
		||||
                                    }
 | 
			
		||||
                                } else {
 | 
			
		||||
@@ -241,7 +267,7 @@ function scrollTocToActive() {
 | 
			
		||||
 | 
			
		||||
    // Mark the current page in the TOC
 | 
			
		||||
    $("#tocView a[data-page]")
 | 
			
		||||
    // Remove the currently active thumbnail
 | 
			
		||||
        // Remove the currently active thumbnail
 | 
			
		||||
        .removeClass("active")
 | 
			
		||||
        // Find the new one
 | 
			
		||||
        .filter("[data-page=" + (currentImage + 1) + "]")
 | 
			
		||||
@@ -409,6 +435,7 @@ function showLeftPage() {
 | 
			
		||||
    } else {
 | 
			
		||||
        showNextPage();
 | 
			
		||||
    }
 | 
			
		||||
    setBookmark();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showRightPage() {
 | 
			
		||||
@@ -417,6 +444,7 @@ function showRightPage() {
 | 
			
		||||
    } else {
 | 
			
		||||
        showPrevPage();
 | 
			
		||||
    }
 | 
			
		||||
    setBookmark();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showPrevPage() {
 | 
			
		||||
@@ -427,6 +455,7 @@ function showPrevPage() {
 | 
			
		||||
    } else {
 | 
			
		||||
        updatePage();
 | 
			
		||||
    }
 | 
			
		||||
    updateDirectionButtons();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showNextPage() {
 | 
			
		||||
@@ -437,6 +466,7 @@ function showNextPage() {
 | 
			
		||||
    } else {
 | 
			
		||||
        updatePage();
 | 
			
		||||
    }
 | 
			
		||||
    updateDirectionButtons();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function scrollCurrentImageIntoView() {
 | 
			
		||||
@@ -621,11 +651,21 @@ function drawCanvas() {
 | 
			
		||||
    $("#mainContent").append(canvasElement);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updateArrows() {
 | 
			
		||||
    if ($('input[name="direction"]:checked').val() === "0") {
 | 
			
		||||
        $("#prev_page_key").html("←");
 | 
			
		||||
        $("#next_page_key").html("→");
 | 
			
		||||
    } else {
 | 
			
		||||
        $("#prev_page_key").html("→");
 | 
			
		||||
        $("#next_page_key").html("←");
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function init(filename) {
 | 
			
		||||
    var request = new XMLHttpRequest();
 | 
			
		||||
    request.open("GET", filename);
 | 
			
		||||
    request.responseType = "arraybuffer";
 | 
			
		||||
    request.addEventListener("load", function() {
 | 
			
		||||
    request.addEventListener("load", function () {
 | 
			
		||||
        if (request.status >= 200 && request.status < 300) {
 | 
			
		||||
            loadFromArrayBuffer(request.response);
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -641,18 +681,18 @@ function init(filename) {
 | 
			
		||||
 | 
			
		||||
    $(document).keydown(keyHandler);
 | 
			
		||||
 | 
			
		||||
    $(window).resize(function() {
 | 
			
		||||
    $(window).resize(function () {
 | 
			
		||||
        updateScale();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Open TOC menu
 | 
			
		||||
    $("#slider").click(function() {
 | 
			
		||||
    $("#slider").click(function () {
 | 
			
		||||
        $("#sidebar").toggleClass("open");
 | 
			
		||||
        $("#main").toggleClass("closed");
 | 
			
		||||
        $(this).toggleClass("icon-menu icon-right");
 | 
			
		||||
 | 
			
		||||
        // We need this in a timeout because if we call it during the CSS transition, IE11 shakes the page ¯\_(ツ)_/¯
 | 
			
		||||
        setTimeout(function() {
 | 
			
		||||
        setTimeout(function () {
 | 
			
		||||
            // Focus on the TOC or the main content area, depending on which is open
 | 
			
		||||
            $("#main:not(.closed) #mainContent, #sidebar.open #tocView").focus();
 | 
			
		||||
            scrollTocToActive();
 | 
			
		||||
@@ -660,12 +700,12 @@ function init(filename) {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Open Settings modal
 | 
			
		||||
    $("#setting").click(function() {
 | 
			
		||||
    $("#setting").click(function () {
 | 
			
		||||
        $("#settings-modal").toggleClass("md-show");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // On Settings input change
 | 
			
		||||
    $("#settings input").on("change", function() {
 | 
			
		||||
    $("#settings input").on("change", function () {
 | 
			
		||||
        // Get either the checked boolean or the assigned value
 | 
			
		||||
        var value = this.type === "checkbox" ? this.checked : this.value;
 | 
			
		||||
 | 
			
		||||
@@ -674,39 +714,40 @@ function init(filename) {
 | 
			
		||||
 | 
			
		||||
        settings[this.name] = value;
 | 
			
		||||
 | 
			
		||||
        if(["hflip", "vflip", "rotateTimes"].includes(this.name)) {
 | 
			
		||||
        if (["hflip", "vflip", "rotateTimes"].includes(this.name)) {
 | 
			
		||||
            reloadImages();
 | 
			
		||||
        } else if(this.name === "direction") {
 | 
			
		||||
        } else if (this.name === "direction") {
 | 
			
		||||
            updateDirectionButtons();
 | 
			
		||||
            return updateProgress();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        updatePage();
 | 
			
		||||
        updateScale();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Close modal
 | 
			
		||||
    $(".closer, .overlay").click(function() {
 | 
			
		||||
    $(".closer, .overlay").click(function () {
 | 
			
		||||
        $(".md-show").removeClass("md-show");
 | 
			
		||||
		$("#mainContent").focus(); // focus back on the main container so you use up/down keys without having to click on it
 | 
			
		||||
        $("#mainContent").focus(); // focus back on the main container so you use up/down keys without having to click on it
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // TOC thumbnail pagination
 | 
			
		||||
    $("#thumbnails").on("click", "a", function() {
 | 
			
		||||
    $("#thumbnails").on("click", "a", function () {
 | 
			
		||||
        currentImage = $(this).data("page") - 1;
 | 
			
		||||
        updatePage();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Fullscreen mode
 | 
			
		||||
    if (typeof screenfull !== "undefined") {
 | 
			
		||||
        $("#fullscreen").click(function() {
 | 
			
		||||
        $("#fullscreen").click(function () {
 | 
			
		||||
            screenfull.toggle($("#container")[0]);
 | 
			
		||||
			// Focus on main container so you can use up/down keys immediately after fullscreen
 | 
			
		||||
			$("#mainContent").focus();
 | 
			
		||||
            // Focus on main container so you can use up/down keys immediately after fullscreen
 | 
			
		||||
            $("#mainContent").focus();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (screenfull.raw) {
 | 
			
		||||
            var $button = $("#fullscreen");
 | 
			
		||||
            document.addEventListener(screenfull.raw.fullscreenchange, function() {
 | 
			
		||||
            document.addEventListener(screenfull.raw.fullscreenchange, function () {
 | 
			
		||||
                screenfull.isFullscreen
 | 
			
		||||
                    ? $button.addClass("icon-resize-small").removeClass("icon-resize-full")
 | 
			
		||||
                    : $button.addClass("icon-resize-full").removeClass("icon-resize-small");
 | 
			
		||||
@@ -717,16 +758,16 @@ function init(filename) {
 | 
			
		||||
    // Focus the scrollable area so that keyboard scrolling work as expected
 | 
			
		||||
    $("#mainContent").focus();
 | 
			
		||||
 | 
			
		||||
    $("#mainContent").swipe( {
 | 
			
		||||
        swipeRight:function() {
 | 
			
		||||
    $("#mainContent").swipe({
 | 
			
		||||
        swipeRight: function () {
 | 
			
		||||
            showLeftPage();
 | 
			
		||||
        },
 | 
			
		||||
        swipeLeft:function() {
 | 
			
		||||
        swipeLeft: function () {
 | 
			
		||||
            showRightPage();
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
    $(".mainImage").click(function(evt) {
 | 
			
		||||
        // Firefox does not support offsetX/Y so we have to manually calculate
 | 
			
		||||
    $(".mainImage").click(function (evt) {
 | 
			
		||||
        // Firefox does not support offsetX/Y, so we have to manually calculate
 | 
			
		||||
        // where the user clicked in the image.
 | 
			
		||||
        var mainContentWidth = $("#mainContent").width();
 | 
			
		||||
        var mainContentHeight = $("#mainContent").height();
 | 
			
		||||
@@ -762,30 +803,38 @@ function init(filename) {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Scrolling up/down will update current image if a new image is into view (for Long Strip Display)
 | 
			
		||||
    $("#mainContent").scroll(function(){
 | 
			
		||||
    $("#mainContent").scroll(function (){
 | 
			
		||||
        var scroll = $("#mainContent").scrollTop();
 | 
			
		||||
        if(settings.pageDisplay === 0) {
 | 
			
		||||
        var viewLength = 0;
 | 
			
		||||
        $(".mainImage").each(function(){
 | 
			
		||||
            viewLength += $(this).height();
 | 
			
		||||
        });
 | 
			
		||||
        if (settings.pageDisplay === 0) {
 | 
			
		||||
            // Don't trigger the scroll for Single Page
 | 
			
		||||
        } else if(scroll > prevScrollPosition) {
 | 
			
		||||
        } else if (scroll > prevScrollPosition) {
 | 
			
		||||
            //Scroll Down
 | 
			
		||||
            if(currentImage + 1 < imageFiles.length) {
 | 
			
		||||
                if(currentImageOffset(currentImage + 1) <= 1) {
 | 
			
		||||
                    currentImage++;
 | 
			
		||||
            if (currentImage + 1 < imageFiles.length) {
 | 
			
		||||
                if (currentImageOffset(currentImage + 1) <= 1) {
 | 
			
		||||
                    currentImage = Math.floor((imageFiles.length) / (viewLength-viewLength/(imageFiles.length)) * scroll, 0);
 | 
			
		||||
                    if ( currentImage >= imageFiles.length) {
 | 
			
		||||
                        currentImage = imageFiles.length - 1;
 | 
			
		||||
                    }
 | 
			
		||||
                    console.log(currentImage);
 | 
			
		||||
                    scrollTocToActive();
 | 
			
		||||
                    updateProgress();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            //Scroll Up
 | 
			
		||||
            if(currentImage - 1 > -1 ) {
 | 
			
		||||
                if(currentImageOffset(currentImage - 1) >= 0) {
 | 
			
		||||
                    currentImage--;
 | 
			
		||||
            if (currentImage - 1 > -1) {
 | 
			
		||||
                if (currentImageOffset(currentImage - 1) >= 0) {
 | 
			
		||||
                    currentImage = Math.floor((imageFiles.length) / (viewLength-viewLength/(imageFiles.length)) * scroll, 0);
 | 
			
		||||
                    console.log(currentImage);
 | 
			
		||||
                    scrollTocToActive();
 | 
			
		||||
                    updateProgress();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Update scroll position
 | 
			
		||||
        prevScrollPosition = scroll;
 | 
			
		||||
    });
 | 
			
		||||
@@ -794,3 +843,31 @@ function init(filename) {
 | 
			
		||||
function currentImageOffset(imageIndex) {
 | 
			
		||||
    return $(".mainImage").eq(imageIndex).offset().top - $("#mainContent").position().top
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setBookmark() {
 | 
			
		||||
  // get csrf_token
 | 
			
		||||
    let csrf_token = $("input[name='csrf_token']").val();
 | 
			
		||||
    //This sends a bookmark update to calibreweb.
 | 
			
		||||
    $.ajax(calibre.bookmarkUrl, {
 | 
			
		||||
        method: "post",
 | 
			
		||||
        data: {
 | 
			
		||||
            csrf_token: csrf_token,
 | 
			
		||||
            bookmark: currentImage
 | 
			
		||||
        }
 | 
			
		||||
    }).fail(function (xhr, status, error) {
 | 
			
		||||
        console.error(error);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$(function() {
 | 
			
		||||
    $('input[name="direction"]').change(function () {
 | 
			
		||||
        updateArrows();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('#left').click(function () {
 | 
			
		||||
        showLeftPage();
 | 
			
		||||
    });
 | 
			
		||||
    $('#right').click(function () {
 | 
			
		||||
        showRightPage();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -333,7 +333,6 @@ $(function() {
 | 
			
		||||
                } else {
 | 
			
		||||
                    $("#parent").addClass('hidden')
 | 
			
		||||
                }
 | 
			
		||||
                // console.log(data);
 | 
			
		||||
                data.files.forEach(function(entry) {
 | 
			
		||||
                    if(entry.type === "dir") {
 | 
			
		||||
                        var type = "<span class=\"glyphicon glyphicon-folder-close\"></span>";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8">
 | 
			
		||||
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
 | 
			
		||||
@@ -20,23 +21,6 @@
 | 
			
		||||
  <script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
 | 
			
		||||
  <script src="{{ url_for('static', filename='js/compress/uncompress.js') }}"></script>
 | 
			
		||||
  <script src="{{ url_for('static', filename='js/kthoom.js') }}"></script>
 | 
			
		||||
  <script>
 | 
			
		||||
    var updateArrows = function() {
 | 
			
		||||
      if ($('input[name="direction"]:checked').val() === "0") {
 | 
			
		||||
        $("#prev_page_key").html("←");
 | 
			
		||||
        $("#next_page_key").html("→");
 | 
			
		||||
      } else {
 | 
			
		||||
        $("#prev_page_key").html("→");
 | 
			
		||||
        $("#next_page_key").html("←");
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    document.onreadystatechange = function () {
 | 
			
		||||
      if (document.readyState == "complete") {
 | 
			
		||||
      	init("{{ url_for('web.serve_book', book_id=comicfile, book_format=extension) }}");
 | 
			
		||||
      	updateArrows();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  </script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<div id="sidebar">
 | 
			
		||||
@@ -77,8 +61,8 @@
 | 
			
		||||
  <div id="mainContent" tabindex="-1">
 | 
			
		||||
    <div id="mainText" style="display:none"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div id="left" class="arrow" onclick="showLeftPage()">‹</div>
 | 
			
		||||
  <div id="right" class="arrow" onclick="showRightPage()">›</div>
 | 
			
		||||
  <div id="left" class="arrow" style="display:none">‹</div>
 | 
			
		||||
  <div id="right" class="arrow" style="display:none">›</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="modal md-effect-1" id="settings-modal">
 | 
			
		||||
@@ -89,8 +73,8 @@
 | 
			
		||||
        <table>
 | 
			
		||||
          <thead>
 | 
			
		||||
          <tr><th colspan="2">{{_('Keyboard Shortcuts')}}</th></tr>
 | 
			
		||||
          </thead>
 | 
			
		||||
          <tbody>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody>
 | 
			
		||||
          <tr><td id="prev_page_key">←</td> <td>{{_('Previous Page')}}</td></tr>
 | 
			
		||||
          <tr><td id="next_page_key">→</td> <td>{{_('Next Page')}}</td></tr>
 | 
			
		||||
          <tr><td>S</td>      <td>{{_('Single Page Display')}}</td></tr>
 | 
			
		||||
@@ -102,21 +86,21 @@
 | 
			
		||||
          <tr><td>R</td>      <td>{{_('Rotate Right')}}</td></tr>
 | 
			
		||||
          <tr><td>L</td>      <td>{{_('Rotate Left')}}</td></tr>
 | 
			
		||||
          <tr><td>F</td>      <td>{{_('Flip Image')}}</td></tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="settings-column">
 | 
			
		||||
        <table id="settings">
 | 
			
		||||
          <thead>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th>{{_('Settings')}}</th>
 | 
			
		||||
          </tr>
 | 
			
		||||
          </thead>
 | 
			
		||||
          <tbody>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th>{{_('Theme')}}:</th>
 | 
			
		||||
            <td>
 | 
			
		||||
              <div class="inputs">
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="settings-column">
 | 
			
		||||
          <table id="settings">
 | 
			
		||||
            <thead>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <th>{{_('Settings')}}</th>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <th>{{_('Theme')}}:</th>
 | 
			
		||||
                <td>
 | 
			
		||||
                  <div class="inputs">
 | 
			
		||||
                <label for="lightTheme"><input type="radio" id="lightTheme" name="theme" value="light" /> {{_('Light')}}</label>
 | 
			
		||||
                <label for="darkTheme"><input type="radio" id="darkTheme" name="theme" value="dark" /> {{_('Dark')}}</label>
 | 
			
		||||
              </div>
 | 
			
		||||
@@ -139,59 +123,83 @@
 | 
			
		||||
                <label for="fitWidth"><input type="radio" id="fitWidth" name="fitMode" value="87" /> {{_('Width')}}</label>
 | 
			
		||||
                <label for="fitHeight"><input type="radio" id="fitHeight" name="fitMode" value="72" /> {{_('Height')}}</label>
 | 
			
		||||
                <label for="fitNative"><input type="radio" id="fitNative" name="fitMode" value="78" /> {{_('Native')}}</label>
 | 
			
		||||
              </div>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th>{{_('Rotate')}}:</th>
 | 
			
		||||
            <td>
 | 
			
		||||
              <div class="inputs">
 | 
			
		||||
                <label for="r0"><input type="radio" id="r0" name="rotateTimes" value="0" /> 0°</label>
 | 
			
		||||
                <label for="r90"><input type="radio" id="r90" name="rotateTimes" value="1" /> 90°</label>
 | 
			
		||||
                <label for="r180"><input type="radio" id="r180" name="rotateTimes" value="2" /> 180°</label>
 | 
			
		||||
                <label for="r270"><input type="radio" id="r270" name="rotateTimes" value="3" /> 270°</label>
 | 
			
		||||
              </div>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th>{{_('Flip')}}:</th>
 | 
			
		||||
            <td>
 | 
			
		||||
              <div class="inputs">
 | 
			
		||||
                <label for="vflip"><input type="checkbox" id="vflip" name="vflip" /> {{_('Horizontal')}}</label>
 | 
			
		||||
                <label for="hflip"><input type="checkbox" id="hflip" name="hflip" /> {{_('Vertical')}}</label>
 | 
			
		||||
              </div>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th>{{_('Direction')}}:</th>
 | 
			
		||||
            <td>
 | 
			
		||||
              <div class="inputs">
 | 
			
		||||
                  </div>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <th>{{_('Rotate')}}:</th>
 | 
			
		||||
                <td>
 | 
			
		||||
                  <div class="inputs">
 | 
			
		||||
                    <label for="r0"><input type="radio" id="r0" name="rotateTimes" value="0" /> 0°</label>
 | 
			
		||||
                    <label for="r90"><input type="radio" id="r90" name="rotateTimes" value="1" /> 90°</label>
 | 
			
		||||
                    <label for="r180"><input type="radio" id="r180" name="rotateTimes" value="2" /> 180°</label>
 | 
			
		||||
                    <label for="r270"><input type="radio" id="r270" name="rotateTimes" value="3" /> 270°</label>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <th>{{_('Flip')}}:</th>
 | 
			
		||||
                <td>
 | 
			
		||||
                  <div class="inputs">
 | 
			
		||||
                    <label for="vflip"><input type="checkbox" id="vflip" name="vflip" /> {{_('Horizontal')}}</label>
 | 
			
		||||
                    <label for="hflip"><input type="checkbox" id="hflip" name="hflip" /> {{_('Vertical')}}</label>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <th>{{_('Direction')}}:</th>
 | 
			
		||||
                <td>
 | 
			
		||||
                  <div class="inputs">
 | 
			
		||||
                <label for="leftToRight"><input type="radio" id="leftToRight" name="direction" value="0" /> {{_('Left to Right')}}</label>
 | 
			
		||||
                <label for="rightToLeft"><input type="radio" id="rightToLeft" name="direction" value="1" /> {{_('Right to Left')}}</label>
 | 
			
		||||
              </div>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th>{{_('Next Page')}}:</th>
 | 
			
		||||
            <td>
 | 
			
		||||
              <div class="inputs">
 | 
			
		||||
                <label for="resetToTop"><input type="radio" id="resetToTop" name="nextPage" value="0" /> {{_('Reset to Top')}}</label>
 | 
			
		||||
                <label for="rememberPosition"><input type="radio" id="rememberPosition" name="nextPage" value="1" /> {{_('Remember Position')}}</label>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>                
 | 
			
		||||
            <th>{{_('Scrollbar')}}:</th>
 | 
			
		||||
            <td>
 | 
			
		||||
              <div class="inputs">
 | 
			
		||||
                <label for="showScrollbar"><input type="radio" id="showScrollbar" name="scrollbar" value="1" /> {{_('Show')}}</label>
 | 
			
		||||
                <label for="hideScrollbar"><input type="radio" id="hideScrollbar" name="scrollbar" value="0" /> {{_('Hide')}}</label>
 | 
			
		||||
              </div>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="closer icon-cancel-circled"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="closer icon-cancel-circled"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="overlay"></div>
 | 
			
		||||
<script>
 | 
			
		||||
  $('input[name="direction"]').change(function() {
 | 
			
		||||
    updateArrows();
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
  <div class="overlay"></div>
 | 
			
		||||
  <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
			
		||||
  <script>
 | 
			
		||||
    window.calibre = {
 | 
			
		||||
        bookmarkUrl: "{{ url_for('web.set_bookmark', book_id=comicfile, book_format=extension.upper()) }}",
 | 
			
		||||
        bookmark: "{{ bookmark.bookmark_key if bookmark != None }}",
 | 
			
		||||
        useBookmarks: "{{ current_user.is_authenticated | tojson }}"
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    document.onreadystatechange = function () {
 | 
			
		||||
      if (document.readyState == "complete") {
 | 
			
		||||
          if (calibre.useBookmarks) {
 | 
			
		||||
              currentImage = eval(calibre.bookmark);
 | 
			
		||||
              if (typeof currentImage !== 'number') {
 | 
			
		||||
                  currentImage = 0;
 | 
			
		||||
              }
 | 
			
		||||
          }
 | 
			
		||||
          init("{{ url_for('web.serve_book', book_id=comicfile, book_format=extension) }}");
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
@@ -16,12 +16,12 @@
 | 
			
		||||
#  You should have received a copy of the GNU General Public License
 | 
			
		||||
#  along with this program. If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from tornado.wsgi import WSGIContainer
 | 
			
		||||
import tornado
 | 
			
		||||
 | 
			
		||||
from tornado import escape
 | 
			
		||||
from tornado import httputil
 | 
			
		||||
from tornado.ioloop import IOLoop
 | 
			
		||||
 | 
			
		||||
from typing import List, Tuple, Optional, Callable, Any, Dict, Text
 | 
			
		||||
from types import TracebackType
 | 
			
		||||
@@ -34,61 +34,67 @@ if typing.TYPE_CHECKING:
 | 
			
		||||
class MyWSGIContainer(WSGIContainer):
 | 
			
		||||
 | 
			
		||||
    def __call__(self, request: httputil.HTTPServerRequest) -> None:
 | 
			
		||||
        data = {}  # type: Dict[str, Any]
 | 
			
		||||
        response = []  # type: List[bytes]
 | 
			
		||||
        if tornado.version_info < (6, 3, 0, -99):
 | 
			
		||||
            data = {}  # type: Dict[str, Any]
 | 
			
		||||
            response = []  # type: List[bytes]
 | 
			
		||||
 | 
			
		||||
        def start_response(
 | 
			
		||||
            status: str,
 | 
			
		||||
            headers: List[Tuple[str, str]],
 | 
			
		||||
            exc_info: Optional[
 | 
			
		||||
                Tuple[
 | 
			
		||||
                    "Optional[Type[BaseException]]",
 | 
			
		||||
                    Optional[BaseException],
 | 
			
		||||
                    Optional[TracebackType],
 | 
			
		||||
                ]
 | 
			
		||||
            ] = None,
 | 
			
		||||
        ) -> Callable[[bytes], Any]:
 | 
			
		||||
            data["status"] = status
 | 
			
		||||
            data["headers"] = headers
 | 
			
		||||
            return response.append
 | 
			
		||||
            def start_response(
 | 
			
		||||
                status: str,
 | 
			
		||||
                headers: List[Tuple[str, str]],
 | 
			
		||||
                exc_info: Optional[
 | 
			
		||||
                    Tuple[
 | 
			
		||||
                        "Optional[Type[BaseException]]",
 | 
			
		||||
                        Optional[BaseException],
 | 
			
		||||
                        Optional[TracebackType],
 | 
			
		||||
                    ]
 | 
			
		||||
                ] = None,
 | 
			
		||||
            ) -> Callable[[bytes], Any]:
 | 
			
		||||
                data["status"] = status
 | 
			
		||||
                data["headers"] = headers
 | 
			
		||||
                return response.append
 | 
			
		||||
 | 
			
		||||
        app_response = self.wsgi_application(
 | 
			
		||||
            MyWSGIContainer.environ(request), start_response
 | 
			
		||||
        )
 | 
			
		||||
            app_response = self.wsgi_application(
 | 
			
		||||
                MyWSGIContainer.environ(self, request), start_response
 | 
			
		||||
            )
 | 
			
		||||
            try:
 | 
			
		||||
                response.extend(app_response)
 | 
			
		||||
                body = b"".join(response)
 | 
			
		||||
            finally:
 | 
			
		||||
                if hasattr(app_response, "close"):
 | 
			
		||||
                    app_response.close()  # type: ignore
 | 
			
		||||
            if not data:
 | 
			
		||||
                raise Exception("WSGI app did not call start_response")
 | 
			
		||||
 | 
			
		||||
            status_code_str, reason = data["status"].split(" ", 1)
 | 
			
		||||
            status_code = int(status_code_str)
 | 
			
		||||
            headers = data["headers"]  # type: List[Tuple[str, str]]
 | 
			
		||||
            header_set = set(k.lower() for (k, v) in headers)
 | 
			
		||||
            body = escape.utf8(body)
 | 
			
		||||
            if status_code != 304:
 | 
			
		||||
                if "content-length" not in header_set:
 | 
			
		||||
                    headers.append(("Content-Length", str(len(body))))
 | 
			
		||||
                if "content-type" not in header_set:
 | 
			
		||||
                    headers.append(("Content-Type", "text/html; charset=UTF-8"))
 | 
			
		||||
            if "server" not in header_set:
 | 
			
		||||
                headers.append(("Server", "TornadoServer/%s" % tornado.version))
 | 
			
		||||
 | 
			
		||||
            start_line = httputil.ResponseStartLine("HTTP/1.1", status_code, reason)
 | 
			
		||||
            header_obj = httputil.HTTPHeaders()
 | 
			
		||||
            for key, value in headers:
 | 
			
		||||
                header_obj.add(key, value)
 | 
			
		||||
            assert request.connection is not None
 | 
			
		||||
            request.connection.write_headers(start_line, header_obj, chunk=body)
 | 
			
		||||
            request.connection.finish()
 | 
			
		||||
            self._log(status_code, request)
 | 
			
		||||
        else:
 | 
			
		||||
            IOLoop.current().spawn_callback(self.handle_request, request)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def environ(self, request: httputil.HTTPServerRequest) -> Dict[Text, Any]:
 | 
			
		||||
        try:
 | 
			
		||||
            response.extend(app_response)
 | 
			
		||||
            body = b"".join(response)
 | 
			
		||||
        finally:
 | 
			
		||||
            if hasattr(app_response, "close"):
 | 
			
		||||
                app_response.close()  # type: ignore
 | 
			
		||||
        if not data:
 | 
			
		||||
            raise Exception("WSGI app did not call start_response")
 | 
			
		||||
 | 
			
		||||
        status_code_str, reason = data["status"].split(" ", 1)
 | 
			
		||||
        status_code = int(status_code_str)
 | 
			
		||||
        headers = data["headers"]  # type: List[Tuple[str, str]]
 | 
			
		||||
        header_set = set(k.lower() for (k, v) in headers)
 | 
			
		||||
        body = escape.utf8(body)
 | 
			
		||||
        if status_code != 304:
 | 
			
		||||
            if "content-length" not in header_set:
 | 
			
		||||
                headers.append(("Content-Length", str(len(body))))
 | 
			
		||||
            if "content-type" not in header_set:
 | 
			
		||||
                headers.append(("Content-Type", "text/html; charset=UTF-8"))
 | 
			
		||||
        if "server" not in header_set:
 | 
			
		||||
            headers.append(("Server", "TornadoServer/%s" % tornado.version))
 | 
			
		||||
 | 
			
		||||
        start_line = httputil.ResponseStartLine("HTTP/1.1", status_code, reason)
 | 
			
		||||
        header_obj = httputil.HTTPHeaders()
 | 
			
		||||
        for key, value in headers:
 | 
			
		||||
            header_obj.add(key, value)
 | 
			
		||||
        assert request.connection is not None
 | 
			
		||||
        request.connection.write_headers(start_line, header_obj, chunk=body)
 | 
			
		||||
        request.connection.finish()
 | 
			
		||||
        self._log(status_code, request)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def environ(request: httputil.HTTPServerRequest) -> Dict[Text, Any]:
 | 
			
		||||
        environ = WSGIContainer.environ(request)
 | 
			
		||||
            environ = WSGIContainer.environ(self, request)
 | 
			
		||||
        except TypeError as e:
 | 
			
		||||
            environ = WSGIContainer.environ(request)
 | 
			
		||||
        environ['RAW_URI'] = request.path
 | 
			
		||||
        return environ
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1014,7 +1014,7 @@ def series_list():
 | 
			
		||||
                                                func.max(db.Books.series_index), db.Books.id)
 | 
			
		||||
                       .join(db.books_series_link).join(db.Series).filter(calibre_db.common_filters())
 | 
			
		||||
                       .group_by(text('books_series_link.series'))
 | 
			
		||||
                       .having(func.max(db.Books.series_index))
 | 
			
		||||
                       .having(or_(func.max(db.Books.series_index), db.Books.series_index==""))
 | 
			
		||||
                       .order_by(order)
 | 
			
		||||
                       .all())
 | 
			
		||||
            return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=char_list,
 | 
			
		||||
@@ -1569,7 +1569,7 @@ def read_book(book_id, book_format):
 | 
			
		||||
                        title = title + " #" + '{0:.2f}'.format(book.series_index).rstrip('0').rstrip('.')
 | 
			
		||||
                log.debug("Start comic reader for %d", book_id)
 | 
			
		||||
                return render_title_template('readcbr.html', comicfile=all_name, title=title,
 | 
			
		||||
                                             extension=fileExt)
 | 
			
		||||
                                             extension=fileExt, bookmark=bookmark)
 | 
			
		||||
        log.debug("Selected book is unavailable. File does not exist or is not accessible")
 | 
			
		||||
        flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
 | 
			
		||||
              category="error")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +1,31 @@
 | 
			
		||||
# GDrive Integration
 | 
			
		||||
google-api-python-client>=1.7.11,<2.90.0
 | 
			
		||||
gevent>20.6.0,<23.0.0
 | 
			
		||||
google-api-python-client>=1.7.11,<2.98.0
 | 
			
		||||
gevent>20.6.0,<24.0.0
 | 
			
		||||
greenlet>=0.4.17,<2.1.0
 | 
			
		||||
httplib2>=0.9.2,<0.23.0
 | 
			
		||||
oauth2client>=4.0.0,<4.1.4
 | 
			
		||||
uritemplate>=3.0.0,<4.2.0
 | 
			
		||||
pyasn1-modules>=0.0.8,<0.4.0
 | 
			
		||||
pyasn1>=0.1.9,<0.6.0
 | 
			
		||||
PyDrive2>=1.3.1,<1.16.0
 | 
			
		||||
PyYAML>=3.12
 | 
			
		||||
PyDrive2>=1.3.1,<1.18.0
 | 
			
		||||
PyYAML>=3.12,<6.1
 | 
			
		||||
rsa>=3.4.2,<4.10.0
 | 
			
		||||
 | 
			
		||||
# Gmail
 | 
			
		||||
google-auth-oauthlib>=0.4.3,<0.9.0
 | 
			
		||||
google-api-python-client>=1.7.11,<2.90.0
 | 
			
		||||
google-auth-oauthlib>=0.4.3,<1.1.0
 | 
			
		||||
google-api-python-client>=1.7.11,<2.98.0
 | 
			
		||||
 | 
			
		||||
# goodreads
 | 
			
		||||
goodreads>=0.3.2,<0.4.0
 | 
			
		||||
python-Levenshtein>=0.12.0,<0.21.0
 | 
			
		||||
python-Levenshtein>=0.12.0,<0.22.0
 | 
			
		||||
 | 
			
		||||
# ldap login
 | 
			
		||||
python-ldap>=3.0.0,<3.5.0
 | 
			
		||||
Flask-SimpleLDAP>=1.4.0,<1.5.0
 | 
			
		||||
 | 
			
		||||
# oauth
 | 
			
		||||
Flask-Dance>=2.0.0,<6.3.0
 | 
			
		||||
SQLAlchemy-Utils>=0.33.5,<0.40.0
 | 
			
		||||
Flask-Dance>=2.0.0,<7.1.0
 | 
			
		||||
SQLAlchemy-Utils>=0.33.5,<0.42.0
 | 
			
		||||
 | 
			
		||||
# metadata extraction
 | 
			
		||||
rarfile>=3.2
 | 
			
		||||
@@ -33,8 +33,8 @@ scholarly>=1.2.0,<1.8
 | 
			
		||||
markdown2>=2.0.0,<2.5.0
 | 
			
		||||
html2text>=2020.1.16,<2022.1.1
 | 
			
		||||
python-dateutil>=2.1,<2.9.0
 | 
			
		||||
beautifulsoup4>=4.0.1,<4.12.0
 | 
			
		||||
faust-cchardet>=2.1.18
 | 
			
		||||
beautifulsoup4>=4.0.1,<4.13.0
 | 
			
		||||
faust-cchardet>=2.1.18,<2.1.20
 | 
			
		||||
py7zr>=0.15.0,<0.21.0
 | 
			
		||||
 | 
			
		||||
# Comics
 | 
			
		||||
@@ -42,4 +42,4 @@ natsort>=2.2.0,<8.4.0
 | 
			
		||||
comicapi>=2.2.0,<3.3.0
 | 
			
		||||
 | 
			
		||||
# Kobo integration
 | 
			
		||||
jsonschema>=3.2.0,<4.18.0
 | 
			
		||||
jsonschema>=3.2.0,<4.20.0
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,20 @@
 | 
			
		||||
Werkzeug<3.0.0
 | 
			
		||||
APScheduler>=3.6.3,<3.11.0
 | 
			
		||||
Babel>=1.3,<3.0
 | 
			
		||||
Flask-Babel>=0.11.1,<3.1.0
 | 
			
		||||
Flask-Babel>=0.11.1,<3.2.0
 | 
			
		||||
Flask-Login>=0.3.2,<0.6.3
 | 
			
		||||
Flask-Principal>=0.3.2,<0.5.1
 | 
			
		||||
Flask>=1.0.2,<2.4.0
 | 
			
		||||
iso-639>=0.4.5,<0.5.0
 | 
			
		||||
PyPDF>=3.0.0,<3.8.0
 | 
			
		||||
PyPDF>=3.0.0,<3.16.0
 | 
			
		||||
pytz>=2016.10
 | 
			
		||||
requests>=2.11.1,<2.29.0
 | 
			
		||||
requests>=2.28.0,<2.32.0
 | 
			
		||||
SQLAlchemy>=1.3.0,<2.0.0
 | 
			
		||||
tornado>=4.1,<6.3
 | 
			
		||||
tornado>=6.3,<6.4
 | 
			
		||||
Wand>=0.4.4,<0.7.0
 | 
			
		||||
unidecode>=0.04.19,<1.4.0
 | 
			
		||||
lxml>=3.8.0,<5.0.0
 | 
			
		||||
flask-wtf>=0.14.2,<1.2.0
 | 
			
		||||
chardet>=3.0.0,<4.1.0
 | 
			
		||||
advocate>=1.0.0,<1.1.0
 | 
			
		||||
Flask-Limiter>=2.3.0,<3.4.0
 | 
			
		||||
Flask-Limiter>=2.3.0,<3.5.0
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								setup.cfg
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								setup.cfg
									
									
									
									
									
								
							@@ -38,64 +38,65 @@ console_scripts =
 | 
			
		||||
[options]
 | 
			
		||||
include_package_data = True
 | 
			
		||||
install_requires = 
 | 
			
		||||
	Werkzeug<3.0.0
 | 
			
		||||
	APScheduler>=3.6.3,<3.11.0
 | 
			
		||||
	Babel>=1.3,<3.0
 | 
			
		||||
	Flask-Babel>=0.11.1,<3.1.0
 | 
			
		||||
	Flask-Babel>=0.11.1,<3.2.0
 | 
			
		||||
	Flask-Login>=0.3.2,<0.6.3
 | 
			
		||||
	Flask-Principal>=0.3.2,<0.5.1
 | 
			
		||||
	Flask>=1.0.2,<2.4.0
 | 
			
		||||
	iso-639>=0.4.5,<0.5.0
 | 
			
		||||
	PyPDF>=3.0.0,<3.8.0
 | 
			
		||||
	PyPDF>=3.0.0,<3.16.0
 | 
			
		||||
	pytz>=2016.10
 | 
			
		||||
	requests>=2.11.1,<2.29.0
 | 
			
		||||
	requests>=2.28.0,<2.32.0
 | 
			
		||||
	SQLAlchemy>=1.3.0,<2.0.0
 | 
			
		||||
	tornado>=4.1,<6.3
 | 
			
		||||
	tornado>=6.3,<6.4
 | 
			
		||||
	Wand>=0.4.4,<0.7.0
 | 
			
		||||
	unidecode>=0.04.19,<1.4.0
 | 
			
		||||
	lxml>=3.8.0,<5.0.0
 | 
			
		||||
	flask-wtf>=0.14.2,<1.2.0
 | 
			
		||||
	chardet>=3.0.0,<4.1.0
 | 
			
		||||
	advocate>=1.0.0,<1.1.0
 | 
			
		||||
	Flask-Limiter>=2.3.0,<3.4.0
 | 
			
		||||
	Flask-Limiter>=2.3.0,<3.5.0
 | 
			
		||||
	
 | 
			
		||||
 | 
			
		||||
[options.extras_require]
 | 
			
		||||
gdrive = 
 | 
			
		||||
	google-api-python-client>=1.7.11,<2.90.0
 | 
			
		||||
	gevent>20.6.0,<23.0.0
 | 
			
		||||
	google-api-python-client>=1.7.11,<2.98.0
 | 
			
		||||
	gevent>20.6.0,<24.0.0
 | 
			
		||||
	greenlet>=0.4.17,<2.1.0
 | 
			
		||||
	httplib2>=0.9.2,<0.23.0
 | 
			
		||||
	oauth2client>=4.0.0,<4.1.4
 | 
			
		||||
	uritemplate>=3.0.0,<4.2.0
 | 
			
		||||
	pyasn1-modules>=0.0.8,<0.4.0
 | 
			
		||||
	pyasn1>=0.1.9,<0.6.0
 | 
			
		||||
	PyDrive2>=1.3.1,<1.16.0
 | 
			
		||||
	PyYAML>=3.12
 | 
			
		||||
	PyDrive2>=1.3.1,<1.18.0
 | 
			
		||||
	PyYAML>=3.12,<6.1
 | 
			
		||||
	rsa>=3.4.2,<4.10.0
 | 
			
		||||
gmail = 
 | 
			
		||||
	google-auth-oauthlib>=0.4.3,<0.9.0
 | 
			
		||||
	google-api-python-client>=1.7.11,<2.90.0
 | 
			
		||||
	google-auth-oauthlib>=0.4.3,<1.1.0
 | 
			
		||||
	google-api-python-client>=1.7.11,<2.98.0
 | 
			
		||||
goodreads = 
 | 
			
		||||
	goodreads>=0.3.2,<0.4.0
 | 
			
		||||
	python-Levenshtein>=0.12.0,<0.21.0
 | 
			
		||||
	python-Levenshtein>=0.12.0,<0.22.0
 | 
			
		||||
ldap = 
 | 
			
		||||
	python-ldap>=3.0.0,<3.5.0
 | 
			
		||||
	Flask-SimpleLDAP>=1.4.0,<1.5.0
 | 
			
		||||
oauth = 
 | 
			
		||||
	Flask-Dance>=2.0.0,<6.3.0
 | 
			
		||||
	SQLAlchemy-Utils>=0.33.5,<0.40.0
 | 
			
		||||
	Flask-Dance>=2.0.0,<7.1.0
 | 
			
		||||
	SQLAlchemy-Utils>=0.33.5,<0.42.0
 | 
			
		||||
metadata = 
 | 
			
		||||
	rarfile>=3.2
 | 
			
		||||
	scholarly>=1.2.0,<1.8
 | 
			
		||||
	markdown2>=2.0.0,<2.5.0
 | 
			
		||||
	html2text>=2020.1.16,<2022.1.1
 | 
			
		||||
	python-dateutil>=2.1,<2.9.0
 | 
			
		||||
	beautifulsoup4>=4.0.1,<4.12.0
 | 
			
		||||
	faust-cchardet>=2.1.18
 | 
			
		||||
	beautifulsoup4>=4.0.1,<4.13.0
 | 
			
		||||
	faust-cchardet>=2.1.18,<2.1.20
 | 
			
		||||
	py7zr>=0.15.0,<0.21.0
 | 
			
		||||
comics = 
 | 
			
		||||
	natsort>=2.2.0,<8.4.0
 | 
			
		||||
	comicapi>=2.2.0,<3.3.0
 | 
			
		||||
kobo = 
 | 
			
		||||
	jsonschema>=3.2.0,<4.18.0
 | 
			
		||||
	jsonschema>=3.2.0,<4.20.0
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -37,20 +37,20 @@
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <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>2023-08-23 21:16:31</p>
 | 
			
		||||
            <p class='text-justify attribute'><strong>Start Time: </strong>2023-10-11 19:32:23</p>
 | 
			
		||||
            
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="col-xs-6 col-md-6 col-sm-offset-3">
 | 
			
		||||
            
 | 
			
		||||
            <p class='text-justify attribute'><strong>Stop Time: </strong>2023-08-24 03:51:45</p>
 | 
			
		||||
            <p class='text-justify attribute'><strong>Stop Time: </strong>2023-10-12 01:29:49</p>
 | 
			
		||||
            
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="col-xs-6 col-md-6 col-sm-offset-3">
 | 
			
		||||
           <p class='text-justify attribute'><strong>Duration: </strong>5h 34 min</p>
 | 
			
		||||
           <p class='text-justify attribute'><strong>Duration: </strong>4h 56 min</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      </div>
 | 
			
		||||
@@ -234,11 +234,11 @@
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <tr id="su" class="passClass">
 | 
			
		||||
    <tr id="su" class="failClass">
 | 
			
		||||
        <td>TestBackupMetadata</td>
 | 
			
		||||
        <td class="text-center">22</td>
 | 
			
		||||
        <td class="text-center">22</td>
 | 
			
		||||
        <td class="text-center">0</td>
 | 
			
		||||
        <td class="text-center">21</td>
 | 
			
		||||
        <td class="text-center">1</td>
 | 
			
		||||
        <td class="text-center">0</td>
 | 
			
		||||
        <td class="text-center">0</td>
 | 
			
		||||
        <td class="text-center">
 | 
			
		||||
@@ -248,11 +248,31 @@
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
        <tr id='pt2.1' class='hiddenRow bg-success'>
 | 
			
		||||
        <tr id="ft2.1" class="none bg-danger">
 | 
			
		||||
            <td>
 | 
			
		||||
                <div class='testcase'>TestBackupMetadata - test_backup_all</div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td colspan='6' align='center'>PASS</td>
 | 
			
		||||
            <td colspan='6'>
 | 
			
		||||
                <div class="text-center">
 | 
			
		||||
                    <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft2.1')">FAIL</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!--css div popup start-->
 | 
			
		||||
                <div id="div_ft2.1" class="popup_window test_output" style="display:block;">
 | 
			
		||||
                    <div class='close_button pull-right'>
 | 
			
		||||
                        <button type="button" class="close" aria-label="Close" onfocus="this.blur();"
 | 
			
		||||
                                onclick="document.getElementById('div_ft2.1').style.display='none'"><span
 | 
			
		||||
                                aria-hidden="true">×</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_backup_metadata.py", line 49, in test_backup_all
 | 
			
		||||
    self.assertEqual(1, len(res))
 | 
			
		||||
AssertionError: 1 != 0</pre>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="clearfix"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!--css div popup end-->
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
@@ -322,7 +342,7 @@
 | 
			
		||||
    
 | 
			
		||||
        <tr id='pt2.9' class='hiddenRow bg-success'>
 | 
			
		||||
            <td>
 | 
			
		||||
                <div class='testcase'>TestBackupMetadata - test_backup_change_book_seriesindex</div>
 | 
			
		||||
                <div class='testcase'>TestBackupMetadata - test_backup_change_book_series_index</div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td colspan='6' align='center'>PASS</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
@@ -1014,12 +1034,12 @@
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <tr id="su" class="errorClass">
 | 
			
		||||
    <tr id="su" class="skipClass">
 | 
			
		||||
        <td>TestEditAdditionalBooks</td>
 | 
			
		||||
        <td class="text-center">20</td>
 | 
			
		||||
        <td class="text-center">17</td>
 | 
			
		||||
        <td class="text-center">18</td>
 | 
			
		||||
        <td class="text-center">0</td>
 | 
			
		||||
        <td class="text-center">0</td>
 | 
			
		||||
        <td class="text-center">1</td>
 | 
			
		||||
        <td class="text-center">2</td>
 | 
			
		||||
        <td class="text-center">
 | 
			
		||||
            <a onclick="showClassDetail('c12', 20)">Detail</a>
 | 
			
		||||
@@ -1136,31 +1156,11 @@
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
        <tr id="et12.13" class="none bg-info">
 | 
			
		||||
        <tr id='pt12.13' class='hiddenRow bg-success'>
 | 
			
		||||
            <td>
 | 
			
		||||
                <div class='testcase'>TestEditAdditionalBooks - test_upload_metadata_cb7</div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td colspan='6'>
 | 
			
		||||
                <div class="text-center">
 | 
			
		||||
                    <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et12.13')">ERROR</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!--css div popup start-->
 | 
			
		||||
                <div id="div_et12.13" class="popup_window test_output" style="display:block;">
 | 
			
		||||
                    <div class='close_button pull-right'>
 | 
			
		||||
                        <button type="button" class="close" aria-label="Close" onfocus="this.blur();"
 | 
			
		||||
                                onclick="document.getElementById('div_et12.13').style.display='none'"><span
 | 
			
		||||
                                aria-hidden="true">×</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_additional_books.py", line 225, in test_upload_metadata_cb7
 | 
			
		||||
    self.check_element_on_page((By.ID, 'edit_cancel')).click()
 | 
			
		||||
AttributeError: 'bool' object has no attribute 'click'</pre>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="clearfix"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!--css div popup end-->
 | 
			
		||||
            </td>
 | 
			
		||||
            <td colspan='6' align='center'>PASS</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
@@ -1246,12 +1246,12 @@ AttributeError: 'bool' object has no attribute 'click'</pre>
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <tr id="su" class="errorClass">
 | 
			
		||||
    <tr id="su" class="skipClass">
 | 
			
		||||
        <td>TestEditBooks</td>
 | 
			
		||||
        <td class="text-center">38</td>
 | 
			
		||||
        <td class="text-center">34</td>
 | 
			
		||||
        <td class="text-center">36</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">
 | 
			
		||||
            <a onclick="showClassDetail('c13', 38)">Detail</a>
 | 
			
		||||
@@ -1537,31 +1537,11 @@ AttributeError: 'bool' object has no attribute 'click'</pre>
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
        <tr id="et13.28" class="none bg-info">
 | 
			
		||||
        <tr id='pt13.28' class='hiddenRow bg-success'>
 | 
			
		||||
            <td>
 | 
			
		||||
                <div class='testcase'>TestEditBooks - test_upload_book_cb7</div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td colspan='6'>
 | 
			
		||||
                <div class="text-center">
 | 
			
		||||
                    <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et13.28')">ERROR</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!--css div popup start-->
 | 
			
		||||
                <div id="div_et13.28" class="popup_window test_output" style="display:block;">
 | 
			
		||||
                    <div class='close_button pull-right'>
 | 
			
		||||
                        <button type="button" class="close" aria-label="Close" onfocus="this.blur();"
 | 
			
		||||
                                onclick="document.getElementById('div_et13.28').style.display='none'"><span
 | 
			
		||||
                                aria-hidden="true">×</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 1159, in test_upload_book_cb7
 | 
			
		||||
    self.check_element_on_page((By.ID, 'edit_cancel')).click()
 | 
			
		||||
AttributeError: 'bool' object has no attribute 'click'</pre>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="clearfix"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!--css div popup end-->
 | 
			
		||||
            </td>
 | 
			
		||||
            <td colspan='6' align='center'>PASS</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
@@ -1647,31 +1627,11 @@ AttributeError: 'bool' object has no attribute 'click'</pre>
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
        <tr id="et13.38" class="none bg-info">
 | 
			
		||||
        <tr id='pt13.38' class='hiddenRow bg-success'>
 | 
			
		||||
            <td>
 | 
			
		||||
                <div class='testcase'>TestEditBooks - test_upload_cover_hdd</div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td colspan='6'>
 | 
			
		||||
                <div class="text-center">
 | 
			
		||||
                    <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et13.38')">ERROR</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!--css div popup start-->
 | 
			
		||||
                <div id="div_et13.38" class="popup_window test_output" style="display:block;">
 | 
			
		||||
                    <div class='close_button pull-right'>
 | 
			
		||||
                        <button type="button" class="close" aria-label="Close" onfocus="this.blur();"
 | 
			
		||||
                                onclick="document.getElementById('div_et13.38').style.display='none'"><span
 | 
			
		||||
                                aria-hidden="true">×</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 866, in test_upload_cover_hdd
 | 
			
		||||
    self.delete_book(details['id'])
 | 
			
		||||
NameError: name 'details' is not defined</pre>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="clearfix"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!--css div popup end-->
 | 
			
		||||
            </td>
 | 
			
		||||
            <td colspan='6' align='center'>PASS</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
@@ -1992,12 +1952,12 @@ NameError: name 'details' is not defined</pre>
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <tr id="su" class="failClass">
 | 
			
		||||
    <tr id="su" class="errorClass">
 | 
			
		||||
        <td>TestLoadMetadata</td>
 | 
			
		||||
        <td class="text-center">1</td>
 | 
			
		||||
        <td class="text-center">0</td>
 | 
			
		||||
        <td class="text-center">1</td>
 | 
			
		||||
        <td class="text-center">0</td>
 | 
			
		||||
        <td class="text-center">1</td>
 | 
			
		||||
        <td class="text-center">0</td>
 | 
			
		||||
        <td class="text-center">
 | 
			
		||||
            <a onclick="showClassDetail('c17', 1)">Detail</a>
 | 
			
		||||
@@ -2006,32 +1966,26 @@ NameError: name 'details' is not defined</pre>
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
        <tr id="ft17.1" class="none bg-danger">
 | 
			
		||||
        <tr id="et17.1" class="none bg-info">
 | 
			
		||||
            <td>
 | 
			
		||||
                <div class='testcase'>TestLoadMetadata - test_load_metadata</div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td colspan='6'>
 | 
			
		||||
                <div class="text-center">
 | 
			
		||||
                    <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft17.1')">FAIL</a>
 | 
			
		||||
                    <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et17.1')">ERROR</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!--css div popup start-->
 | 
			
		||||
                <div id="div_ft17.1" class="popup_window test_output" style="display:block;">
 | 
			
		||||
                <div id="div_et17.1" class="popup_window test_output" style="display:block;">
 | 
			
		||||
                    <div class='close_button pull-right'>
 | 
			
		||||
                        <button type="button" class="close" aria-label="Close" onfocus="this.blur();"
 | 
			
		||||
                                onclick="document.getElementById('div_ft17.1').style.display='none'"><span
 | 
			
		||||
                                onclick="document.getElementById('div_et17.1').style.display='none'"><span
 | 
			
		||||
                                aria-hidden="true">×</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_metadata.py", line 209, in test_load_metadata
 | 
			
		||||
    self.assertEqual(old_results, results)
 | 
			
		||||
AssertionError: Lists differ: [] != [{'cover_element': <selenium.webdriver.rem[10121 chars]4/'}]
 | 
			
		||||
 | 
			
		||||
Second list contains 20 additional elements.
 | 
			
		||||
First extra element 0:
 | 
			
		||||
{'cover_element': <selenium.webdriver.remote.webelement.WebElement (session="34034d2d-f804-47c1-b9ad-fcf09f75f812", element="6dfe81e2-4752-4f1f-bd33-9388d0d529c1")>, 'cover': 'https://books.google.com/books/content?id=Ub8TAQAAIAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api&fife=w800-h900', 'source': 'https://books.google.com/', 'author': 'Martin Vogt', 'publisher': '', 'title': 'Der Buchtitel in der römischen Poesie', 'title_link': 'https://books.google.com/books?id=Ub8TAQAAIAAJ'}
 | 
			
		||||
 | 
			
		||||
Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
  File "/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py", line 84, in test_load_metadata
 | 
			
		||||
    elif 'https://amazon.com/' == results[20]['source']:
 | 
			
		||||
IndexError: list index out of range</pre>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="clearfix"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
@@ -3374,13 +3328,13 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
 | 
			
		||||
    <tr id="su" class="passClass">
 | 
			
		||||
        <td>TestOPDSFeed</td>
 | 
			
		||||
        <td class="text-center">23</td>
 | 
			
		||||
        <td class="text-center">23</td>
 | 
			
		||||
        <td class="text-center">24</td>
 | 
			
		||||
        <td class="text-center">24</td>
 | 
			
		||||
        <td class="text-center">0</td>
 | 
			
		||||
        <td class="text-center">0</td>
 | 
			
		||||
        <td class="text-center">0</td>
 | 
			
		||||
        <td class="text-center">
 | 
			
		||||
            <a onclick="showClassDetail('c36', 23)">Detail</a>
 | 
			
		||||
            <a onclick="showClassDetail('c36', 24)">Detail</a>
 | 
			
		||||
        </td>
 | 
			
		||||
    </tr>
 | 
			
		||||
 | 
			
		||||
@@ -3559,7 +3513,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
    
 | 
			
		||||
        <tr id='pt36.20' class='hiddenRow bg-success'>
 | 
			
		||||
            <td>
 | 
			
		||||
                <div class='testcase'>TestOPDSFeed - test_opds_tags</div>
 | 
			
		||||
                <div class='testcase'>TestOPDSFeed - test_opds_stats</div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td colspan='6' align='center'>PASS</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
@@ -3568,7 +3522,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
    
 | 
			
		||||
        <tr id='pt36.21' class='hiddenRow bg-success'>
 | 
			
		||||
            <td>
 | 
			
		||||
                <div class='testcase'>TestOPDSFeed - test_opds_top_rated</div>
 | 
			
		||||
                <div class='testcase'>TestOPDSFeed - test_opds_tags</div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td colspan='6' align='center'>PASS</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
@@ -3577,7 +3531,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
    
 | 
			
		||||
        <tr id='pt36.22' class='hiddenRow bg-success'>
 | 
			
		||||
            <td>
 | 
			
		||||
                <div class='testcase'>TestOPDSFeed - test_opds_unicode_user</div>
 | 
			
		||||
                <div class='testcase'>TestOPDSFeed - test_opds_top_rated</div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td colspan='6' align='center'>PASS</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
@@ -3585,6 +3539,15 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
        <tr id='pt36.23' class='hiddenRow bg-success'>
 | 
			
		||||
            <td>
 | 
			
		||||
                <div class='testcase'>TestOPDSFeed - test_opds_unicode_user</div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td colspan='6' align='center'>PASS</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
        <tr id='pt36.24' class='hiddenRow bg-success'>
 | 
			
		||||
            <td>
 | 
			
		||||
                <div class='testcase'>TestOPDSFeed - test_recently_added</div>
 | 
			
		||||
            </td>
 | 
			
		||||
@@ -4082,11 +4045,11 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <tr id="su" class="skipClass">
 | 
			
		||||
    <tr id="su" class="failClass">
 | 
			
		||||
        <td>TestThumbnails</td>
 | 
			
		||||
        <td class="text-center">8</td>
 | 
			
		||||
        <td class="text-center">7</td>
 | 
			
		||||
        <td class="text-center">0</td>
 | 
			
		||||
        <td class="text-center">6</td>
 | 
			
		||||
        <td class="text-center">1</td>
 | 
			
		||||
        <td class="text-center">0</td>
 | 
			
		||||
        <td class="text-center">1</td>
 | 
			
		||||
        <td class="text-center">
 | 
			
		||||
@@ -4159,11 +4122,31 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
        <tr id='pt45.8' class='hiddenRow bg-success'>
 | 
			
		||||
        <tr id="ft45.8" class="none bg-danger">
 | 
			
		||||
            <td>
 | 
			
		||||
                <div class='testcase'>TestThumbnails - test_sideloaded_book</div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td colspan='6' align='center'>PASS</td>
 | 
			
		||||
            <td colspan='6'>
 | 
			
		||||
                <div class="text-center">
 | 
			
		||||
                    <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft45.8')">FAIL</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!--css div popup start-->
 | 
			
		||||
                <div id="div_ft45.8" class="popup_window test_output" style="display:block;">
 | 
			
		||||
                    <div class='close_button pull-right'>
 | 
			
		||||
                        <button type="button" class="close" aria-label="Close" onfocus="this.blur();"
 | 
			
		||||
                                onclick="document.getElementById('div_ft45.8').style.display='none'"><span
 | 
			
		||||
                                aria-hidden="true">×</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_thumbnails.py", line 311, in test_sideloaded_book
 | 
			
		||||
    self.assertAlmostEqual(diff(BytesIO(list_cover), BytesIO(old_list_cover), delete_diff_file=True), 0.0,
 | 
			
		||||
AssertionError: 0.004399004046062869 != 0.0 within 0.0001 delta (0.004399004046062869 difference)</pre>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="clearfix"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!--css div popup end-->
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
@@ -5237,10 +5220,10 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
 | 
			
		||||
    <tr id='total_row' class="text-center bg-grey">
 | 
			
		||||
        <td>Total</td>
 | 
			
		||||
        <td>461</td>
 | 
			
		||||
        <td>448</td>
 | 
			
		||||
        <td>462</td>
 | 
			
		||||
        <td>450</td>
 | 
			
		||||
        <td>2</td>
 | 
			
		||||
        <td>1</td>
 | 
			
		||||
        <td>3</td>
 | 
			
		||||
        <td>9</td>
 | 
			
		||||
        <td> </td>
 | 
			
		||||
    </tr>
 | 
			
		||||
@@ -5269,7 +5252,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>Platform</th>
 | 
			
		||||
              <td>Linux 6.2.0-26-generic #26~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Jul 13 16:27:29 UTC 2 x86_64 x86_64</td>
 | 
			
		||||
              <td>Linux 6.2.0-34-generic #34~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Sep  7 13:12:03 UTC 2 x86_64 x86_64</td>
 | 
			
		||||
              <td>Basic</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5293,7 +5276,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>Babel</th>
 | 
			
		||||
              <td>2.12.1</td>
 | 
			
		||||
              <td>2.13.0</td>
 | 
			
		||||
              <td>Basic</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5311,13 +5294,13 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>flask-babel</th>
 | 
			
		||||
              <td>3.0.1</td>
 | 
			
		||||
              <td>3.1.0</td>
 | 
			
		||||
              <td>Basic</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>Flask-Limiter</th>
 | 
			
		||||
              <td>3.3.1</td>
 | 
			
		||||
              <td>3.4.1</td>
 | 
			
		||||
              <td>Basic</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5335,13 +5318,13 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>Flask-WTF</th>
 | 
			
		||||
              <td>1.1.1</td>
 | 
			
		||||
              <td>1.1.2</td>
 | 
			
		||||
              <td>Basic</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>greenlet</th>
 | 
			
		||||
              <td>2.0.2</td>
 | 
			
		||||
              <td>3.0.0</td>
 | 
			
		||||
              <td>Basic</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5371,19 +5354,19 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>pypdf</th>
 | 
			
		||||
              <td>3.7.1</td>
 | 
			
		||||
              <td>3.15.5</td>
 | 
			
		||||
              <td>Basic</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>pytz</th>
 | 
			
		||||
              <td>2022.7.1</td>
 | 
			
		||||
              <td>2023.3.post1</td>
 | 
			
		||||
              <td>Basic</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>requests</th>
 | 
			
		||||
              <td>2.28.2</td>
 | 
			
		||||
              <td>2.31.0</td>
 | 
			
		||||
              <td>Basic</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5395,13 +5378,13 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>tornado</th>
 | 
			
		||||
              <td>6.2</td>
 | 
			
		||||
              <td>6.3.3</td>
 | 
			
		||||
              <td>Basic</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>Unidecode</th>
 | 
			
		||||
              <td>1.3.6</td>
 | 
			
		||||
              <td>1.3.7</td>
 | 
			
		||||
              <td>Basic</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5419,7 +5402,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>google-api-python-client</th>
 | 
			
		||||
              <td>2.97.0</td>
 | 
			
		||||
              <td>2.103.0</td>
 | 
			
		||||
              <td>TestBackupMetadataGdrive</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5449,7 +5432,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>google-api-python-client</th>
 | 
			
		||||
              <td>2.97.0</td>
 | 
			
		||||
              <td>2.103.0</td>
 | 
			
		||||
              <td>TestCliGdrivedb</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5479,7 +5462,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>google-api-python-client</th>
 | 
			
		||||
              <td>2.97.0</td>
 | 
			
		||||
              <td>2.103.0</td>
 | 
			
		||||
              <td>TestEbookConvertCalibreGDrive</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5509,7 +5492,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>google-api-python-client</th>
 | 
			
		||||
              <td>2.97.0</td>
 | 
			
		||||
              <td>2.103.0</td>
 | 
			
		||||
              <td>TestEbookConvertGDriveKepubify</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5551,7 +5534,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>rarfile</th>
 | 
			
		||||
              <td>4.0</td>
 | 
			
		||||
              <td>4.1</td>
 | 
			
		||||
              <td>TestEditAdditionalBooks</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5563,7 +5546,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>google-api-python-client</th>
 | 
			
		||||
              <td>2.97.0</td>
 | 
			
		||||
              <td>2.103.0</td>
 | 
			
		||||
              <td>TestEditAuthorsGdrive</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5599,7 +5582,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>google-api-python-client</th>
 | 
			
		||||
              <td>2.97.0</td>
 | 
			
		||||
              <td>2.103.0</td>
 | 
			
		||||
              <td>TestEditBooksOnGdrive</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5641,7 +5624,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>google-api-python-client</th>
 | 
			
		||||
              <td>2.97.0</td>
 | 
			
		||||
              <td>2.103.0</td>
 | 
			
		||||
              <td>TestSetupGdrive</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5677,19 +5660,19 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>python-Levenshtein</th>
 | 
			
		||||
              <td>0.21.1</td>
 | 
			
		||||
              <td>0.23.0</td>
 | 
			
		||||
              <td>TestGoodreads</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>jsonschema</th>
 | 
			
		||||
              <td>4.19.0</td>
 | 
			
		||||
              <td>4.19.1</td>
 | 
			
		||||
              <td>TestKoboSync</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>jsonschema</th>
 | 
			
		||||
              <td>4.19.0</td>
 | 
			
		||||
              <td>4.19.1</td>
 | 
			
		||||
              <td>TestKoboSyncBig</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5701,7 +5684,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
          
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>jsonschema</th>
 | 
			
		||||
              <td>4.19.0</td>
 | 
			
		||||
              <td>4.19.1</td>
 | 
			
		||||
              <td>TestLdapLogin</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          
 | 
			
		||||
@@ -5731,7 +5714,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    drawCircle(448, 1, 3, 9);
 | 
			
		||||
    drawCircle(450, 2, 1, 9);
 | 
			
		||||
    showCase(5);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user