Merge branch 'Develop' into tasks

# Conflicts:
#	cps/db.py
pull/1580/head
blitzmann 4 years ago
commit 6322919bc7

@ -81,10 +81,10 @@ SIDEBAR_PUBLISHER = 1 << 12
SIDEBAR_RATING = 1 << 13 SIDEBAR_RATING = 1 << 13
SIDEBAR_FORMAT = 1 << 14 SIDEBAR_FORMAT = 1 << 14
SIDEBAR_ARCHIVED = 1 << 15 SIDEBAR_ARCHIVED = 1 << 15
# SIDEBAR_LIST = 1 << 16 SIDEBAR_LIST = 1 << 16
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS
ADMIN_USER_SIDEBAR = (SIDEBAR_ARCHIVED << 1) - 1 ADMIN_USER_SIDEBAR = (SIDEBAR_LIST << 1) - 1
UPDATE_STABLE = 0 << 0 UPDATE_STABLE = 0 << 0
AUTO_UPDATE_STABLE = 1 << 0 AUTO_UPDATE_STABLE = 1 << 0

@ -29,7 +29,8 @@ from sqlalchemy import create_engine
from sqlalchemy import Table, Column, ForeignKey, CheckConstraint from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float
from sqlalchemy.orm import relationship, sessionmaker, scoped_session from sqlalchemy.orm import relationship, sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm.collections import InstrumentedList
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from flask_login import current_user from flask_login import current_user
from sqlalchemy.sql.expression import and_, true, false, text, func, or_ from sqlalchemy.sql.expression import and_, true, false, text, func, or_
@ -97,6 +98,9 @@ class Identifiers(Base):
self.type = id_type self.type = id_type
self.book = book self.book = book
#def get(self):
# return {self.type: self.val}
def formatType(self): def formatType(self):
if self.type == "amazon": if self.type == "amazon":
return u"Amazon" return u"Amazon"
@ -149,6 +153,9 @@ class Comments(Base):
self.text = text self.text = text
self.book = book self.book = book
def get(self):
return self.text
def __repr__(self): def __repr__(self):
return u"<Comments({0})>".format(self.text) return u"<Comments({0})>".format(self.text)
@ -162,6 +169,9 @@ class Tags(Base):
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
def get(self):
return self.name
def __repr__(self): def __repr__(self):
return u"<Tags('{0})>".format(self.name) return u"<Tags('{0})>".format(self.name)
@ -179,6 +189,9 @@ class Authors(Base):
self.sort = sort self.sort = sort
self.link = link self.link = link
def get(self):
return self.name
def __repr__(self): def __repr__(self):
return u"<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link) return u"<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link)
@ -194,6 +207,9 @@ class Series(Base):
self.name = name self.name = name
self.sort = sort self.sort = sort
def get(self):
return self.name
def __repr__(self): def __repr__(self):
return u"<Series('{0},{1}')>".format(self.name, self.sort) return u"<Series('{0},{1}')>".format(self.name, self.sort)
@ -207,6 +223,9 @@ class Ratings(Base):
def __init__(self, rating): def __init__(self, rating):
self.rating = rating self.rating = rating
def get(self):
return self.rating
def __repr__(self): def __repr__(self):
return u"<Ratings('{0}')>".format(self.rating) return u"<Ratings('{0}')>".format(self.rating)
@ -220,6 +239,12 @@ class Languages(Base):
def __init__(self, lang_code): def __init__(self, lang_code):
self.lang_code = lang_code self.lang_code = lang_code
def get(self):
if self.language_name:
return self.language_name
else:
return self.lang_code
def __repr__(self): def __repr__(self):
return u"<Languages('{0}')>".format(self.lang_code) return u"<Languages('{0}')>".format(self.lang_code)
@ -235,6 +260,9 @@ class Publishers(Base):
self.name = name self.name = name
self.sort = sort self.sort = sort
def get(self):
return self.name
def __repr__(self): def __repr__(self):
return u"<Publishers('{0},{1}')>".format(self.name, self.sort) return u"<Publishers('{0},{1}')>".format(self.name, self.sort)
@ -255,6 +283,10 @@ class Data(Base):
self.uncompressed_size = uncompressed_size self.uncompressed_size = uncompressed_size
self.name = name self.name = name
# ToDo: Check
def get(self):
return self.name
def __repr__(self): def __repr__(self):
return u"<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name) return u"<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name)
@ -262,14 +294,14 @@ class Data(Base):
class Books(Base): class Books(Base):
__tablename__ = 'books' __tablename__ = 'books'
DEFAULT_PUBDATE = "0101-01-01 00:00:00+00:00" DEFAULT_PUBDATE = datetime(101, 1, 1, 0, 0, 0, 0) # ("0101-01-01 00:00:00+00:00")
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(collation='NOCASE'), nullable=False, default='Unknown') title = Column(String(collation='NOCASE'), nullable=False, default='Unknown')
sort = Column(String(collation='NOCASE')) sort = Column(String(collation='NOCASE'))
author_sort = Column(String(collation='NOCASE')) author_sort = Column(String(collation='NOCASE'))
timestamp = Column(TIMESTAMP, default=datetime.utcnow) timestamp = Column(TIMESTAMP, default=datetime.utcnow)
pubdate = Column(String) # , default=datetime.utcnow) pubdate = Column(TIMESTAMP, default=DEFAULT_PUBDATE)
series_index = Column(String, nullable=False, default="1.0") series_index = Column(String, nullable=False, default="1.0")
last_modified = Column(TIMESTAMP, default=datetime.utcnow) last_modified = Column(TIMESTAMP, default=datetime.utcnow)
path = Column(String, default="", nullable=False) path = Column(String, default="", nullable=False)
@ -301,6 +333,9 @@ class Books(Base):
self.path = path self.path = path
self.has_cover = has_cover self.has_cover = has_cover
#def as_dict(self):
# return {c.name: getattr(self, c.name) for c in self.__table__.columns}
def __repr__(self): def __repr__(self):
return u"<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort, return u"<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort,
self.timestamp, self.pubdate, self.series_index, self.timestamp, self.pubdate, self.series_index,
@ -329,6 +364,39 @@ class Custom_Columns(Base):
display_dict['enum_values'] = [x.decode('unicode_escape') for x in display_dict['enum_values']] display_dict['enum_values'] = [x.decode('unicode_escape') for x in display_dict['enum_values']]
return display_dict return display_dict
class AlchemyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj.__class__, DeclarativeMeta):
# an SQLAlchemy class
fields = {}
for field in [x for x in dir(obj) if not x.startswith('_') and x != 'metadata']:
if field == 'books':
continue
data = obj.__getattribute__(field)
try:
if isinstance(data, str):
data = data.replace("'","\'")
elif isinstance(data, InstrumentedList):
el =list()
for ele in data:
if ele.get:
el.append(ele.get())
else:
el.append(json.dumps(ele, cls=AlchemyEncoder))
data =",".join(el)
if data == '[]':
data = ""
else:
json.dumps(data)
fields[field] = data
except:
fields[field] = ""
# a json-encodable dict
return fields
return json.JSONEncoder.default(self, obj)
class CalibreDB(): class CalibreDB():
@ -494,10 +562,11 @@ class CalibreDB():
pos_content_cc_filter, ~neg_content_cc_filter, archived_filter) pos_content_cc_filter, ~neg_content_cc_filter, archived_filter)
# Fill indexpage with all requested data from database # Fill indexpage with all requested data from database
def fill_indexpage(self, page, database, db_filter, order, *join): def fill_indexpage(self, page, pagesize, database, db_filter, order, *join):
return self.fill_indexpage_with_archived_books(page, database, db_filter, order, False, *join) return self.fill_indexpage_with_archived_books(page, pagesize, database, db_filter, order, False, *join)
def fill_indexpage_with_archived_books(self, page, database, db_filter, order, allow_show_archived, *join): def fill_indexpage_with_archived_books(self, page, pagesize, database, db_filter, order, allow_show_archived, *join):
pagesize = pagesize or self.config.config_books_per_page
if current_user.show_detail_random(): if current_user.show_detail_random():
randm = self.session.query(Books) \ randm = self.session.query(Books) \
.filter(self.common_filters(allow_show_archived)) \ .filter(self.common_filters(allow_show_archived)) \
@ -505,14 +574,14 @@ class CalibreDB():
.limit(self.config.config_random_books) .limit(self.config.config_random_books)
else: else:
randm = false() randm = false()
off = int(int(self.config.config_books_per_page) * (page - 1)) off = int(int(pagesize) * (page - 1))
query = self.session.query(database) \ query = self.session.query(database) \
.join(*join, isouter=True) \ .join(*join, isouter=True) \
.filter(db_filter) \ .filter(db_filter) \
.filter(self.common_filters(allow_show_archived)) .filter(self.common_filters(allow_show_archived))
pagination = Pagination(page, self.config.config_books_per_page, pagination = Pagination(page, pagesize,
len(query.all())) len(query.all()))
entries = query.order_by(*order).offset(off).limit(self.config.config_books_per_page).all() entries = query.order_by(*order).offset(off).limit(pagesize).all()
for book in entries: for book in entries:
book = self.order_authors(book) book = self.order_authors(book)
return entries, randm, pagination return entries, randm, pagination
@ -552,20 +621,26 @@ class CalibreDB():
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first() .filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first()
# read search results from calibre-database and return it (function is used for feed and simple search # read search results from calibre-database and return it (function is used for feed and simple search
def get_search_results(self, term): def get_search_results(self, term, offset=None, order=None, limit=None):
order = order or [Books.sort]
if offset != None and limit != None:
offset = int(offset)
limit = offset + int(limit)
term.strip().lower() term.strip().lower()
self.session.connection().connection.connection.create_function("lower", 1, lcase) self.session.connection().connection.connection.create_function("lower", 1, lcase)
q = list() q = list()
authorterms = re.split("[, ]+", term) authorterms = re.split("[, ]+", term)
for authorterm in authorterms: for authorterm in authorterms:
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%"))) q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
return self.session.query(Books).filter(self.common_filters(True)).filter( result = self.session.query(Books).filter(self.common_filters(True)).filter(
or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")), or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")),
Books.series.any(func.lower(Series.name).ilike("%" + term + "%")), Books.series.any(func.lower(Series.name).ilike("%" + term + "%")),
Books.authors.any(and_(*q)), Books.authors.any(and_(*q)),
Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")), Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")),
func.lower(Books.title).ilike("%" + term + "%") func.lower(Books.title).ilike("%" + term + "%")
)).order_by(Books.sort).all() )).order_by(*order).all()
result_count = len(result)
return result[offset:limit], result_count
# Creates for all stored languages a translated speaking name in the array for the UI # Creates for all stored languages a translated speaking name in the array for the UI
def speaking_language(self, languages=None): def speaking_language(self, languages=None):

@ -27,6 +27,7 @@ import json
from shutil import copyfile from shutil import copyfile
from uuid import uuid4 from uuid import uuid4
from babel import Locale as LC
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import current_user, login_required from flask_login import current_user, login_required
@ -171,21 +172,42 @@ def modify_identifiers(input_identifiers, db_identifiers, db_session):
changed = True changed = True
return changed return changed
@editbook.route("/ajax/delete/<int:book_id>")
@login_required
def delete_book_from_details(book_id):
return Response(delete_book(book_id,"", True), mimetype='application/json')
@editbook.route("/delete/<int:book_id>/", defaults={'book_format': ""}) @editbook.route("/delete/<int:book_id>", defaults={'book_format': ""})
@editbook.route("/delete/<int:book_id>/<string:book_format>/") @editbook.route("/delete/<int:book_id>/<string:book_format>")
@login_required @login_required
def delete_book(book_id, book_format): def delete_book_ajax(book_id, book_format):
return delete_book(book_id,book_format, False)
def delete_book(book_id, book_format, jsonResponse):
warning = {}
if current_user.role_delete_books(): if current_user.role_delete_books():
book = calibre_db.get_book(book_id) book = calibre_db.get_book(book_id)
if book: if book:
try: try:
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper()) result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
if not result: if not result:
flash(error, category="error") if jsonResponse:
return redirect(url_for('editbook.edit_book', book_id=book_id)) return json.dumps({"location": url_for("editbook.edit_book"),
"type": "alert",
"format": "",
"error": error}),
else:
flash(error, category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id))
if error: if error:
flash(error, category="warning") if jsonResponse:
warning = {"location": url_for("editbook.edit_book"),
"type": "warning",
"format": "",
"error": error}
else:
flash(error, category="warning")
if not book_format: if not book_format:
# delete book from Shelfs, Downloads, Read list # delete book from Shelfs, Downloads, Read list
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete() ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete()
@ -241,11 +263,23 @@ def delete_book(book_id, book_format):
# book not found # book not found
log.error('Book with id "%s" could not be deleted: not found', book_id) log.error('Book with id "%s" could not be deleted: not found', book_id)
if book_format: if book_format:
flash(_('Book Format Successfully Deleted'), category="success") if jsonResponse:
return redirect(url_for('editbook.edit_book', book_id=book_id)) return json.dumps([warning, {"location": url_for("editbook.edit_book", book_id=book_id),
"type": "success",
"format": book_format,
"message": _('Book Format Successfully Deleted')}])
else:
flash(_('Book Format Successfully Deleted'), category="success")
return redirect(url_for('editbook.edit_book', book_id=book_id))
else: else:
flash(_('Book Successfully Deleted'), category="success") if jsonResponse:
return redirect(url_for('web.index')) return json.dumps([warning, {"location": url_for('web.index'),
"type": "success",
"format": book_format,
"message": _('Book Successfully Deleted')}])
else:
flash(_('Book Successfully Deleted'), category="success")
return redirect(url_for('web.index'))
def render_edit_book(book_id): def render_edit_book(book_id):
@ -896,3 +930,112 @@ def convert_bookformat(book_id):
else: else:
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error") flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id)) return redirect(url_for('editbook.edit_book', book_id=book_id))
@editbook.route("/ajax/editbooks/<param>", methods=['POST'])
@login_required_if_no_ano
def edit_list_book(param):
vals = request.form.to_dict()
# calibre_db.update_title_sort(config)
#calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
book = calibre_db.get_book(vals['pk'])
if param =='series_index':
edit_book_series_index(vals['value'], book)
elif param =='tags':
edit_book_tags(vals['value'], book)
elif param =='series':
edit_book_series(vals['value'], book)
elif param =='publishers':
vals['publisher'] = vals['value']
edit_book_publisher(vals, book)
elif param =='languages':
edit_book_languages(vals['value'], book)
elif param =='author_sort':
book.author_sort = vals['value']
elif param =='title':
book.title = vals['value']
helper.update_dir_stucture(book.id, config.config_calibre_dir)
elif param =='sort':
book.sort = vals['value']
# ToDo: edit books
elif param =='authors':
input_authors = vals['value'].split('&')
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
sort_authors_list = list()
for inp in input_authors:
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
if not stored_author:
stored_author = helper.get_sorted_author(inp)
else:
stored_author = stored_author.sort
sort_authors_list.append(helper.get_sorted_author(stored_author))
sort_authors = ' & '.join(sort_authors_list)
if book.author_sort != sort_authors:
book.author_sort = sort_authors
helper.update_dir_stucture(book.id, config.config_calibre_dir, input_authors[0])
book.last_modified = datetime.utcnow()
calibre_db.session.commit()
return ""
@editbook.route("/ajax/sort_value/<field>/<int:bookid>")
@login_required
def get_sorted_entry(field, bookid):
if field == 'title' or field == 'authors':
book = calibre_db.get_filtered_book(bookid)
if book:
if field == 'title':
return json.dumps({'sort': book.sort})
elif field == 'authors':
return json.dumps({'author_sort': book.author_sort})
return ""
@editbook.route("/ajax/simulatemerge", methods=['POST'])
@login_required
def simulate_merge_list_book():
vals = request.get_json().get('Merge_books')
if vals:
to_book = calibre_db.get_book(vals[0]).title
vals.pop(0)
if to_book:
for book_id in vals:
from_book = []
from_book.append(calibre_db.get_book(book_id).title)
return json.dumps({'to': to_book, 'from': from_book})
return ""
@editbook.route("/ajax/mergebooks", methods=['POST'])
@login_required
def merge_list_book():
vals = request.get_json().get('Merge_books')
to_file = list()
if vals:
# load all formats from target book
to_book = calibre_db.get_book(vals[0])
vals.pop(0)
if to_book:
for file in to_book.data:
to_file.append(file.format)
to_name = helper.get_valid_filename(to_book.title) + ' - ' + \
helper.get_valid_filename(to_book.authors[0].name)
for book_id in vals:
from_book = calibre_db.get_book(book_id)
if from_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,
to_book.path,
to_name + "." + element.format.lower()))
filepath_old = os.path.normpath(os.path.join(config.config_calibre_dir,
from_book.path,
element.name + "." + element.format.lower()))
copyfile(filepath_old, filepath_new)
to_book.data.append(db.Data(to_book.id,
element.format,
element.uncompressed_size,
to_name))
delete_book(from_book.id,"", True) # json_resp =
return json.dumps({'success': True})
return ""

@ -137,7 +137,7 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name), taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name),
text=text text=text
)) ))
return return

@ -76,22 +76,18 @@ def mimetype_filter(val):
@jinjia.app_template_filter('formatdate') @jinjia.app_template_filter('formatdate')
def formatdate_filter(val): def formatdate_filter(val):
try: try:
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val) return format_date(val, format='medium', locale=get_locale())
formatdate = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S")
return format_date(formatdate, format='medium', locale=get_locale())
except AttributeError as e: except AttributeError as e:
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e, log.error('Babel error: %s, Current user locale: %s, Current User: %s', e,
current_user.locale, current_user.locale,
current_user.nickname current_user.nickname
) )
return formatdate return val
@jinjia.app_template_filter('formatdateinput') @jinjia.app_template_filter('formatdateinput')
def format_date_input(val): def format_date_input(val):
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val) input_date = val.isoformat().split('T', 1)[0] # Hack to support dates <1900
date_obj = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S")
input_date = date_obj.isoformat().split('T', 1)[0] # Hack to support dates <1900
return '' if input_date == "0101-01-01" else input_date return '' if input_date == "0101-01-01" else input_date

@ -100,7 +100,7 @@ def feed_normal_search():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_new(): def feed_new():
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, True, [db.Books.timestamp.desc()]) db.Books, True, [db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination) return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@ -118,7 +118,7 @@ def feed_discover():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_best_rated(): def feed_best_rated():
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books.ratings.any(db.Ratings.rating > 9), db.Books, db.Books.ratings.any(db.Ratings.rating > 9),
[db.Books.timestamp.desc()]) [db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination) return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@ -164,7 +164,7 @@ def feed_authorindex():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_author(book_id): def feed_author(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books,
db.Books.authors.any(db.Authors.id == book_id), db.Books.authors.any(db.Authors.id == book_id),
[db.Books.timestamp.desc()]) [db.Books.timestamp.desc()])
@ -190,7 +190,7 @@ def feed_publisherindex():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_publisher(book_id): def feed_publisher(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books,
db.Books.publishers.any(db.Publishers.id == book_id), db.Books.publishers.any(db.Publishers.id == book_id),
[db.Books.timestamp.desc()]) [db.Books.timestamp.desc()])
@ -218,7 +218,7 @@ def feed_categoryindex():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_category(book_id): def feed_category(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books,
db.Books.tags.any(db.Tags.id == book_id), db.Books.tags.any(db.Tags.id == book_id),
[db.Books.timestamp.desc()]) [db.Books.timestamp.desc()])
@ -245,7 +245,7 @@ def feed_seriesindex():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_series(book_id): def feed_series(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books,
db.Books.series.any(db.Series.id == book_id), db.Books.series.any(db.Series.id == book_id),
[db.Books.series_index]) [db.Books.series_index])
@ -276,7 +276,7 @@ def feed_ratingindex():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_ratings(book_id): def feed_ratings(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books,
db.Books.ratings.any(db.Ratings.id == book_id), db.Books.ratings.any(db.Ratings.id == book_id),
[db.Books.timestamp.desc()]) [db.Books.timestamp.desc()])
@ -304,7 +304,7 @@ def feed_formatindex():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_format(book_id): def feed_format(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books,
db.Books.data.any(db.Data.format == book_id.upper()), db.Books.data.any(db.Data.format == book_id.upper()),
[db.Books.timestamp.desc()]) [db.Books.timestamp.desc()])
@ -338,7 +338,7 @@ def feed_languagesindex():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_languages(book_id): def feed_languages(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books,
db.Books.languages.any(db.Languages.id == book_id), db.Books.languages.any(db.Languages.id == book_id),
[db.Books.timestamp.desc()]) [db.Books.timestamp.desc()])
@ -408,7 +408,7 @@ def get_metadata_calibre_companion(uuid, library):
def feed_search(term): def feed_search(term):
if term: if term:
entries = calibre_db.get_search_results(term) entries, __ = calibre_db.get_search_results(term)
entriescount = len(entries) if len(entries) > 0 else 1 entriescount = len(entries) if len(entries) > 0 else 1
pagination = Pagination(1, entriescount, entriescount) pagination = Pagination(1, entriescount, entriescount)
return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination) return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination)

@ -5,7 +5,7 @@ body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{
.cover .badge{ .cover .badge{
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; left: 0;
background-color: #cc7b19; background-color: #cc7b19;
border-radius: 0; border-radius: 0;
padding: 0 8px; padding: 0 8px;

File diff suppressed because one or more lines are too long

@ -51,7 +51,22 @@ body h2 {
color:#444; color:#444;
} }
a { color: #45b29d; } a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d; }
.book-remove:hover { color: #23527c; }
.btn-default a { color: #444; }
.btn-default a:hover {
color: #45b29d;
text-decoration: None;
}
.btn-default:hover {
color: #45b29d;
}
.editable-click, a.editable-click, a.editable-click:hover { border-bottom: None; }
.navigation .nav-head { .navigation .nav-head {
text-transform: uppercase; text-transform: uppercase;
@ -63,6 +78,7 @@ a { color: #45b29d; }
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
padding-top: 20px; padding-top: 20px;
} }
.navigation li a { .navigation li a {
color: #444; color: #444;
text-decoration: none; text-decoration: none;

@ -411,6 +411,19 @@ bitjs.archive = bitjs.archive || {};
return "unrar.js"; return "unrar.js";
}; };
/**
* Unrarrer5
* @extends {bitjs.archive.Unarchiver}
* @constructor
*/
bitjs.archive.Unrarrer5 = function(arrayBuffer, optPathToBitJS) {
bitjs.base(this, arrayBuffer, optPathToBitJS);
};
bitjs.inherits(bitjs.archive.Unrarrer5, bitjs.archive.Unarchiver);
bitjs.archive.Unrarrer5.prototype.getScriptFileName = function() {
return "unrar5.js";
};
/** /**
* Untarrer * Untarrer
* @extends {bitjs.archive.Unarchiver} * @extends {bitjs.archive.Unarchiver}

@ -14,10 +14,10 @@
/* global VM_FIXEDGLOBALSIZE, VM_GLOBALMEMSIZE, MAXWINMASK, VM_GLOBALMEMADDR, MAXWINSIZE */ /* global VM_FIXEDGLOBALSIZE, VM_GLOBALMEMSIZE, MAXWINMASK, VM_GLOBALMEMADDR, MAXWINSIZE */
// This file expects to be invoked as a Worker (see onmessage below). // This file expects to be invoked as a Worker (see onmessage below).
importScripts("../io/bitstream.js"); /*importScripts("../io/bitstream.js");
importScripts("../io/bytebuffer.js"); importScripts("../io/bytebuffer.js");
importScripts("archive.js"); importScripts("archive.js");
importScripts("rarvm.js"); importScripts("rarvm.js");*/
// Progress variables. // Progress variables.
var currentFilename = ""; var currentFilename = "";
@ -29,19 +29,21 @@ var totalFilesInArchive = 0;
// Helper functions. // Helper functions.
var info = function(str) { var info = function(str) {
postMessage(new bitjs.archive.UnarchiveInfoEvent(str)); console.log(str);
// postMessage(new bitjs.archive.UnarchiveInfoEvent(str));
}; };
var err = function(str) { var err = function(str) {
postMessage(new bitjs.archive.UnarchiveErrorEvent(str)); console.log(str);
// postMessage(new bitjs.archive.UnarchiveErrorEvent(str));
}; };
var postProgress = function() { var postProgress = function() {
postMessage(new bitjs.archive.UnarchiveProgressEvent( /*postMessage(new bitjs.archive.UnarchiveProgressEvent(
currentFilename, currentFilename,
currentFileNumber, currentFileNumber,
currentBytesUnarchivedInFile, currentBytesUnarchivedInFile,
currentBytesUnarchived, currentBytesUnarchived,
totalUncompressedBytesInArchive, totalUncompressedBytesInArchive,
totalFilesInArchive)); totalFilesInArchive));*/
}; };
// shows a byte value as its hex representation // shows a byte value as its hex representation
@ -1298,7 +1300,7 @@ var unrar = function(arrayBuffer) {
totalUncompressedBytesInArchive = 0; totalUncompressedBytesInArchive = 0;
totalFilesInArchive = 0; totalFilesInArchive = 0;
postMessage(new bitjs.archive.UnarchiveStartEvent()); //postMessage(new bitjs.archive.UnarchiveStartEvent());
var bstream = new bitjs.io.BitStream(arrayBuffer, false /* rtl */); var bstream = new bitjs.io.BitStream(arrayBuffer, false /* rtl */);
var header = new RarVolumeHeader(bstream); var header = new RarVolumeHeader(bstream);
@ -1348,7 +1350,7 @@ var unrar = function(arrayBuffer) {
localfile.unrar(); localfile.unrar();
if (localfile.isValid) { if (localfile.isValid) {
postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile)); // postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile));
postProgress(); postProgress();
} }
} }
@ -1358,7 +1360,7 @@ var unrar = function(arrayBuffer) {
} else { } else {
err("Invalid RAR file"); err("Invalid RAR file");
} }
postMessage(new bitjs.archive.UnarchiveFinishEvent()); // postMessage(new bitjs.archive.UnarchiveFinishEvent());
}; };
// event.data.file has the ArrayBuffer. // event.data.file has the ArrayBuffer.

File diff suppressed because it is too large Load Diff

@ -19,6 +19,17 @@ var direction = 0; // Descending order
var sort = 0; // Show sorted entries var sort = 0; // Show sorted entries
$("#sort_name").click(function() { $("#sort_name").click(function() {
var class_name = $("h1").attr('Class') + "_sort_name";
var obj = {};
obj[class_name] = sort;
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/view",
data: JSON.stringify({obj}),
});
var count = 0; var count = 0;
var index = 0; var index = 0;
var store; var store;
@ -40,9 +51,7 @@ $("#sort_name").click(function() {
count++; count++;
} }
}); });
/*listItems.sort(function(a,b){
return $(a).children()[1].innerText.localeCompare($(b).children()[1].innerText)
});*/
// Find count of middle element // Find count of middle element
if (count > 20) { if (count > 20) {
var middle = parseInt(count / 2, 10) + (count % 2); var middle = parseInt(count / 2, 10) + (count % 2);

@ -162,10 +162,15 @@ function initProgressClick() {
function loadFromArrayBuffer(ab) { function loadFromArrayBuffer(ab) {
var start = (new Date).getTime(); var start = (new Date).getTime();
var h = new Uint8Array(ab, 0, 10); var h = new Uint8Array(ab, 0, 10);
unrar5(ab);
var pathToBitJS = "../../static/js/archive/"; var pathToBitJS = "../../static/js/archive/";
var lastCompletion = 0; var lastCompletion = 0;
if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { //Rar! /*if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { //Rar!
unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS); if (h[7] === 0x01) {
unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS);
} else {
unarchiver = new bitjs.archive.Unrarrer5(ab, pathToBitJS);
}
} else if (h[0] === 80 && h[1] === 75) { //PK (Zip) } else if (h[0] === 80 && h[1] === 75) { //PK (Zip)
unarchiver = new bitjs.archive.Unzipper(ab, pathToBitJS); unarchiver = new bitjs.archive.Unzipper(ab, pathToBitJS);
} else if (h[0] === 255 && h[1] === 216) { // JPEG } else if (h[0] === 255 && h[1] === 216) { // JPEG
@ -229,7 +234,7 @@ function loadFromArrayBuffer(ab) {
unarchiver.start(); unarchiver.start();
} else { } else {
alert("Some error"); alert("Some error");
} }*/
} }
function scrollTocToActive() { function scrollTocToActive() {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -58,6 +58,60 @@ $(document).on("change", "select[data-controlall]", function() {
} }
}); });
$("#delete_confirm").click(function() {
//get data-id attribute of the clicked element
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
var path = src.substring(0, src.lastIndexOf("/"));
var deleteId = $(this).data("delete-id");
var bookFormat = $(this).data("delete-format");
if (bookFormat) {
window.location.href = path + "/../../delete/" + deleteId + "/" + bookFormat;
} else {
if ($(this).data("delete-format")) {
path = path + "/../../ajax/delete/" + deleteId;
$.ajax({
method:"get",
url: path,
timeout: 900,
success:function(data) {
data.forEach(function(item) {
if (!jQuery.isEmptyObject(item)) {
if (item.format != "") {
$("button[data-delete-format='"+item.format+"']").addClass('hidden');
}
$( ".navbar" ).after( '<div class="row-fluid text-center" style="margin-top: -20px;">' +
'<div id="flash_'+item.type+'" class="alert alert-'+item.type+'">'+item.message+'</div>' +
'</div>');
}
});
}
});
} else {
window.location.href = path + "/../../delete/" + deleteId;
}
}
});
//triggered when modal is about to be shown
$("#deleteModal").on("show.bs.modal", function(e) {
//get data-id attribute of the clicked element and store in button
var bookId = $(e.relatedTarget).data("delete-id");
var bookfomat = $(e.relatedTarget).data("delete-format");
if (bookfomat) {
$("#book_format").removeClass('hidden');
$("#book_complete").addClass('hidden');
} else {
$("#book_complete").removeClass('hidden');
$("#book_format").addClass('hidden');
}
$(e.currentTarget).find("#delete_confirm").data("delete-id", bookId);
$(e.currentTarget).find("#delete_confirm").data("delete-format", bookfomat);
});
$(function() { $(function() {
var updateTimerID; var updateTimerID;
@ -324,16 +378,19 @@ $(function() {
}); });
$(".update-view").click(function(e) { $(".update-view").click(function(e) {
var target = $(this).data("target");
var view = $(this).data("view"); var view = $(this).data("view");
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
var data = {}; $.ajax({
data[target] = view; method:"post",
console.debug("Updating view data: ", data); contentType: "application/json; charset=utf-8",
$.post( "/ajax/view", data).done(function( ) { dataType: "json",
location.reload(); url: window.location.pathname + "/../../ajax/view",
data: JSON.stringify({"series_view":view}),
success: function success() {
location.reload();
}
}); });
}); });
}); });

@ -1,5 +1,5 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) /* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2018 OzzieIsaacs * Copyright (C) 2020 OzzieIsaacs
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -15,10 +15,157 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
/* exported TableActions, RestrictionActions*/ /* exported TableActions, RestrictionActions, EbookActions, responseHandler */
var selections = [];
$(function() { $(function() {
$("#books-table").on("check.bs.table check-all.bs.table uncheck.bs.table uncheck-all.bs.table",
function (e, rowsAfter, rowsBefore) {
var rows = rowsAfter;
if (e.type === "uncheck-all") {
rows = rowsBefore;
}
var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) {
return row.id;
});
var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference";
selections = window._[func](selections, ids);
if (selections.length >= 2) {
$("#merge_books").removeClass("disabled");
$("#merge_books").attr("aria-disabled", false);
} else {
$("#merge_books").addClass("disabled");
$("#merge_books").attr("aria-disabled", true);
}
if (selections.length < 1) {
$("#delete_selection").addClass("disabled");
$("#delete_selection").attr("aria-disabled", true);
}
else{
$("#delete_selection").removeClass("disabled");
$("#delete_selection").attr("aria-disabled", false);
}
});
$("#delete_selection").click(function() {
$("#books-table").bootstrapTable('uncheckAll');
});
$("#merge_confirm").click(function() {
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/mergebooks",
data: JSON.stringify({"Merge_books":selections}),
success: function success() {
$('#books-table').bootstrapTable('refresh');
$("#books-table").bootstrapTable('uncheckAll');
}
});
});
$("#merge_books").click(function() {
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/simulatemerge",
data: JSON.stringify({"Merge_books":selections}),
success: function success(book_titles) {
$.each(book_titles.from, function(i, item) {
$("<span>- " + item + "</span>").appendTo("#merge_from");
});
$('#merge_to').text("- " + book_titles.to);
}
});
});
var column = [];
$("#books-table > thead > tr > th").each(function() {
var element = {};
if ($(this).attr("data-edit")) {
element = {
editable: {
mode: "inline",
emptytext: "<span class='glyphicon glyphicon-plus'></span>",
}
};
}
var validateText = $(this).attr("data-edit-validate");
if (validateText) {
element.editable.validate = function (value) {
if ($.trim(value) === "") return validateText;
};
}
column.push(element);
});
$("#books-table").bootstrapTable({
sidePagination: "server",
pagination: true,
paginationDetailHAlign: " hidden",
paginationHAlign: "left",
idField: "id",
uniqueId: "id",
search: true,
showColumns: true,
searchAlign: "left",
showSearchButton : false,
searchOnEnterKey: true,
checkboxHeader: false,
maintainMetaData: true,
responseHandler: responseHandler,
columns: column,
formatNoMatches: function () {
return "";
},
onEditableSave: function (field, row, oldvalue, $el) {
if (field === 'title' || field === 'authors') {
$.ajax({
method:"get",
dataType: "json",
url: window.location.pathname + "/../../ajax/sort_value/" + field + '/' + row.id,
success: function success(data) {
var key = Object.keys(data)[0]
$("#books-table").bootstrapTable('updateCellByUniqueId', {
id: row.id,
field: key,
value: data[key]
});
console.log(data);
}
});
}
},
onColumnSwitch: function (field, checked) {
var visible = $("#books-table").bootstrapTable('getVisibleColumns');
var hidden = $("#books-table").bootstrapTable('getHiddenColumns');
var visibility =[]
var st = ""
visible.forEach(function(item) {
st += "\""+ item.field + "\":\"" +"true"+ "\","
});
hidden.forEach(function(item) {
st += "\""+ item.field + "\":\"" +"false"+ "\","
});
st = st.slice(0, -1);
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/table_settings",
data: "{" + st + "}",
});
},
});
$("#domain_allow_submit").click(function(event) { $("#domain_allow_submit").click(function(event) {
event.preventDefault(); event.preventDefault();
$("#domain_add_allow").ajaxForm(); $("#domain_add_allow").ajaxForm();
@ -33,6 +180,7 @@ $(function() {
} }
}); });
}); });
$("#domain-allow-table").bootstrapTable({ $("#domain-allow-table").bootstrapTable({
formatNoMatches: function () { formatNoMatches: function () {
return ""; return "";
@ -205,6 +353,7 @@ function TableActions (value, row) {
].join(""); ].join("");
} }
/* Function for deleting domain restrictions */ /* Function for deleting domain restrictions */
function RestrictionActions (value, row) { function RestrictionActions (value, row) {
return [ return [
@ -213,3 +362,20 @@ function RestrictionActions (value, row) {
"</div>" "</div>"
].join(""); ].join("");
} }
/* Function for deleting books */
function EbookActions (value, row) {
return [
"<div class=\"book-remove\" data-toggle=\"modal\" data-target=\"#deleteModal\" data-ajax=\"1\" data-delete-id=\"" + row.id + "\" title=\"Remove\">",
"<i class=\"glyphicon glyphicon-trash\"></i>",
"</div>"
].join("");
}
/* Function for keeping checked rows */
function responseHandler(res) {
$.each(res.rows, function (i, row) {
row.state = $.inArray(row.id, selections) !== -1;
});
return res;
}

@ -161,8 +161,8 @@
</table> </table>
<div class="hidden" id="update_error"> <span>{{update_error}}</span></div> <div class="hidden" id="update_error"> <span>{{update_error}}</span></div>
<div class="btn btn-default" id="check_for_update">{{_('Check for Update')}}</div> <div class="btn btn-primary" id="check_for_update">{{_('Check for Update')}}</div>
<div class="btn btn-default hidden" id="perform_update" data-toggle="modal" data-target="#StatusDialog">{{_('Perform Update')}}</div> <div class="btn btn-primary hidden" id="perform_update" data-toggle="modal" data-target="#StatusDialog">{{_('Perform Update')}}</div>
</div> </div>
</div> </div>
</div> </div>

@ -23,14 +23,14 @@
<h3>{{_("In Library")}}</h3> <h3>{{_("In Library")}}</h3>
{% endif %} {% endif %}
<div class="filterheader hidden-xs hidden-sm"> <div class="filterheader hidden-xs hidden-sm">
<a id="new" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a> <a id="new" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a id="old" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> <a id="old" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
<a id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a> <a id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
<a id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a> <a id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
<a id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a> <a id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> <a id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
<!--div class="btn-group character" role="group"> <!--div class="btn-group character" role="group">
<a id="no_shelf" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort='pubold')}}"><span class="glyphicon glyphicon-list"></span><b>?</b></a> <a id="no_shelf" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='pubold')}}"><span class="glyphicon glyphicon-list"></span><b>?</b></a>
<div id="all" class="btn btn-primary">{{_('All')}}</div> <div id="all" class="btn btn-primary">{{_('All')}}</div>
</div--> </div-->
</div> </div>
@ -53,7 +53,7 @@
{% if not loop.first %} {% if not loop.first %}
<span class="author-hidden-divider">&amp;</span> <span class="author-hidden-divider">&amp;</span>
{% endif %} {% endif %}
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %} {% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a> <a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %} {% endif %}
@ -61,7 +61,7 @@
{% if not loop.first %} {% if not loop.first %}
<span>&amp;</span> <span>&amp;</span>
{% endif %} {% endif %}
<a class="author-name" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% for format in entry.data %} {% for format in entry.data %}

@ -7,14 +7,12 @@
</div> </div>
{% if g.user.role_delete_books() %} {% if g.user.role_delete_books() %}
<div class="text-center"> <div class="text-center">
<button type="button" class="btn btn-danger" id="delete" data-toggle="modal" data-target="#deleteModal">{{_("Delete Book")}}</button> <button type="button" class="btn btn-danger" id="delete" data-toggle="modal" data-delete-id="{{ book.id }}" data-target="#deleteModal">{{_("Delete Book")}}</button>
</div> </div>
{% if book.data|length > 1 %} {% if book.data|length > 1 %}
<div class="text-center more-stuff"><h4>{{_('Delete formats:')}}</h4> <div class="text-center more-stuff"><h4>{{_('Delete formats:')}}</h4>
{% for file in book.data %} {% for file in book.data %}
<div class="form-group"> <button type="button" class="btn btn-danger" id="delete_format" data-toggle="modal" data-delete-id="{{ book.id }}" data-delete-format="{{ file.format }}" data-target="#deleteModal">{{_('Delete')}} - {{file.format}}</button>
<a href="{{ url_for('editbook.delete_book', book_id=book.id, book_format=file.format) }}" class="btn btn-danger" type="button">{{_('Delete')}} - {{file.format}}</a>
</div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
@ -193,34 +191,7 @@
{% endblock %} {% endblock %}
{% block modal %} {% block modal %}
{% if g.user.role_delete_books() %} {{ delete_book(book.id) }}
<div class="modal fade" id="deleteModal" role="dialog" aria-labelledby="metaDeleteLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span>{{_('Are you really sure?')}}</span>
</div>
<div class="modal-body text-center">
<p>
<span>{{_('This book will be permanently erased from database')}}</span>
<span>{{_('and hard disk')}}</span>
</p>
{% if config.config_kobo_sync %}
<p>
<span>{{_('Important Kobo Note: deleted books will remain on any paired Kobo device.')}}</span>
<span>{{_('Books must first be archived and the device synced before a book can safely be deleted.')}}</span>
</p>
{% endif %}
</div>
<div class="modal-footer">
<a href="{{ url_for('editbook.delete_book', book_id=book.id) }}" id="delete_confirm" class="btn btn-danger">{{_('Delete')}}</a>
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
{% endif %}
<div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel"> <div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">

@ -1,59 +1,99 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% macro text_table_row(parameter, edit_text, show_text, validate) -%}
<h1 class="{{page}}">{{_(title)}}</h1> <th data-field="{{ parameter }}" id="{{ parameter }}" data-sortable="true"
data-visible = "{{visiblility.get(parameter)}}"
{% if g.user.role_edit() %}
data-editable-type="text"
data-editable-url="{{ url_for('editbook.edit_list_book', param=parameter)}}"
data-editable-title="{{ edit_text }}"
data-edit="true"
{% if validate %}data-edit-validate="{{ _('This Field is Required') }}" {% endif %}
{% endif %}
>{{ show_text }}</th>
{%- endmacro %}
<div class="filterheader hidden-xs hidden-sm"> {% block header %}
{% if entries.__len__() %} <link href="{{ url_for('static', filename='css/libs/bootstrap-table.min.css') }}" rel="stylesheet">
{% if data == 'author' %} <link href="{{ url_for('static', filename='css/libs/bootstrap-editable.css') }}" rel="stylesheet">
<button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button> {% endblock %}
{% endif %} {% block body %}
{% endif %} <h2 class="{{page}}">{{_(title)}}</h2>
<button id="desc" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button> <div class="col-xs-12 col-sm-6">
<button id="asc" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button> <div class="row">
{% if charlist|length %} <div class="btn btn-default disabled" id="merge_books" data-toggle="modal" data-target="#mergeModal" aria-disabled="true">{{_('Merge selected books')}}</div>
<button id="all" class="btn btn-primary">{{_('All')}}</button> <div class="btn btn-default disabled" id="delete_selection" aria-disabled="true">{{_('Remove Selections')}}</div>
{% endif %} </div>
<div class="btn-group character" role="group"> </div>
{% for char in charlist%} <div class="col-xs-12 col-sm-6">
<button class="btn btn-primary char">{{char.char}}</button> <div class="row">
{% endfor %} <input type="checkbox" id="autoupdate_titlesort" name="autoupdate_titlesort" checked>
<label for="autoupdate_titlesort">{{_('Update Title Sort automatically')}}</label>
</div>
<div class="row">
<input type="checkbox" id="autoupdate_autorsort" name="autoupdate_autorsort" checked>
<label for="autoupdate_autorsort">{{_('Update Author Sort automatically')}}</label>
</div> </div>
{% if title == "Series" %}
<button class="update-view btn btn-primary" href="#" data-target="series_view" data-view="grid">Grid</button>
{% endif %}
</div> </div>
<div class="container">
<div id="list" class="col-xs-12 col-sm-6"> <table id="books-table" class="table table-no-bordered table-striped"
{% for entry in entries %} data-url="{{url_for('web.list_books')}}">
{% if loop.index0 == (loop.length/2+loop.length%2)|int and loop.length > 20 %} <thead>
<tr>
{% if g.user.role_edit() %}
<th data-field="state" data-checkbox="true" data-sortable="true"></th>
{% endif %}
<th data-field="id" id="id" data-visible="false" data-switchable="false"></th>
{{ text_table_row('title', _('Enter Title'),_('Title'), true) }}
{{ text_table_row('sort', _('Enter Title Sort'),_('Title Sort'), false) }}
{{ text_table_row('author_sort', _('Enter Author Sort'),_('Author Sort'), false) }}
{{ text_table_row('authors', _('Enter Authors'),_('Authors'), true) }}
{{ text_table_row('tags', _('Enter Categories'),_('Categories'), false) }}
{{ text_table_row('series', _('Enter Series'),_('Series'), false) }}
<th data-field="series_index" id="series_index" data-visible="{{visiblility.get('series_index')}}" data-edit-validate="{{ _('This Field is Required') }}" data-sortable="true" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.01" data-editable-min="0" data-editable-url="{{ url_for('editbook.edit_list_book', param='series_index')}}" data-edit="true" data-editable-title="{{_('Enter title')}}"{% endif %}>{{_('Series Index')}}</th>
{{ text_table_row('languages', _('Enter Languages'),_('Languages'), false) }}
<!--th data-field="pubdate" data-type="date" data-visible="{{visiblility.get('pubdate')}}" data-viewformat="dd.mm.yyyy" id="pubdate" data-sortable="true">{{_('Publishing Date')}}</th-->
{{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false) }}
{% if g.user.role_edit() %}
<th data-align="right" data-formatter="EbookActions" data-switchable="false">{{_('Delete')}}</th>
{% endif %}
</tr>
</thead>
</table>
{% endblock %}
{% block modal %}
{{ delete_book(0) }}
{% if g.user.role_edit() %}
<div class="modal fade" id="mergeModal" role="dialog" aria-labelledby="metaMergeLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span>{{_('Are you really sure?')}}</span>
</div>
<div class="modal-body">
<p></p>
<div class="text-left">{{_('Books with Title will be merged from:')}}</div>
<p></p>
<div class=text-left" id="merge_from"></div>
<p></p>
<div class="text-left">{{_('Into Book with Title:')}}</div>
<p></p>
<div class=text-left" id="merge_to"></div>
</div> </div>
<div id="second" class="col-xs-12 col-sm-6"> <div class="modal-footer">
{% endif %} <input type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" id="merge_confirm" data-dismiss="modal">
<div class="row" {% if entry[0].sort %}data-name="{{entry[0].name}}"{% endif %} data-id="{% if entry[0].sort %}{{entry[0].sort}}{% else %}{% if entry.name %}{{entry.name}}{% else %}{{entry[0].name}}{% endif %}{% endif %}"> <button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
<div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{entry.count}}</span></div>
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{% if entry.format %}{{url_for('web.books_list', data=data, sort='new', book_id=entry.format )}}{% else %}{{url_for('web.books_list', data=data, sort='new', book_id=entry[0].id )}}{% endif %}">
{% if entry.name %}
<div class="rating">
{% for number in range(entry.name) %}
<span class="glyphicon glyphicon-star good"></span>
{% if loop.last and loop.index < 5 %}
{% for numer in range(5 - loop.index) %}
<span class="glyphicon glyphicon-star"></span>
{% endfor %}
{% endif %}
{% endfor %}
</div>
{% else %}
{% if entry.format %}
{{entry.format}}
{% else %}
{{entry[0].name}}{% endif %}{% endif %}</a></div>
</div> </div>
{% endfor %}
</div> </div>
</div> </div>
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script src="{{ url_for('static', filename='js/filter_list.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
<script>
</script>
{% endblock %} {% endblock %}

@ -92,7 +92,7 @@
<h2 id="title">{{entry.title|shortentitle(40)}}</h2> <h2 id="title">{{entry.title|shortentitle(40)}}</h2>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a> <a href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a>
{% if not loop.last %} {% if not loop.last %}
&amp; &amp;
{% endif %} {% endif %}
@ -114,7 +114,7 @@
{% endif %} {% endif %}
{% if entry.series|length > 0 %} {% if entry.series|length > 0 %}
<p>{{_('Book')}} {{entry.series_index}} {{_('of')}} <a href="{{url_for('web.books_list', data='series',sort='abc', book_id=entry.series[0].id)}}">{{entry.series[0].name}}</a></p> <p>{{_('Book')}} {{entry.series_index}} {{_('of')}} <a href="{{url_for('web.books_list', data='series', sort_param='abc', book_id=entry.series[0].id)}}">{{entry.series[0].name}}</a></p>
{% endif %} {% endif %}
{% if entry.languages.__len__() > 0 %} {% if entry.languages.__len__() > 0 %}
@ -143,7 +143,7 @@
<span class="glyphicon glyphicon-tags"></span> <span class="glyphicon glyphicon-tags"></span>
{% for tag in entry.tags %} {% for tag in entry.tags %}
<a href="{{ url_for('web.books_list', data='category', sort='new', book_id=tag.id) }}" class="btn btn-xs btn-info" role="button">{{tag.name}}</a> <a href="{{ url_for('web.books_list', data='category', sort_param='new', book_id=tag.id) }}" class="btn btn-xs btn-info" role="button">{{tag.name}}</a>
{%endfor%} {%endfor%}
</p> </p>
@ -154,13 +154,13 @@
<div class="publishers"> <div class="publishers">
<p> <p>
<span>{{_('Publisher')}}: <span>{{_('Publisher')}}:
<a href="{{url_for('web.books_list', data='publisher', sort='new', book_id=entry.publishers[0].id ) }}">{{entry.publishers[0].name}}</a> <a href="{{url_for('web.books_list', data='publisher', sort_param='new', book_id=entry.publishers[0].id ) }}">{{entry.publishers[0].name}}</a>
</span> </span>
</p> </p>
</div> </div>
{% endif %} {% endif %}
{% if entry.pubdate[:10] != '0101-01-01' %} {% if (entry.pubdate|string)[:10] != '0101-01-01' %}
<div class="publishing-date"> <div class="publishing-date">
<p>{{_('Published')}}: {{entry.pubdate|formatdate}} </p> <p>{{_('Published')}}: {{entry.pubdate|formatdate}} </p>
</div> </div>
@ -281,7 +281,7 @@
{% if g.user.role_edit() %} {% if g.user.role_edit() %}
<div class="btn-toolbar" role="toolbar"> <div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Edit/Delete book"> <div class="btn-group" role="group" aria-label="Edit/Delete book">
<a href="{{ url_for('editbook.edit_book', book_id=entry.id) }}" class="btn btn-sm btn-warning" id="edit_book" role="button"><span class="glyphicon glyphicon-edit"></span> {{_('Edit Metadata')}}</a> <a href="{{ url_for('editbook.edit_book', book_id=entry.id) }}" class="btn btn-sm btn-primary" id="edit_book" role="button"><span class="glyphicon glyphicon-edit"></span> {{_('Edit Metadata')}}</a>
</div> </div>
</div> </div>
{% endif %} {% endif %}

@ -22,7 +22,7 @@
{% if not loop.first %} {% if not loop.first %}
<span class="author-hidden-divider">&amp;</span> <span class="author-hidden-divider">&amp;</span>
{% endif %} {% endif %}
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %} {% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a> <a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %} {% endif %}
@ -30,7 +30,7 @@
{% if not loop.first %} {% if not loop.first %}
<span>&amp;</span> <span>&amp;</span>
{% endif %} {% endif %}
<a class="author-name" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</p> </p>

@ -27,13 +27,13 @@
{% for entry in entries %} {% for entry in entries %}
<div class="col-sm-3 col-lg-2 col-xs-6 book sortable" {% if entry[0].sort %}data-name="{{entry[0].series[0].name}}"{% endif %} data-id="{% if entry[0].series[0].name %}{{entry[0].series[0].name}}{% endif %}"> <div class="col-sm-3 col-lg-2 col-xs-6 book sortable" {% if entry[0].sort %}data-name="{{entry[0].series[0].name}}"{% endif %} data-id="{% if entry[0].series[0].name %}{{entry[0].series[0].name}}{% endif %}">
<div class="cover"> <div class="cover">
<a href="{{url_for('web.books_list', data=data, sort='new', book_id=entry[0].series[0].id )}}"> <a href="{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].series[0].id )}}">
<img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/> <img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/>
<span class="badge">{{entry.count}}</span> <span class="badge">{{entry.count}}</span>
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">
<a href="{{url_for('web.books_list', data=data, sort='new', book_id=entry[0].series[0].id )}}"> <a href="{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].series[0].id )}}">
<p class="title">{{entry[0].series[0].name|shortentitle}}</p> <p class="title">{{entry[0].series[0].name|shortentitle}}</p>
</a> </a>
</div> </div>

@ -21,7 +21,7 @@
{% if not loop.first %} {% if not loop.first %}
<span class="author-hidden-divider">&amp;</span> <span class="author-hidden-divider">&amp;</span>
{% endif %} {% endif %}
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %} {% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a> <a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %} {% endif %}
@ -29,7 +29,7 @@
{% if not loop.first %} {% if not loop.first %}
<span>&amp;</span> <span>&amp;</span>
{% endif %} {% endif %}
<a class="author-name" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</p> </p>
@ -54,14 +54,14 @@
<div class="discover load-more"> <div class="discover load-more">
<h2 class="{{title}}">{{_(title)}}</h2> <h2 class="{{title}}">{{_(title)}}</h2>
<div class="filterheader hidden-xs hidden-sm"> <div class="filterheader hidden-xs hidden-sm">
<a data-toggle="tooltip" id="new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a> <a data-toggle="tooltip" id="new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a id="old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> <a id="old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
<a id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a> <a id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
<a id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a> <a id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
<a id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a> <a id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> <a id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
<!--div class="btn-group character"> <!--div class="btn-group character">
<a id="no_shelf" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort='pubold')}}"><span class="glyphicon glyphicon-list"></span> <b>{{_('Group by series')}}</b></a> <a id="no_shelf" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='pubold')}}"><span class="glyphicon glyphicon-list"></span> <b>{{_('Group by series')}}</b></a>
</div--> </div-->
</div> </div>
@ -84,7 +84,7 @@
{% if not loop.first %} {% if not loop.first %}
<span class="author-hidden-divider">&amp;</span> <span class="author-hidden-divider">&amp;</span>
{% endif %} {% endif %}
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', book_id=author.id, sort='new') }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', book_id=author.id, sort_param='new') }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %} {% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a> <a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %} {% endif %}
@ -92,7 +92,7 @@
{% if not loop.first %} {% if not loop.first %}
<span>&amp;</span> <span>&amp;</span>
{% endif %} {% endif %}
<a class="author-name" href="{{url_for('web.books_list', data='author', book_id=author.id, sort='new') }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', book_id=author.id, sort_param='new') }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% for format in entry.data %} {% for format in entry.data %}

@ -10,7 +10,7 @@
{% endif %} {% endif %}
<div class="row"> <div class="row">
<div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{lang_counter[loop.index0].bookcount}}</span></div> <div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{lang_counter[loop.index0].bookcount}}</span></div>
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{{url_for('web.books_list', book_id=lang.lang_code, data=data, sort='new')}}">{{lang.name}}</a></div> <div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{{url_for('web.books_list', book_id=lang.lang_code, data=data, sort_param='new')}}">{{lang.name}}</a></div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

@ -1,4 +1,4 @@
{% from 'modal_restriction.html' import restrict_modal %} {% from 'modal_dialogs.html' import restrict_modal, delete_book %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ g.user.locale }}"> <html lang="{{ g.user.locale }}">
<head> <head>
@ -128,7 +128,7 @@
<li class="nav-head hidden-xs">{{_('Browse')}}</li> <li class="nav-head hidden-xs">{{_('Browse')}}</li>
{% for element in sidebar %} {% for element in sidebar %}
{% if g.user.check_visibility(element['visibility']) and element['public'] %} {% if g.user.check_visibility(element['visibility']) and element['public'] %}
<li id="nav_{{element['id']}}" {% if page == element['page'] %}class="active"{% endif %}><a href="{{url_for(element['link'], data=element['page'], sort='new')}}"><span class="glyphicon {{element['glyph']}}"></span>{{_(element['text'])}}</a></li> <li id="nav_{{element['id']}}" {% if page == element['page'] %}class="active"{% endif %}><a href="{{url_for(element['link'], data=element['page'], sort_param='stored')}}"><span class="glyphicon {{element['glyph']}}"></span>{{_(element['text'])}}</a></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if g.user.is_authenticated or g.allow_anonymous %} {% if g.user.is_authenticated or g.allow_anonymous %}
@ -136,10 +136,6 @@
{% for shelf in g.shelves_access %} {% for shelf in g.shelves_access %}
<li><a href="{{url_for('shelf.show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list shelf"></span>{{shelf.name|shortentitle(40)}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li> <li><a href="{{url_for('shelf.show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list shelf"></span>{{shelf.name|shortentitle(40)}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li>
{% endfor %} {% endfor %}
<!--li class="nav-head hidden-xs your-shelves">{{_('Your Shelves')}}</li>
{% for shelf in g.user.shelf %}
<li><a href="{{url_for('shelf.show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list private_shelf"></span>{{shelf.name|shortentitle(40)}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li>
{% endfor %}-->
{% if not g.user.is_anonymous %} {% if not g.user.is_anonymous %}
<li id="nav_createshelf" class="create-shelf"><a href="{{url_for('shelf.create_shelf')}}">{{_('Create a Shelf')}}</a></li> <li id="nav_createshelf" class="create-shelf"><a href="{{url_for('shelf.create_shelf')}}">{{_('Create a Shelf')}}</a></li>
<li id="nav_about" {% if page == 'stat' %}class="active"{% endif %}><a href="{{url_for('about.stats')}}"><span class="glyphicon glyphicon-info-sign"></span> {{_('About')}}</a></li> <li id="nav_about" {% if page == 'stat' %}class="active"{% endif %}><a href="{{url_for('about.stats')}}"><span class="glyphicon glyphicon-info-sign"></span> {{_('About')}}</a></li>
@ -155,29 +151,29 @@
{% if pagination and (pagination.has_next or pagination.has_prev) %} {% if pagination and (pagination.has_next or pagination.has_prev) %}
<div class="pagination"> <div class="pagination">
{% if pagination.has_prev %} {% if pagination.has_prev %}
<a class="previous" href="{{ (pagination.page - 1)|url_for_other_page <li class="page-item page-next"><a class="page-link" aria-label="next page" href="{{ (pagination.page - 1)|url_for_other_page
}}">&laquo; {{_('Previous')}}</a> }}">&laquo; {{_('Previous')}}</a></li>
{% endif %} {% endif %}
{% for page in pagination.iter_pages() %} {% for page in pagination.iter_pages() %}
{% if page %} {% if page %}
{% if page != pagination.page %} {% if page != pagination.page %}
<a href="{{ (page)|url_for_other_page }}">{{ page }}</a> <li class="page-item"><a class="page-link" aria-label="to page {{ page }}" href="{{ (page)|url_for_other_page }}">{{ page }}</a></li>
{% else %} {% else %}
<strong>{{ page }}</strong> <li class="page-item active"><a class="page-link" aria-label="to page {{ page }}" href="{{ (page)|url_for_other_page }}">{{ page }}</a></li>
{% endif %} {% endif %}
{% else %} {% else %}
<span class="ellipsis"></span> <li class="page-item page-last-separator disabled"><a class="page-link" aria-label=""></a></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if pagination.has_next %} {% if pagination.has_next %}
<a class="next" href="{{ (pagination.page + 1)|url_for_other_page <li class="page-item page-next"><a class="page-link" aria-label="next page" href="{{ (pagination.page + 1)|url_for_other_page
}}">{{_('Next')}} &raquo;</a> }}">{{_('Next')}} &raquo;</a></li>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div>
</div> </div>
</div> </div>
</div>
<div class="modal fade" id="bookDetailsModal" tabindex="-1" role="dialog" aria-labelledby="bookDetailsModalLabel"> <div class="modal fade" id="bookDetailsModal" tabindex="-1" role="dialog" aria-labelledby="bookDetailsModalLabel">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content"> <div class="modal-content">
@ -196,7 +192,6 @@
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<!--script src="https://code.jquery.com/jquery.js"></script-->
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
<!-- Include all compiled plugins (below), or include individual files as needed --> <!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="{{ url_for('static', filename='js/libs/bootstrap.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/bootstrap.min.js') }}"></script>
@ -227,9 +222,11 @@
}); });
$(document).ready(function() { $(document).ready(function() {
var inp = $('#query').first() var inp = $('#query').first()
var val = inp.val() if (inp.length) {
if (val !== "undefined") { var val = inp.val()
if (val.length) {
inp.val('').blur().focus().val(val) inp.val('').blur().focus().val(val)
}
} }
}); });
}); });

@ -32,7 +32,7 @@
{% endif %} {% endif %}
<div class="row" {% if entry[0].sort %}data-name="{{entry[0].name}}"{% endif %} data-id="{% if entry[0].sort %}{{entry[0].sort}}{% else %}{% if entry.name %}{{entry.name}}{% else %}{{entry[0].name}}{% endif %}{% endif %}"> <div class="row" {% if entry[0].sort %}data-name="{{entry[0].name}}"{% endif %} data-id="{% if entry[0].sort %}{{entry[0].sort}}{% else %}{% if entry.name %}{{entry.name}}{% else %}{{entry[0].name}}{% endif %}{% endif %}">
<div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{entry.count}}</span></div> <div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{entry.count}}</span></div>
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{% if entry.format %}{{url_for('web.books_list', data=data, sort='new', book_id=entry.format )}}{% else %}{{url_for('web.books_list', data=data, sort='new', book_id=entry[0].id )}}{% endif %}"> <div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{% if entry.format %}{{url_for('web.books_list', data=data, sort_param='new', book_id=entry.format )}}{% else %}{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].id )}}{% endif %}">
{% if entry.name %} {% if entry.name %}
<div class="rating"> <div class="rating">
{% for number in range(entry.name) %} {% for number in range(entry.name) %}

@ -37,3 +37,34 @@
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro delete_book(bookid) %}
{% if g.user.role_delete_books() %}
<div class="modal fade" id="deleteModal" role="dialog" aria-labelledby="metaDeleteLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span>{{_('Are you really sure?')}}</span>
</div>
<div class="modal-body text-center">
<p>
<span class="hidden" id="book_format">{{_('This book format will be permanently erased from database')}}</span>
<span class="hidden" id="book_complete">{{_('This book will be permanently erased from database')}}</span>
<span>{{_('and hard disk')}}</span>
</p>
{% if config.config_kobo_sync %}
<p>
<span>{{_('Important Kobo Note: deleted books will remain on any paired Kobo device.')}}</span>
<span>{{_('Books must first be archived and the device synced before a book can safely be deleted.')}}</span>
</p>
{% endif %}
</div>
<div class="modal-footer">
<input type="button" class="btn btn-danger" value="{{_('Delete')}}" name="delete_confirm" id="delete_confirm" data-dismiss="modal">
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endmacro %}

@ -14,8 +14,13 @@
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/io/bytestream.js') }}"></script>
<script src="{{ url_for('static', filename='js/io/bytebuffer.js') }}"></script>
<script src="{{ url_for('static', filename='js/io/bitstream.js') }}"></script>
<script src="{{ url_for('static', filename='js/archive/archive.js') }}"></script>
<script src="{{ url_for('static', filename='js/archive/rarvm.js') }}"></script>
<script src="{{ url_for('static', filename='js/archive/unrar5.js') }}"></script>
<script src="{{ url_for('static', filename='js/kthoom.js') }}"></script> <script src="{{ url_for('static', filename='js/kthoom.js') }}"></script>
<script src="{{ url_for('static', filename='js/archive/archive.js') }}"></script>
<script> <script>
var updateArrows = function() { var updateArrows = function() {
if ($('input[name="direction"]:checked').val() === "0") { if ($('input[name="direction"]:checked').val() === "0") {

@ -5,7 +5,7 @@
<h2>{{_('No Results Found')}} {{adv_searchterm}}</h2> <h2>{{_('No Results Found')}} {{adv_searchterm}}</h2>
<p>{{_('Search Term:')}} {{adv_searchterm}}</p> <p>{{_('Search Term:')}} {{adv_searchterm}}</p>
{% else %} {% else %}
<h2>{{entries|length}} {{_('Results for:')}} {{adv_searchterm}}</h2> <h2>{{result_count}} {{_('Results for:')}} {{adv_searchterm}}</h2>
{% if g.user.is_authenticated %} {% if g.user.is_authenticated %}
{% if g.user.shelf.all() or g.shelves_access %} {% if g.user.shelf.all() or g.shelves_access %}
<div id="shelf-actions" class="btn-toolbar" role="toolbar"> <div id="shelf-actions" class="btn-toolbar" role="toolbar">
@ -25,18 +25,14 @@
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
<!--div class="filterheader hidden-xs hidden-sm"--><!-- ToDo: Implement filter for search results --> <div class="filterheader hidden-xs hidden-sm"><!-- ToDo: Implement filter for search results -->
<!--a id="new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='new')}}"><span class="glyphicon glyphicon-sort-by-order"></span></a> <a id="new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='new', query=query)}}"><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a id="old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='old')}}"><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> <a id="old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='old', query=query)}}"><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
<a id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a> <a id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='abc', query=query)}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
<a id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a> <a id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='zyx', query=query)}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
<a id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a> <a id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='pubnew', query=query)}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> <a id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='pubold', query=query)}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
</div> </div>
<div class="btn-group character" role="group">
<a id="no_shelf" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='pubold')}}"><span class="glyphicon glyphicon-list"></span><b>?</b></a>
<div id="all" class="btn btn-primary">{{_('All')}}</div>
</div-->
{% endif %} {% endif %}
<div class="row"> <div class="row">
@ -59,7 +55,7 @@
{% if not loop.first %} {% if not loop.first %}
<span class="author-hidden-divider">&amp;</span> <span class="author-hidden-divider">&amp;</span>
{% endif %} {% endif %}
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %} {% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a> <a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %} {% endif %}
@ -67,7 +63,7 @@
{% if not loop.first %} {% if not loop.first %}
<span>&amp;</span> <span>&amp;</span>
{% endif %} {% endif %}
<a class="author-name" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% for format in entry.data %} {% for format in entry.data %}

@ -1,7 +1,7 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
<div class="col-md-10 col-lg-6"> <div class="col-md-10 col-lg-6">
<form role="form" id="search" action="{{ url_for('web.advanced_search') }}" method="GET"> <form role="form" id="search" action="{{ url_for('web.advanced_search_form') }}" method="POST">
<div class="form-group"> <div class="form-group">
<label for="book_title">{{_('Book Title')}}</label> <label for="book_title">{{_('Book Title')}}</label>
<input type="text" class="form-control" name="book_title" id="book_title" value=""> <input type="text" class="form-control" name="book_title" id="book_title" value="">

@ -31,7 +31,7 @@
{% if not loop.first %} {% if not loop.first %}
<span class="author-hidden-divider">&amp;</span> <span class="author-hidden-divider">&amp;</span>
{% endif %} {% endif %}
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %} {% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a> <a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %} {% endif %}
@ -39,7 +39,7 @@
{% if not loop.first %} {% if not loop.first %}
<span>&amp;</span> <span>&amp;</span>
{% endif %} {% endif %}
<a class="author-name" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</p> </p>

@ -37,7 +37,7 @@
<p class="title">{{entry.title|shortentitle}}</p> <p class="title">{{entry.title|shortentitle}}</p>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')}}</a> <a href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')}}</a>
{% if not loop.last %} {% if not loop.last %}
&amp; &amp;
{% endif %} {% endif %}

@ -23,6 +23,7 @@ import sys
import datetime import datetime
import itertools import itertools
import uuid import uuid
import json
from binascii import hexlify from binascii import hexlify
from flask import g from flask import g
@ -41,7 +42,7 @@ except ImportError:
oauth_support = False oauth_support = False
from sqlalchemy import create_engine, exc, exists, event from sqlalchemy import create_engine, exc, exists, event
from sqlalchemy import Column, ForeignKey from sqlalchemy import Column, ForeignKey
from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float, JSON
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import backref, relationship, sessionmaker, Session from sqlalchemy.orm import backref, relationship, sessionmaker, Session
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
@ -109,10 +110,10 @@ def get_sidebar_config(kwargs=None):
{"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived", {"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived", "visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived",
"show_text": _('Show archived books'), "config_show": content}) "show_text": _('Show archived books'), "config_show": content})
'''sidebar.append( sidebar.append(
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_list', "id": "list", {"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
"visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list", "visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list",
"show_text": _('Show Books List'), "config_show": content})''' "show_text": _('Show Books List'), "config_show": content})
return sidebar return sidebar
@ -218,7 +219,9 @@ class User(UserBase, Base):
denied_column_value = Column(String, default="") denied_column_value = Column(String, default="")
allowed_column_value = Column(String, default="") allowed_column_value = Column(String, default="")
remote_auth_token = relationship('RemoteAuthToken', backref='user', lazy='dynamic') remote_auth_token = relationship('RemoteAuthToken', backref='user', lazy='dynamic')
series_view = Column(String(10), default="list") #series_view = Column(String(10), default="list")
view_settings = Column(JSON, default={})
if oauth_support: if oauth_support:
@ -259,7 +262,8 @@ class Anonymous(AnonymousUserMixin, UserBase):
self.allowed_tags = data.allowed_tags self.allowed_tags = data.allowed_tags
self.denied_column_value = data.denied_column_value self.denied_column_value = data.denied_column_value
self.allowed_column_value = data.allowed_column_value self.allowed_column_value = data.allowed_column_value
self.series_view = data.series_view self.view_settings = data.view_settings
def role_admin(self): def role_admin(self):
return False return False
@ -567,10 +571,11 @@ def migrate_Database(session):
conn.execute("ALTER TABLE user ADD column `allowed_column_value` String DEFAULT ''") conn.execute("ALTER TABLE user ADD column `allowed_column_value` String DEFAULT ''")
session.commit() session.commit()
try: try:
session.query(exists().where(User.series_view)).scalar() session.query(exists().where(User.view_settings)).scalar()
except exc.OperationalError: except exc.OperationalError:
with engine.connect() as conn: with engine.connect() as conn:
conn.execute("ALTER TABLE user ADD column `series_view` VARCHAR(10) DEFAULT 'list'") conn.execute("ALTER TABLE user ADD column `view_settings` VARCHAR(10) DEFAULT '{}'")
session.commit()
if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() \ if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() \
is None: is None:
@ -591,14 +596,15 @@ def migrate_Database(session):
"locale VARCHAR(2)," "locale VARCHAR(2),"
"sidebar_view INTEGER," "sidebar_view INTEGER,"
"default_language VARCHAR(3)," "default_language VARCHAR(3),"
"series_view VARCHAR(10)," # "series_view VARCHAR(10),"
"view_settings VARCHAR,"
"UNIQUE (nickname)," "UNIQUE (nickname),"
"UNIQUE (email))") "UNIQUE (email))")
conn.execute("INSERT INTO user_id(id, nickname, email, role, password, kindle_mail,locale," conn.execute("INSERT INTO user_id(id, nickname, email, role, password, kindle_mail,locale,"
"sidebar_view, default_language, series_view) " "sidebar_view, default_language, view_settings) "
"SELECT id, nickname, email, role, password, kindle_mail, locale," "SELECT id, nickname, email, role, password, kindle_mail, locale,"
"sidebar_view, default_language FROM user") "sidebar_view, default_language FROM user")
# delete old user table and rename new user_id table to user: # delete old user table and rename new user_id table to user:
conn.execute("DROP TABLE user") conn.execute("DROP TABLE user")
conn.execute("ALTER TABLE user_id RENAME TO user") conn.execute("ALTER TABLE user_id RENAME TO user")
session.commit() session.commit()

@ -30,8 +30,8 @@ import traceback
import binascii import binascii
import re import re
from babel import Locale as LC
from babel.dates import format_date from babel.dates import format_date
from babel import Locale as LC
from babel.core import UnknownLocaleError from babel.core import UnknownLocaleError
from flask import Blueprint, jsonify from flask import Blueprint, jsonify
from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, url_for from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, url_for
@ -39,6 +39,7 @@ from flask_babel import gettext as _
from flask_login import login_user, logout_user, login_required, current_user, confirm_login from flask_login import login_user, logout_user, login_required, current_user, confirm_login
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
from sqlalchemy.sql.expression import text, func, true, false, not_, and_, or_ from sqlalchemy.sql.expression import text, func, true, false, not_, and_, or_
from sqlalchemy.orm.attributes import flag_modified
from werkzeug.exceptions import default_exceptions, InternalServerError from werkzeug.exceptions import default_exceptions, InternalServerError
from sqlalchemy.sql.functions import coalesce from sqlalchemy.sql.functions import coalesce
@ -287,14 +288,6 @@ def edit_required(f):
# ################################### Helper functions ################################################################ # ################################### Helper functions ################################################################
# Returns the template for rendering and includes the instance name
def render_title_template(*args, **kwargs):
sidebar = ub.get_sidebar_config(kwargs)
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar,
accept=constants.EXTENSIONS_UPLOAD,
*args, **kwargs)
@web.before_app_request @web.before_app_request
def before_request(): def before_request():
if current_user.is_authenticated: if current_user.is_authenticated:
@ -472,20 +465,30 @@ def toggle_archived(book_id):
@web.route("/ajax/view", methods=["POST"]) @web.route("/ajax/view", methods=["POST"])
@login_required @login_required
def update_view(): def update_view():
to_save = request.form.to_dict() to_save = request.get_json()
allowed_view = ['grid', 'list'] allowed_view = ['grid', 'list']
if "series_view" in to_save and to_save["series_view"] in allowed_view: if "series_view" in to_save and to_save["series_view"] in allowed_view:
current_user.series_view = to_save["series_view"] try:
#visibility = json.loads(current_user.view_settings)
current_user.view_settings['series_view'] = to_save["series_view"]
# current_user.view_settings = json.dumps(visibility)
try:
flag_modified(current_user, "view_settings")
except AttributeError:
pass
ub.session.commit()
except InvalidRequestError:
log.error("Invalid request received: %r ", request, )
return "Invalid request", 400
except Exception:
log.error("Could not save series_view_settings: %r %r", request, to_save)
return "Invalid request", 400
elif "authorslist" in to_save:
pass
else: else:
log.error("Invalid request received: %r %r", request, to_save) log.error("Invalid request received: %r %r", request, to_save)
return "Invalid request", 400 return "Invalid request", 400
return "1", 200
try:
ub.session.commit()
except InvalidRequestError:
log.error("Invalid request received: %r ", request, )
return "Invalid request", 400
return "", 200
''' '''
@ -609,25 +612,29 @@ def get_matching_tags():
return json_dumps return json_dumps
# ################################### View Books list ################################################################## # Returns the template for rendering and includes the instance name
def render_title_template(*args, **kwargs):
sidebar = ub.get_sidebar_config(kwargs)
@web.route("/", defaults={'page': 1}) return render_template(instance=config.config_calibre_web_title, sidebar=sidebar,
@web.route('/page/<int:page>') accept=constants.EXTENSIONS_UPLOAD,
@login_required_if_no_ano *args, **kwargs)
def index(page):
entries, random, pagination = calibre_db.fill_indexpage(page, db.Books, True, [db.Books.timestamp.desc()])
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=_(u"Recently Added Books"), page="root")
@web.route('/<data>/<sort>', defaults={'page': 1, 'book_id': "1"}) def render_books_list(data, sort, book_id, page):
@web.route('/<data>/<sort>/', defaults={'page': 1, 'book_id': "1"})
@web.route('/<data>/<sort>/<book_id>', defaults={'page': 1})
@web.route('/<data>/<sort>/<book_id>/<int:page>')
@login_required_if_no_ano
def books_list(data, sort, book_id, page):
order = [db.Books.timestamp.desc()] order = [db.Books.timestamp.desc()]
if sort == 'stored':
view = current_user.view_settings.get(data)
sort = view
else:
try:
current_user.view_settings[data] = sort
try:
flag_modified(current_user, "view_settings")
except AttributeError:
pass
ub.session.commit()
except InvalidRequestError:
log.error("Invalid request received: %r ", request, )
if sort == 'pubnew': if sort == 'pubnew':
order = [db.Books.pubdate.desc()] order = [db.Books.pubdate.desc()]
if sort == 'pubold': if sort == 'pubold':
@ -643,7 +650,7 @@ def books_list(data, sort, book_id, page):
if data == "rated": if data == "rated":
if current_user.check_visibility(constants.SIDEBAR_BEST_RATED): if current_user.check_visibility(constants.SIDEBAR_BEST_RATED):
entries, random, pagination = calibre_db.fill_indexpage(page, entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.ratings.any(db.Ratings.rating > 9), db.Books.ratings.any(db.Ratings.rating > 9),
order) order)
@ -653,7 +660,7 @@ def books_list(data, sort, book_id, page):
abort(404) abort(404)
elif data == "discover": elif data == "discover":
if current_user.check_visibility(constants.SIDEBAR_RANDOM): if current_user.check_visibility(constants.SIDEBAR_RANDOM):
entries, __, pagination = calibre_db.fill_indexpage(page, db.Books, True, [func.randomblob(2)]) entries, __, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, [func.randomblob(2)])
pagination = Pagination(1, config.config_books_per_page, config.config_books_per_page) pagination = Pagination(1, config.config_books_per_page, config.config_books_per_page)
return render_title_template('discover.html', entries=entries, pagination=pagination, id=book_id, return render_title_template('discover.html', entries=entries, pagination=pagination, id=book_id,
title=_(u"Discover (Random Books)"), page="discover") title=_(u"Discover (Random Books)"), page="discover")
@ -681,10 +688,18 @@ def books_list(data, sort, book_id, page):
return render_language_books(page, book_id, order) return render_language_books(page, book_id, order)
elif data == "archived": elif data == "archived":
return render_archived_books(page, order) return render_archived_books(page, order)
elif data == "search":
term = (request.args.get('query') or '')
offset = int(int(config.config_books_per_page) * (page - 1))
if '&' not in term:
return render_search_results(term, offset, order, config.config_books_per_page)
else:
return render_adv_search_results(term, offset, order, config.config_books_per_page)
else: else:
entries, random, pagination = calibre_db.fill_indexpage(page, db.Books, True, order) website = data or "newest"
entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=_(u"Books"), page="newest") title=_(u"Books"), page=website)
def render_hot_books(page): def render_hot_books(page):
@ -717,7 +732,7 @@ def render_hot_books(page):
def render_author_books(page, author_id, order): def render_author_books(page, author_id, order):
entries, __, pagination = calibre_db.fill_indexpage(page, entries, __, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.authors.any(db.Authors.id == author_id), db.Books.authors.any(db.Authors.id == author_id),
[order[0], db.Series.name, db.Books.series_index], [order[0], db.Series.name, db.Books.series_index],
@ -745,7 +760,7 @@ def render_author_books(page, author_id, order):
def render_publisher_books(page, book_id, order): def render_publisher_books(page, book_id, order):
publisher = calibre_db.session.query(db.Publishers).filter(db.Publishers.id == book_id).first() publisher = calibre_db.session.query(db.Publishers).filter(db.Publishers.id == book_id).first()
if publisher: if publisher:
entries, random, pagination = calibre_db.fill_indexpage(page, entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.publishers.any(db.Publishers.id == book_id), db.Books.publishers.any(db.Publishers.id == book_id),
[db.Series.name, order[0], db.Books.series_index], [db.Series.name, order[0], db.Books.series_index],
@ -760,7 +775,7 @@ def render_publisher_books(page, book_id, order):
def render_series_books(page, book_id, order): def render_series_books(page, book_id, order):
name = calibre_db.session.query(db.Series).filter(db.Series.id == book_id).first() name = calibre_db.session.query(db.Series).filter(db.Series.id == book_id).first()
if name: if name:
entries, random, pagination = calibre_db.fill_indexpage(page, entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.series.any(db.Series.id == book_id), db.Books.series.any(db.Series.id == book_id),
[db.Books.series_index, order[0]]) [db.Books.series_index, order[0]])
@ -772,7 +787,7 @@ def render_series_books(page, book_id, order):
def render_ratings_books(page, book_id, order): def render_ratings_books(page, book_id, order):
name = calibre_db.session.query(db.Ratings).filter(db.Ratings.id == book_id).first() name = calibre_db.session.query(db.Ratings).filter(db.Ratings.id == book_id).first()
entries, random, pagination = calibre_db.fill_indexpage(page, entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.ratings.any(db.Ratings.id == book_id), db.Books.ratings.any(db.Ratings.id == book_id),
[db.Books.timestamp.desc(), order[0]]) [db.Books.timestamp.desc(), order[0]])
@ -786,7 +801,7 @@ def render_ratings_books(page, book_id, order):
def render_formats_books(page, book_id, order): def render_formats_books(page, book_id, order):
name = calibre_db.session.query(db.Data).filter(db.Data.format == book_id.upper()).first() name = calibre_db.session.query(db.Data).filter(db.Data.format == book_id.upper()).first()
if name: if name:
entries, random, pagination = calibre_db.fill_indexpage(page, entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.data.any(db.Data.format == book_id.upper()), db.Books.data.any(db.Data.format == book_id.upper()),
[db.Books.timestamp.desc(), order[0]]) [db.Books.timestamp.desc(), order[0]])
@ -799,7 +814,7 @@ def render_formats_books(page, book_id, order):
def render_category_books(page, book_id, order): def render_category_books(page, book_id, order):
name = calibre_db.session.query(db.Tags).filter(db.Tags.id == book_id).first() name = calibre_db.session.query(db.Tags).filter(db.Tags.id == book_id).first()
if name: if name:
entries, random, pagination = calibre_db.fill_indexpage(page, entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.tags.any(db.Tags.id == book_id), db.Books.tags.any(db.Tags.id == book_id),
[order[0], db.Series.name, db.Books.series_index], [order[0], db.Series.name, db.Books.series_index],
@ -819,19 +834,201 @@ def render_language_books(page, name, order):
lang_name = _(isoLanguages.get(part3=name).name) lang_name = _(isoLanguages.get(part3=name).name)
except KeyError: except KeyError:
abort(404) abort(404)
entries, random, pagination = calibre_db.fill_indexpage(page, entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.languages.any(db.Languages.lang_code == name), db.Books.languages.any(db.Languages.lang_code == name),
[db.Books.timestamp.desc(), order[0]]) [db.Books.timestamp.desc(), order[0]])
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name, return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name,
title=_(u"Language: %(name)s", name=lang_name), page="language") title=_(u"Language: %(name)s", name=lang_name), page="language")
def render_read_books(page, are_read, as_xml=False, order=None, *args, **kwargs):
order = order or []
if not config.config_read_column:
if are_read:
db_filter = and_(ub.ReadBook.user_id == int(current_user.id),
ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED)
else:
db_filter = coalesce(ub.ReadBook.read_status, 0) != ub.ReadBook.STATUS_FINISHED
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db_filter,
order,
ub.ReadBook, db.Books.id==ub.ReadBook.book_id)
else:
try:
if are_read:
db_filter = db.cc_classes[config.config_read_column].value == True
else:
db_filter = coalesce(db.cc_classes[config.config_read_column].value, False) != True
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db_filter,
order,
db.cc_classes[config.config_read_column])
except (KeyError, AttributeError):
log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column)
if not as_xml:
flash(_("Custom Column No.%(column)d is not existing in calibre database",
column=config.config_read_column),
category="error")
return redirect(url_for("web.index"))
# ToDo: Handle error Case for opds
if as_xml:
return entries, pagination
else:
if are_read:
name = _(u'Read Books') + ' (' + str(pagination.total_count) + ')'
pagename = "read"
else:
name = _(u'Unread Books') + ' (' + str(pagination.total_count) + ')'
pagename = "unread"
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=name, page=pagename)
def render_archived_books(page, order):
order = order or []
archived_books = (
ub.session.query(ub.ArchivedBook)
.filter(ub.ArchivedBook.user_id == int(current_user.id))
.filter(ub.ArchivedBook.is_archived == True)
.all()
)
archived_book_ids = [archived_book.book_id for archived_book in archived_books]
archived_filter = db.Books.id.in_(archived_book_ids)
entries, random, pagination = calibre_db.fill_indexpage_with_archived_books(page, 0,
db.Books,
archived_filter,
order,
allow_show_archived=True)
name = _(u'Archived Books') + ' (' + str(len(archived_book_ids)) + ')'
pagename = "archived"
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=name, page=pagename)
def render_prepare_search_form(cc):
# prepare data for search-form
tags = calibre_db.session.query(db.Tags)\
.join(db.books_tags_link)\
.join(db.Books)\
.filter(calibre_db.common_filters()) \
.group_by(text('books_tags_link.tag'))\
.order_by(db.Tags.name).all()
series = calibre_db.session.query(db.Series)\
.join(db.books_series_link)\
.join(db.Books)\
.filter(calibre_db.common_filters()) \
.group_by(text('books_series_link.series'))\
.order_by(db.Series.name)\
.filter(calibre_db.common_filters()).all()
extensions = calibre_db.session.query(db.Data)\
.join(db.Books)\
.filter(calibre_db.common_filters()) \
.group_by(db.Data.format)\
.order_by(db.Data.format).all()
if current_user.filter_language() == u"all":
languages = calibre_db.speaking_language()
else:
languages = None
return render_title_template('search_form.html', tags=tags, languages=languages, extensions=extensions,
series=series, title=_(u"search"), cc=cc, page="advsearch")
def render_search_results(term, offset=None, order=None, limit=None):
entries, result_count = calibre_db.get_search_results(term, offset, order, limit)
ids = list()
for element in entries:
ids.append(element.id)
searched_ids[current_user.id] = ids
return render_title_template('search.html',
searchterm=term,
query=term,
adv_searchterm=term,
entries=entries,
result_count=result_count,
title=_(u"Search"),
page="search")
# ################################### View Books list ##################################################################
'''@web.route("/table") @web.route("/", defaults={'page': 1})
@web.route('/page/<int:page>')
@login_required_if_no_ano @login_required_if_no_ano
def index(page):
sort_param = (request.args.get('sort') or 'stored').lower()
return render_books_list("newest", sort_param, 1, page)
@web.route('/<data>/<sort_param>', defaults={'page': 1, 'book_id': "1"})
@web.route('/<data>/<sort_param>/', defaults={'page': 1, 'book_id': "1"})
@web.route('/<data>/<sort_param>/<book_id>', defaults={'page': 1})
@web.route('/<data>/<sort_param>/<book_id>/<int:page>')
@login_required_if_no_ano
def books_list(data, sort_param, book_id, page):
return render_books_list(data, sort_param, book_id, page)
@web.route("/table")
@login_required
def books_table(): def books_table():
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name, visibility = current_user.view_settings.get('table', {})
title=_(u"Language: %(name)s", name=lang_name), page="language")''' return render_title_template('book_table.html', title=_(u"Books list"), page="book_table",
visiblility=visibility)
@web.route("/ajax/listbooks")
@login_required
def list_books():
off = request.args.get("offset") or 0
limit = request.args.get("limit") or config.config_books_per_page
sort = request.args.get("sort")
if request.args.get("order") == 'asc':
order = [db.Books.timestamp.asc()]
else:
order = [db.Books.timestamp.desc()]
search = request.args.get("search")
total_count = calibre_db.session.query(db.Books).count()
if search:
entries, filtered_count = calibre_db.get_search_results(search, off, order, limit)
else:
entries, __, __ = calibre_db.fill_indexpage((int(off) / (int(limit)) + 1), limit, db.Books, True, order)
filtered_count = total_count
for entry in entries:
for index in range(0, len(entry.languages)):
try:
entry.languages[index].language_name = LC.parse(entry.languages[index].lang_code)\
.get_language_name(get_locale())
except UnknownLocaleError:
entry.languages[index].language_name = _(
isoLanguages.get(part3=entry.languages[index].lang_code).name)
table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": entries}
js_list = json.dumps(table_entries, cls=db.AlchemyEncoder)
#js_list = json.dumps(entries, cls=db.AlchemyEncoder)
response = make_response(js_list)
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@web.route("/ajax/table_settings", methods=['POST'])
@login_required
def update_table_settings():
# vals = request.get_json()
# ToDo: Save table settings
current_user.view_settings['table'] = json.loads(request.data)
try:
try:
flag_modified(current_user, "view_settings")
except AttributeError:
pass
ub.session.commit()
except InvalidRequestError:
log.error("Invalid request received: %r ", request, )
return "Invalid request", 400
return ""
@web.route("/author") @web.route("/author")
@login_required_if_no_ano @login_required_if_no_ano
@ -871,7 +1068,7 @@ def publisher_list():
@login_required_if_no_ano @login_required_if_no_ano
def series_list(): def series_list():
if current_user.check_visibility(constants.SIDEBAR_SERIES): if current_user.check_visibility(constants.SIDEBAR_SERIES):
if current_user.series_view == 'list': if current_user.view_settings.get('series_view') == 'list':
entries = calibre_db.session.query(db.Series, func.count('books_series_link.book').label('count')) \ entries = calibre_db.session.query(db.Series, func.count('books_series_link.book').label('count')) \
.join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \ .join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_series_link.series')).order_by(db.Series.sort).all() .group_by(text('books_series_link.series')).order_by(db.Series.sort).all()
@ -988,55 +1185,43 @@ def reconnect():
# ################################### Search functions ################################################################ # ################################### Search functions ################################################################
@web.route("/search", methods=["GET"]) @web.route("/search", methods=["GET"])
@login_required_if_no_ano @login_required_if_no_ano
def search(): def search():
term = request.args.get("query") term = request.args.get("query")
if term: if term:
entries = calibre_db.get_search_results(term) return render_search_results(term)
ids = list()
for element in entries:
ids.append(element.id)
searched_ids[current_user.id] = ids
return render_title_template('search.html',
searchterm=term,
adv_searchterm=term,
entries=entries,
title=_(u"Search"),
page="search")
else: else:
return render_title_template('search.html', return render_title_template('search.html',
searchterm="", searchterm="",
result_count=0,
title=_(u"Search"), title=_(u"Search"),
page="search") page="search")
@web.route("/advanced_search", methods=['POST'])
@web.route("/advanced_search", methods=['GET'])
@login_required_if_no_ano @login_required_if_no_ano
def advanced_search(): def advanced_search():
# Build custom columns names
cc = get_cc_columns(filter_config_custom_read=True) cc = get_cc_columns(filter_config_custom_read=True)
calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase) calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
q = calibre_db.session.query(db.Books).filter(calibre_db.common_filters(True)).order_by(db.Books.sort) q = calibre_db.session.query(db.Books).filter(calibre_db.common_filters(True)).order_by(db.Books.sort)
include_tag_inputs = request.args.getlist('include_tag') include_tag_inputs = request.form.getlist('include_tag')
exclude_tag_inputs = request.args.getlist('exclude_tag') exclude_tag_inputs = request.form.getlist('exclude_tag')
include_series_inputs = request.args.getlist('include_serie') include_series_inputs = request.form.getlist('include_serie')
exclude_series_inputs = request.args.getlist('exclude_serie') exclude_series_inputs = request.form.getlist('exclude_serie')
include_languages_inputs = request.args.getlist('include_language') include_languages_inputs = request.form.getlist('include_language')
exclude_languages_inputs = request.args.getlist('exclude_language') exclude_languages_inputs = request.form.getlist('exclude_language')
include_extension_inputs = request.args.getlist('include_extension') include_extension_inputs = request.form.getlist('include_extension')
exclude_extension_inputs = request.args.getlist('exclude_extension') exclude_extension_inputs = request.form.getlist('exclude_extension')
author_name = request.args.get("author_name") author_name = request.form.get("author_name")
book_title = request.args.get("book_title") book_title = request.form.get("book_title")
publisher = request.args.get("publisher") publisher = request.form.get("publisher")
pub_start = request.args.get("Publishstart") pub_start = request.form.get("Publishstart")
pub_end = request.args.get("Publishend") pub_end = request.form.get("Publishend")
rating_low = request.args.get("ratinghigh") rating_low = request.form.get("ratinghigh")
rating_high = request.args.get("ratinglow") rating_high = request.form.get("ratinglow")
description = request.args.get("comment") description = request.form.get("comment")
if author_name: if author_name:
author_name = author_name.strip().lower().replace(',', '|') author_name = author_name.strip().lower().replace(',', '|')
if book_title: if book_title:
@ -1047,8 +1232,8 @@ def advanced_search():
searchterm = [] searchterm = []
cc_present = False cc_present = False
for c in cc: for c in cc:
if request.args.get('custom_column_' + str(c.id)): if request.form.get('custom_column_' + str(c.id)):
searchterm.extend([(u"%s: %s" % (c.name, request.args.get('custom_column_' + str(c.id))))]) searchterm.extend([(u"%s: %s" % (c.name, request.form.get('custom_column_' + str(c.id))))])
cc_present = True cc_present = True
if include_tag_inputs or exclude_tag_inputs or include_series_inputs or exclude_series_inputs or \ if include_tag_inputs or exclude_tag_inputs or include_series_inputs or exclude_series_inputs or \
@ -1087,8 +1272,8 @@ def advanced_search():
searchterm.extend(ext for ext in exclude_extension_inputs) searchterm.extend(ext for ext in exclude_extension_inputs)
# handle custom columns # handle custom columns
for c in cc: for c in cc:
if request.args.get('custom_column_' + str(c.id)): if request.form.get('custom_column_' + str(c.id)):
searchterm.extend([(u"%s: %s" % (c.name, request.args.get('custom_column_' + str(c.id))))]) searchterm.extend([(u"%s: %s" % (c.name, request.form.get('custom_column_' + str(c.id))))])
searchterm = " + ".join(filter(None, searchterm)) searchterm = " + ".join(filter(None, searchterm))
q = q.filter() q = q.filter()
if author_name: if author_name:
@ -1131,7 +1316,7 @@ def advanced_search():
# search custom culumns # search custom culumns
for c in cc: for c in cc:
custom_query = request.args.get('custom_column_' + str(c.id)) custom_query = request.form.get('custom_column_' + str(c.id))
if custom_query != '' and custom_query is not None: if custom_query != '' and custom_query is not None:
if c.datatype == 'bool': if c.datatype == 'bool':
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any( q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
@ -1150,102 +1335,21 @@ def advanced_search():
for element in q: for element in q:
ids.append(element.id) ids.append(element.id)
searched_ids[current_user.id] = ids searched_ids[current_user.id] = ids
return render_title_template('search.html', adv_searchterm=searchterm, return render_title_template('search.html',
entries=q, title=_(u"search"), page="search") adv_searchterm=searchterm,
# prepare data for search-form query=request.form,
tags = calibre_db.session.query(db.Tags)\ entries=q,
.join(db.books_tags_link)\ result_count=len(q),
.join(db.Books)\ title=_(u"search"), page="search")
.filter(calibre_db.common_filters()) \
.group_by(text('books_tags_link.tag'))\
.order_by(db.Tags.name).all()
series = calibre_db.session.query(db.Series)\
.join(db.books_series_link)\
.join(db.Books)\
.filter(calibre_db.common_filters()) \
.group_by(text('books_series_link.series'))\
.order_by(db.Series.name)\
.filter(calibre_db.common_filters()).all()
extensions = calibre_db.session.query(db.Data)\
.join(db.Books)\
.filter(calibre_db.common_filters()) \
.group_by(db.Data.format)\
.order_by(db.Data.format).all()
if current_user.filter_language() == u"all":
languages = calibre_db.speaking_language()
else:
languages = None
return render_title_template('search_form.html', tags=tags, languages=languages, extensions=extensions,
series=series, title=_(u"search"), cc=cc, page="advsearch")
def render_read_books(page, are_read, as_xml=False, order=None, *args, **kwargs):
order = order or []
if not config.config_read_column:
if are_read:
db_filter = and_(ub.ReadBook.user_id == int(current_user.id),
ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED)
else:
db_filter = coalesce(ub.ReadBook.read_status, 0) != ub.ReadBook.STATUS_FINISHED
entries, random, pagination = calibre_db.fill_indexpage(page,
db.Books,
db_filter,
order,
ub.ReadBook, db.Books.id==ub.ReadBook.book_id)
else:
try:
if are_read:
db_filter = db.cc_classes[config.config_read_column].value == True
else:
db_filter = coalesce(db.cc_classes[config.config_read_column].value, False) != True
entries, random, pagination = calibre_db.fill_indexpage(page,
db.Books,
db_filter,
order,
db.cc_classes[config.config_read_column])
except (KeyError, AttributeError):
log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column)
if not as_xml:
flash(_("Custom Column No.%(column)d is not existing in calibre database",
column=config.config_read_column),
category="error")
return redirect(url_for("web.index"))
# ToDo: Handle error Case for opds
if as_xml:
return entries, pagination
else:
if are_read:
name = _(u'Read Books') + ' (' + str(pagination.total_count) + ')'
pagename = "read"
else:
name = _(u'Unread Books') + ' (' + str(pagination.total_count) + ')'
pagename = "unread"
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=name, page=pagename)
def render_archived_books(page, order):
order = order or []
archived_books = (
ub.session.query(ub.ArchivedBook)
.filter(ub.ArchivedBook.user_id == int(current_user.id))
.filter(ub.ArchivedBook.is_archived == True)
.all()
)
archived_book_ids = [archived_book.book_id for archived_book in archived_books]
archived_filter = db.Books.id.in_(archived_book_ids)
entries, random, pagination = calibre_db.fill_indexpage_with_archived_books(page, @web.route("/advanced_search", methods=['GET'])
db.Books, @login_required_if_no_ano
archived_filter, def advanced_search_form():
order, # Build custom columns names
allow_show_archived=True) cc = get_cc_columns(filter_config_custom_read=True)
return render_prepare_search_form(cc)
name = _(u'Archived Books') + ' (' + str(len(archived_book_ids)) + ')'
pagename = "archived"
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=name, page=pagename)
# ################################### Download/Send ################################################################## # ################################### Download/Send ##################################################################
@ -1551,10 +1655,11 @@ def profile():
languages = calibre_db.speaking_language() languages = calibre_db.speaking_language()
translations = babel.list_translations() + [LC('en')] translations = babel.list_translations() + [LC('en')]
kobo_support = feature_support['kobo'] and config.config_kobo_sync kobo_support = feature_support['kobo'] and config.config_kobo_sync
if feature_support['oauth']: if feature_support['oauth'] and config.config_login_type == 2:
oauth_status = get_oauth_status() oauth_status = get_oauth_status()
else: else:
oauth_status = None oauth_status = None
oauth_check = {}
for book in current_user.downloads: for book in current_user.downloads:
downloadBook = calibre_db.get_book(book.book_id) downloadBook = calibre_db.get_book(book.book_id)

Loading…
Cancel
Save