' % self.name
# Baseclass for Users in Calibre-Web, settings which are depending on certain users are stored here. It is derived from
@@ -168,7 +172,7 @@ class User(UserBase, Base):
__table_args__ = {'sqlite_autoincrement': True}
id = Column(Integer, primary_key=True)
- nickname = Column(String(64), unique=True)
+ name = Column(String(64), unique=True)
email = Column(String(120), unique=True, default="")
role = Column(SmallInteger, default=constants.ROLE_USER)
password = Column(String)
@@ -178,7 +182,6 @@ class User(UserBase, Base):
locale = Column(String(2), default="en")
sidebar_view = Column(Integer, default=1)
default_language = Column(String(3), default="all")
- mature_content = Column(Boolean, default=True)
denied_tags = Column(String, default="")
allowed_tags = Column(String, default="")
denied_column_value = Column(String, default="")
@@ -214,13 +217,12 @@ class Anonymous(AnonymousUserMixin, UserBase):
def loadSettings(self):
data = session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS)\
.first() # type: User
- self.nickname = data.nickname
+ self.name = data.name
self.role = data.role
self.id=data.id
self.sidebar_view = data.sidebar_view
self.default_language = data.default_language
self.locale = data.locale
- # self.mature_content = data.mature_content
self.kindle_mail = data.kindle_mail
self.denied_tags = data.denied_tags
self.allowed_tags = data.allowed_tags
@@ -484,7 +486,7 @@ def migrate_registration_table(engine, session):
def migrate_guest_password(engine, session):
try:
with engine.connect() as conn:
- conn.execute("UPDATE user SET password='' where nickname = 'Guest' and password !=''")
+ conn.execute(text("UPDATE user SET password='' where name = 'Guest' and password !=''"))
session.commit()
except exc.OperationalError:
print('Settings database is not writeable. Exiting...')
@@ -590,37 +592,42 @@ def migrate_Database(session):
with engine.connect() as conn:
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() \
- is None:
- create_anonymous_user(session)
try:
- # check if one table with autoincrement is existing (should be user table)
- with engine.connect() as conn:
- conn.execute("SELECT COUNT(*) FROM sqlite_sequence WHERE name='user'")
+ # check if name is in User table instead of nickname
+ session.query(exists().where(User.name)).scalar()
except exc.OperationalError:
# Create new table user_id and copy contents of table user into it
with engine.connect() as conn:
- conn.execute("CREATE TABLE user_id (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
- "nickname VARCHAR(64),"
+ conn.execute(text("CREATE TABLE user_id (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
+ "name VARCHAR(64),"
"email VARCHAR(120),"
"role SMALLINT,"
"password VARCHAR,"
"kindle_mail VARCHAR(120),"
"locale VARCHAR(2),"
"sidebar_view INTEGER,"
- "default_language VARCHAR(3),"
- "view_settings VARCHAR,"
- "UNIQUE (nickname),"
- "UNIQUE (email))")
- conn.execute("INSERT INTO user_id(id, nickname, email, role, password, kindle_mail,locale,"
- "sidebar_view, default_language, view_settings) "
+ "default_language VARCHAR(3),"
+ "denied_tags VARCHAR,"
+ "allowed_tags VARCHAR,"
+ "denied_column_value VARCHAR,"
+ "allowed_column_value VARCHAR,"
+ "view_settings JSON,"
+ "UNIQUE (name),"
+ "UNIQUE (email))"))
+ conn.execute(text("INSERT INTO user_id(id, name, email, role, password, kindle_mail,locale,"
+ "sidebar_view, default_language, denied_tags, allowed_tags, denied_column_value, "
+ "allowed_column_value, view_settings)"
"SELECT id, nickname, email, role, password, kindle_mail, locale,"
- "sidebar_view, default_language FROM user")
+ "sidebar_view, default_language, denied_tags, allowed_tags, denied_column_value, "
+ "allowed_column_value, view_settings FROM user"))
# delete old user table and rename new user_id table to user:
- conn.execute("DROP TABLE user")
- conn.execute("ALTER TABLE user_id RENAME TO user")
+ conn.execute(text("DROP TABLE user"))
+ conn.execute(text("ALTER TABLE user_id RENAME TO user"))
session.commit()
+ if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() \
+ is None:
+ create_anonymous_user(session)
+
migrate_guest_password(engine, session)
@@ -656,7 +663,7 @@ def delete_download(book_id):
# Generate user Guest (translated text), as anonymous user, no rights
def create_anonymous_user(session):
user = User()
- user.nickname = "Guest"
+ user.name = "Guest"
user.email = 'no@email'
user.role = constants.ROLE_ANONYMOUS
user.password = ''
@@ -671,7 +678,7 @@ def create_anonymous_user(session):
# Generate User admin with admin123 password, and access to everything
def create_admin_user(session):
user = User()
- user.nickname = "admin"
+ user.name = "admin"
user.role = constants.ADMIN_USER_ROLES
user.sidebar_view = constants.ADMIN_USER_SIDEBAR
@@ -707,7 +714,7 @@ def init_db(app_db_path):
if cli.user_credentials:
username, password = cli.user_credentials.split(':')
- user = session.query(User).filter(func.lower(User.nickname) == username.lower()).first()
+ user = session.query(User).filter(func.lower(User.name) == username.lower()).first()
if user:
user.password = generate_password_hash(password)
if session_commit() == "":
diff --git a/cps/updater.py b/cps/updater.py
index 837b72be..87aa842b 100644
--- a/cps/updater.py
+++ b/cps/updater.py
@@ -227,11 +227,17 @@ class Updater(threading.Thread):
os.sep + 'vendor', os.sep + 'calibre-web.log', os.sep + '.git', os.sep + 'client_secrets.json',
os.sep + 'gdrive_credentials', os.sep + 'settings.yaml', os.sep + 'venv', os.sep + 'virtualenv',
os.sep + 'access.log', os.sep + 'access.log1', os.sep + 'access.log2',
- os.sep + '.calibre-web.log.swp', os.sep + '_sqlite3.so'
+ os.sep + '.calibre-web.log.swp', os.sep + '_sqlite3.so', os.sep + 'cps' + os.sep + '.HOMEDIR',
+ os.sep + 'gmail.json'
)
additional_path = self.is_venv()
if additional_path:
exclude = exclude + (additional_path,)
+
+ # check if we are in a package, rename cps.py to __init__.py
+ if constants.HOME_CONFIG:
+ shutil.move(os.path.join(source, 'cps.py'), os.path.join(source, '__init__.py'))
+
for root, dirs, files in os.walk(destination, topdown=True):
for name in files:
old_list.append(os.path.join(root, name).replace(destination, ''))
@@ -398,6 +404,52 @@ class Updater(threading.Thread):
return json.dumps(status)
return ''
+ def _stable_updater_set_status(self, i, newer, status, parents, commit):
+ if i == -1 and newer == False:
+ status.update({
+ 'update': True,
+ 'success': True,
+ 'message': _(
+ u'Click on the button below to update to the latest stable version.'),
+ 'history': parents
+ })
+ self.updateFile = commit[0]['zipball_url']
+ elif i == -1 and newer == True:
+ status.update({
+ 'update': True,
+ 'success': True,
+ 'message': _(u'A new update is available. Click on the button below to '
+ u'update to version: %(version)s', version=commit[0]['tag_name']),
+ 'history': parents
+ })
+ self.updateFile = commit[0]['zipball_url']
+ return status
+
+ def _stable_updater_parse_major_version(self, commit, i, parents, current_version, status):
+ if int(commit[i + 1]['tag_name'].split('.')[1]) == current_version[1]:
+ parents.append([commit[i]['tag_name'],
+ commit[i]['body'].replace('\r\n', '').replace('\n', '
')])
+ status.update({
+ 'update': True,
+ 'success': True,
+ 'message': _(u'A new update is available. Click on the button below to '
+ u'update to version: %(version)s', version=commit[i]['tag_name']),
+ 'history': parents
+ })
+ self.updateFile = commit[i]['zipball_url']
+ else:
+ parents.append([commit[i + 1]['tag_name'],
+ commit[i + 1]['body'].replace('\r\n', '
').replace('\n', '
')])
+ status.update({
+ 'update': True,
+ 'success': True,
+ 'message': _(u'A new update is available. Click on the button below to '
+ u'update to version: %(version)s', version=commit[i + 1]['tag_name']),
+ 'history': parents
+ })
+ self.updateFile = commit[i + 1]['zipball_url']
+ return status, parents
+
def _stable_available_updates(self, request_method):
if request_method == "GET":
parents = []
@@ -459,48 +511,14 @@ class Updater(threading.Thread):
# before major update
if i == (len(commit) - 1):
i -= 1
- if int(commit[i+1]['tag_name'].split('.')[1]) == current_version[1]:
- parents.append([commit[i]['tag_name'],
- commit[i]['body'].replace('\r\n', '
').replace('\n', '
')])
- status.update({
- 'update': True,
- 'success': True,
- 'message': _(u'A new update is available. Click on the button below to '
- u'update to version: %(version)s', version=commit[i]['tag_name']),
- 'history': parents
- })
- self.updateFile = commit[i]['zipball_url']
- else:
- parents.append([commit[i+1]['tag_name'],
- commit[i+1]['body'].replace('\r\n', '
').replace('\n', '
')])
- status.update({
- 'update': True,
- 'success': True,
- 'message': _(u'A new update is available. Click on the button below to '
- u'update to version: %(version)s', version=commit[i+1]['tag_name']),
- 'history': parents
- })
- self.updateFile = commit[i+1]['zipball_url']
+ status, parents = self._stable_updater_parse_major_version(commit,
+ i,
+ parents,
+ current_version,
+ status)
break
- if i == -1 and newer == False:
- status.update({
- 'update': True,
- 'success': True,
- 'message': _(
- u'Click on the button below to update to the latest stable version.'),
- 'history': parents
- })
- self.updateFile = commit[0]['zipball_url']
- elif i == -1 and newer == True:
- status.update({
- 'update': True,
- 'success': True,
- 'message': _(u'A new update is available. Click on the button below to '
- u'update to version: %(version)s', version=commit[0]['tag_name']),
- 'history': parents
- })
- self.updateFile = commit[0]['zipball_url']
+ status = self._stable_updater_set_status(i, newer, status, parents, commit)
return json.dumps(status)
def _get_request_path(self):
diff --git a/cps/uploader.py b/cps/uploader.py
index 82caf308..0d59fd01 100644
--- a/cps/uploader.py
+++ b/cps/uploader.py
@@ -117,8 +117,8 @@ def parse_xmp(pdf_file):
"""
try:
xmp_info = pdf_file.getXmpMetadata()
- except Exception as e:
- log.debug('Can not read XMP metadata %e', e)
+ except Exception as ex:
+ log.debug('Can not read XMP metadata {}'.format(ex))
return None
if xmp_info:
@@ -162,8 +162,8 @@ def parse_xmp(pdf_file):
"""
try:
xmp_info = pdf_file.getXmpMetadata()
- except Exception as e:
- log.debug('Can not read XMP metadata', e)
+ except Exception as ex:
+ log.debug('Can not read XMP metadata {}'.format(ex))
return None
if xmp_info:
diff --git a/cps/usermanagement.py b/cps/usermanagement.py
index cdba4d98..ef7174c4 100644
--- a/cps/usermanagement.py
+++ b/cps/usermanagement.py
@@ -41,7 +41,7 @@ def login_required_if_no_ano(func):
def _fetch_user_by_name(username):
- return ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first()
+ return ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).first()
@lm.user_loader
diff --git a/cps/web.py b/cps/web.py
index 8e4ce297..e1acdcef 100644
--- a/cps/web.py
+++ b/cps/web.py
@@ -24,7 +24,6 @@ from __future__ import division, print_function, unicode_literals
import os
from datetime import datetime
import json
-import re
import mimetypes
import chardet # dependency of requests
@@ -50,9 +49,9 @@ from . import constants, logger, isoLanguages, services
from . import babel, db, ub, config, get_locale, app
from . import calibre_db
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
-from .helper import check_valid_domain, render_task_status, \
+from .helper import check_valid_domain, render_task_status, check_email, check_username, \
get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \
- send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password
+ send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password, valid_email
from .pagination import Pagination
from .redirect import redirect_back
from .usermanagement import login_required_if_no_ano
@@ -215,8 +214,8 @@ def update_view():
for element in to_save:
for param in to_save[element]:
current_user.set_view_property(element, param, to_save[element][param])
- except Exception as e:
- log.error("Could not save view_settings: %r %r: %e", request, to_save, e)
+ except Exception as ex:
+ log.error("Could not save view_settings: %r %r: %e", request, to_save, ex)
return "Invalid request", 400
return "1", 200
@@ -371,7 +370,6 @@ def get_sort_function(sort, data):
def render_books_list(data, sort, book_id, page):
order = get_sort_function(sort, data)
-
if data == "rated":
return render_rated_books(page, book_id, order=order)
elif data == "discover":
@@ -383,7 +381,7 @@ def render_books_list(data, sort, book_id, page):
elif data == "hot":
return render_hot_books(page)
elif data == "download":
- return render_downloaded_books(page, order)
+ return render_downloaded_books(page, order, book_id)
elif data == "author":
return render_author_books(page, book_id, order)
elif data == "publisher":
@@ -463,7 +461,11 @@ def render_hot_books(page):
abort(404)
-def render_downloaded_books(page, order):
+def render_downloaded_books(page, order, user_id):
+ if current_user.role_admin():
+ user_id = int(user_id)
+ else:
+ user_id = current_user.id
if current_user.check_visibility(constants.SIDEBAR_DOWNLOAD):
if current_user.show_detail_random():
random = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \
@@ -474,19 +476,20 @@ def render_downloaded_books(page, order):
entries, __, pagination = calibre_db.fill_indexpage(page,
0,
db.Books,
- ub.Downloads.user_id == int(current_user.id),
+ ub.Downloads.user_id == user_id,
order,
ub.Downloads, db.Books.id == ub.Downloads.book_id)
for book in entries:
if not calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \
.filter(db.Books.id == book.id).first():
ub.delete_download(book.id)
-
+ user = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
return render_title_template('index.html',
random=random,
entries=entries,
pagination=pagination,
- title=_(u"Downloaded books by %(user)s",user=current_user.nickname),
+ id=user_id,
+ title=_(u"Downloaded books by %(user)s",user=user.name),
page="download")
else:
abort(404)
@@ -498,6 +501,7 @@ def render_author_books(page, author_id, order):
db.Books.authors.any(db.Authors.id == author_id),
[order[0], db.Series.name, db.Books.series_index],
db.books_series_link,
+ db.Books.id == db.books_series_link.c.book,
db.Series)
if entries is None or not len(entries):
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
@@ -526,6 +530,7 @@ def render_publisher_books(page, book_id, order):
db.Books.publishers.any(db.Publishers.id == book_id),
[db.Series.name, order[0], db.Books.series_index],
db.books_series_link,
+ db.Books.id == db.books_series_link.c.book,
db.Series)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
title=_(u"Publisher: %(name)s", name=publisher.name), page="publisher")
@@ -579,7 +584,9 @@ def render_category_books(page, book_id, order):
db.Books,
db.Books.tags.any(db.Tags.id == book_id),
[order[0], db.Series.name, db.Books.series_index],
- db.books_series_link, db.Series)
+ db.books_series_link,
+ db.Books.id == db.books_series_link.c.book,
+ db.Series)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
title=_(u"Category: %(name)s", name=name.name), page="category")
else:
@@ -799,8 +806,10 @@ def author_list():
if current_user.check_visibility(constants.SIDEBAR_AUTHOR):
if current_user.get_view_property('author', 'dir') == 'desc':
order = db.Authors.sort.desc()
+ order_no = 0
else:
order = db.Authors.sort.asc()
+ order_no = 1
entries = calibre_db.session.query(db.Authors, func.count('books_authors_link.book').label('count')) \
.join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_authors_link.author')).order_by(order).all()
@@ -810,7 +819,27 @@ def author_list():
for entry in entries:
entry.Authors.name = entry.Authors.name.replace('|', ',')
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
- title=u"Authors", page="authorlist", data='author')
+ title=u"Authors", page="authorlist", data='author', order=order_no)
+ else:
+ abort(404)
+
+@web.route("/downloadlist")
+@login_required_if_no_ano
+def download_list():
+ if current_user.get_view_property('download', 'dir') == 'desc':
+ order = ub.User.name.desc()
+ order_no = 0
+ else:
+ order = ub.User.name.asc()
+ order_no = 1
+ if current_user.check_visibility(constants.SIDEBAR_DOWNLOAD) and current_user.role_admin():
+ entries = ub.session.query(ub.User, func.count(ub.Downloads.book_id).label('count'))\
+ .join(ub.Downloads).group_by(ub.Downloads.user_id).order_by(order).all()
+ charlist = ub.session.query(func.upper(func.substr(ub.User.name, 1, 1)).label('char')) \
+ .filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) \
+ .group_by(func.upper(func.substr(ub.User.name, 1, 1))).all()
+ return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
+ title=_(u"Downloads"), page="downloadlist", data="download", order=order_no)
else:
abort(404)
@@ -820,8 +849,10 @@ def author_list():
def publisher_list():
if current_user.get_view_property('publisher', 'dir') == 'desc':
order = db.Publishers.name.desc()
+ order_no = 0
else:
order = db.Publishers.name.asc()
+ order_no = 1
if current_user.check_visibility(constants.SIDEBAR_PUBLISHER):
entries = calibre_db.session.query(db.Publishers, func.count('books_publishers_link.book').label('count')) \
.join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \
@@ -830,7 +861,7 @@ def publisher_list():
.join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(func.upper(func.substr(db.Publishers.name, 1, 1))).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
- title=_(u"Publishers"), page="publisherlist", data="publisher")
+ title=_(u"Publishers"), page="publisherlist", data="publisher", order=order_no)
else:
abort(404)
@@ -841,8 +872,10 @@ def series_list():
if current_user.check_visibility(constants.SIDEBAR_SERIES):
if current_user.get_view_property('series', 'dir') == 'desc':
order = db.Series.sort.desc()
+ order_no = 0
else:
order = db.Series.sort.asc()
+ order_no = 1
if current_user.get_view_property('series', 'series_view') == 'list':
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()) \
@@ -861,7 +894,8 @@ def series_list():
.group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=charlist,
- title=_(u"Series"), page="serieslist", data="series", bodyClass="grid-view")
+ title=_(u"Series"), page="serieslist", data="series", bodyClass="grid-view",
+ order=order_no)
else:
abort(404)
@@ -872,14 +906,16 @@ def ratings_list():
if current_user.check_visibility(constants.SIDEBAR_RATING):
if current_user.get_view_property('ratings', 'dir') == 'desc':
order = db.Ratings.rating.desc()
+ order_no = 0
else:
order = db.Ratings.rating.asc()
+ order_no = 1
entries = calibre_db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'),
(db.Ratings.rating / 2).label('name')) \
.join(db.books_ratings_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_ratings_link.rating')).order_by(order).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
- title=_(u"Ratings list"), page="ratingslist", data="ratings")
+ title=_(u"Ratings list"), page="ratingslist", data="ratings", order=order_no)
else:
abort(404)
@@ -890,15 +926,17 @@ def formats_list():
if current_user.check_visibility(constants.SIDEBAR_FORMAT):
if current_user.get_view_property('ratings', 'dir') == 'desc':
order = db.Data.format.desc()
+ order_no = 0
else:
order = db.Data.format.asc()
+ order_no = 1
entries = calibre_db.session.query(db.Data,
func.count('data.book').label('count'),
db.Data.format.label('format')) \
.join(db.Books).filter(calibre_db.common_filters()) \
.group_by(db.Data.format).order_by(order).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
- title=_(u"File formats list"), page="formatslist", data="formats")
+ title=_(u"File formats list"), page="formatslist", data="formats", order=order_no)
else:
abort(404)
@@ -938,8 +976,10 @@ def category_list():
if current_user.check_visibility(constants.SIDEBAR_CATEGORY):
if current_user.get_view_property('category', 'dir') == 'desc':
order = db.Tags.name.desc()
+ order_no = 0
else:
order = db.Tags.name.asc()
+ order_no = 1
entries = calibre_db.session.query(db.Tags, func.count('books_tags_link.book').label('count')) \
.join(db.books_tags_link).join(db.Books).order_by(order).filter(calibre_db.common_filters()) \
.group_by(text('books_tags_link.tag')).all()
@@ -947,7 +987,7 @@ def category_list():
.join(db.books_tags_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(func.upper(func.substr(db.Tags.name, 1, 1))).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
- title=_(u"Categories"), page="catlist", data="category")
+ title=_(u"Categories"), page="catlist", data="category", order=order_no)
else:
abort(404)
@@ -1123,14 +1163,6 @@ def extend_search_term(searchterm,
searchterm.extend(tag.name for tag in tag_names)
tag_names = calibre_db.session.query(db_element).filter(db.Tags.id.in_(tags['exclude_' + key])).all()
searchterm.extend(tag.name for tag in tag_names)
- #serie_names = calibre_db.session.query(db.Series).filter(db.Series.id.in_(tags['include_serie'])).all()
- #searchterm.extend(serie.name for serie in serie_names)
- #serie_names = calibre_db.session.query(db.Series).filter(db.Series.id.in_(tags['include_serie'])).all()
- #searchterm.extend(serie.name for serie in serie_names)
- #shelf_names = ub.session.query(ub.Shelf).filter(ub.Shelf.id.in_(tags['include_shelf'])).all()
- #searchterm.extend(shelf.name for shelf in shelf_names)
- #shelf_names = ub.session.query(ub.Shelf).filter(ub.Shelf.id.in_(tags['include_shelf'])).all()
- #searchterm.extend(shelf.name for shelf in shelf_names)
language_names = calibre_db.session.query(db.Languages). \
filter(db.Languages.id.in_(tags['include_language'])).all()
if language_names:
@@ -1304,11 +1336,7 @@ def serve_book(book_id, book_format, anyname):
@login_required_if_no_ano
@download_required
def download_link(book_id, book_format, anyname):
- if "Kobo" in request.headers.get('User-Agent'):
- client = "kobo"
- else:
- client=""
-
+ client = "kobo" if "Kobo" in request.headers.get('User-Agent') else ""
return get_download_link(book_id, book_format, client)
@@ -1320,7 +1348,7 @@ def send_to_kindle(book_id, book_format, convert):
flash(_(u"Please configure the SMTP mail settings first..."), category="error")
elif current_user.kindle_mail:
result = send_mail(book_id, book_format, convert, current_user.kindle_mail, config.config_calibre_dir,
- current_user.nickname)
+ current_user.name)
if result is None:
flash(_(u"Book successfully queued for sending to %(kindlemail)s", kindlemail=current_user.kindle_mail),
category="success")
@@ -1350,52 +1378,41 @@ def register():
if request.method == "POST":
to_save = request.form.to_dict()
- if config.config_register_email:
- nickname = to_save["email"]
- else:
- nickname = to_save.get('nickname', None)
- if not nickname or not to_save.get("email", None):
+ nickname = to_save["email"].strip() if config.config_register_email else to_save.get('name')
+ if not nickname or not to_save.get("email"):
flash(_(u"Please fill out all fields!"), category="error")
return render_title_template('register.html', title=_(u"register"), page="register")
- #if to_save["email"].count("@") != 1 or not \
- # Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
- if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$",
- to_save["email"]):
- flash(_(u"Invalid e-mail address format"), category="error")
- log.warning('Registering failed for user "%s" e-mail address: %s', nickname, to_save["email"])
+ try:
+ nickname = check_username(nickname)
+ email = check_email(to_save["email"])
+ except Exception as ex:
+ flash(str(ex), category="error")
return render_title_template('register.html', title=_(u"register"), page="register")
- existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == nickname
- .lower()).first()
- existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()).first()
- if not existing_user and not existing_email:
- content = ub.User()
- if check_valid_domain(to_save["email"]):
- content.nickname = nickname
- content.email = to_save["email"]
- password = generate_random_password()
- content.password = generate_password_hash(password)
- content.role = config.config_default_role
- content.sidebar_view = config.config_default_show
- try:
- ub.session.add(content)
- ub.session.commit()
- if feature_support['oauth']:
- register_user_with_oauth(content)
- send_registration_mail(to_save["email"], nickname, password)
- except Exception:
- ub.session.rollback()
- flash(_(u"An unknown error occurred. Please try again later."), category="error")
- return render_title_template('register.html', title=_(u"register"), page="register")
- else:
- flash(_(u"Your e-mail is not allowed to register"), category="error")
- log.warning('Registering failed for user "%s" e-mail address: %s', nickname, to_save["email"])
+ content = ub.User()
+ if check_valid_domain(email):
+ content.name = nickname
+ content.email = email
+ password = generate_random_password()
+ content.password = generate_password_hash(password)
+ content.role = config.config_default_role
+ content.sidebar_view = config.config_default_show
+ try:
+ ub.session.add(content)
+ ub.session.commit()
+ if feature_support['oauth']:
+ register_user_with_oauth(content)
+ send_registration_mail(to_save["email"].strip(), nickname, password)
+ except Exception:
+ ub.session.rollback()
+ flash(_(u"An unknown error occurred. Please try again later."), category="error")
return render_title_template('register.html', title=_(u"register"), page="register")
- flash(_(u"Confirmation e-mail was send to your e-mail account."), category="success")
- return redirect(url_for('web.login'))
else:
- flash(_(u"This username or e-mail address is already in use."), category="error")
+ flash(_(u"Your e-mail is not allowed to register"), category="error")
+ log.warning('Registering failed for user "%s" e-mail address: %s', nickname, to_save["email"])
return render_title_template('register.html', title=_(u"register"), page="register")
+ flash(_(u"Confirmation e-mail was send to your e-mail account."), category="success")
+ return redirect(url_for('web.login'))
if feature_support['oauth']:
register_user_with_oauth()
@@ -1414,22 +1431,22 @@ def login():
flash(_(u"Cannot activate LDAP authentication"), category="error")
if request.method == "POST":
form = request.form.to_dict()
- user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == form['username'].strip().lower()) \
+ user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == form['username'].strip().lower()) \
.first()
if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user and form['password'] != "":
login_result, error = services.ldap.bind_user(form['username'], form['password'])
if login_result:
login_user(user, remember=bool(form.get('remember_me')))
- log.debug(u"You are now logged in as: '%s'", user.nickname)
- flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname),
+ log.debug(u"You are now logged in as: '%s'", user.name)
+ flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name),
category="success")
return redirect_back(url_for("web.index"))
elif login_result is None and user and check_password_hash(str(user.password), form['password']) \
- and user.nickname != "Guest":
+ and user.name != "Guest":
login_user(user, remember=bool(form.get('remember_me')))
- log.info("Local Fallback Login as: '%s'", user.nickname)
+ log.info("Local Fallback Login as: '%s'", user.name)
flash(_(u"Fallback Login as: '%(nickname)s', LDAP Server not reachable, or user not known",
- nickname=user.nickname),
+ nickname=user.name),
category="warning")
return redirect_back(url_for("web.index"))
elif login_result is None:
@@ -1442,7 +1459,7 @@ def login():
else:
ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr)
if 'forgot' in form and form['forgot'] == 'forgot':
- if user != None and user.nickname != "Guest":
+ if user != None and user.name != "Guest":
ret, __ = reset_password(user.id)
if ret == 1:
flash(_(u"New Password was send to your email address"), category="info")
@@ -1454,10 +1471,10 @@ def login():
flash(_(u"Please enter valid username to reset password"), category="error")
log.warning('Username missing for password reset IP-address: %s', ipAdress)
else:
- if user and check_password_hash(str(user.password), form['password']) and user.nickname != "Guest":
+ if user and check_password_hash(str(user.password), form['password']) and user.name != "Guest":
login_user(user, remember=bool(form.get('remember_me')))
- log.debug(u"You are now logged in as: '%s'", user.nickname)
- flash(_(u"You are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success")
+ log.debug(u"You are now logged in as: '%s'", user.name)
+ flash(_(u"You are now logged in as: '%(nickname)s'", nickname=user.name), category="success")
config.config_is_initial = False
return redirect_back(url_for("web.index"))
else:
@@ -1486,63 +1503,41 @@ def logout():
return redirect(url_for('web.login'))
-
-
-
# ################################### Users own configuration #########################################################
-def change_profile_email(to_save, kobo_support, local_oauth_check, oauth_status):
- if "email" in to_save and to_save["email"] != current_user.email:
- if config.config_public_reg and not check_valid_domain(to_save["email"]):
- flash(_(u"E-mail is not from valid domain"), category="error")
- return render_title_template("user_edit.html", content=current_user,
- title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
- kobo_support=kobo_support,
- registered_oauth=local_oauth_check, oauth_status=oauth_status)
- current_user.email = to_save["email"]
-
-def change_profile_nickname(to_save, kobo_support, local_oauth_check, translations, languages):
- if "nickname" in to_save and to_save["nickname"] != current_user.nickname:
- # Query User nickname, if not existing, change
- if not ub.session.query(ub.User).filter(ub.User.nickname == to_save["nickname"]).scalar():
- current_user.nickname = to_save["nickname"]
- else:
- flash(_(u"This username is already taken"), category="error")
- return render_title_template("user_edit.html",
- translations=translations,
- languages=languages,
- kobo_support=kobo_support,
- new_user=0, content=current_user,
- registered_oauth=local_oauth_check,
- title=_(u"Edit User %(nick)s",
- nick=current_user.nickname),
- page="edituser")
-
-
def change_profile(kobo_support, local_oauth_check, oauth_status, translations, languages):
to_save = request.form.to_dict()
current_user.random_books = 0
if current_user.role_passwd() or current_user.role_admin():
- if "password" in to_save and to_save["password"]:
+ if to_save.get("password"):
current_user.password = generate_password_hash(to_save["password"])
- if "kindle_mail" in to_save and to_save["kindle_mail"] != current_user.kindle_mail:
- current_user.kindle_mail = to_save["kindle_mail"]
- if "allowed_tags" in to_save and to_save["allowed_tags"] != current_user.allowed_tags:
- current_user.allowed_tags = to_save["allowed_tags"].strip()
- change_profile_email(to_save, kobo_support, local_oauth_check, oauth_status)
- change_profile_nickname(to_save, kobo_support, local_oauth_check, translations, languages)
- if "show_random" in to_save and to_save["show_random"] == "on":
- current_user.random_books = 1
- if "default_language" in to_save:
- current_user.default_language = to_save["default_language"]
- if "locale" in to_save:
- current_user.locale = to_save["locale"]
+ try:
+ if to_save.get("allowed_tags", current_user.allowed_tags) != current_user.allowed_tags:
+ current_user.allowed_tags = to_save["allowed_tags"].strip()
+ if to_save.get("kindle_mail", current_user.kindle_mail) != current_user.kindle_mail:
+ current_user.kindle_mail = valid_email(to_save["kindle_mail"])
+ if to_save.get("email", current_user.email) != current_user.email:
+ current_user.email = check_email(to_save["email"])
+ if to_save.get("name", current_user.name) != current_user.name:
+ # Query User name, if not existing, change
+ current_user.name = check_username(to_save["name"])
+ current_user.random_books = 1 if to_save.get("show_random") == "on" else 0
+ if to_save.get("default_language"):
+ current_user.default_language = to_save["default_language"]
+ if to_save.get("locale"):
+ current_user.locale = to_save["locale"]
+ except Exception as ex:
+ flash(str(ex), category="error")
+ return render_title_template("user_edit.html", content=current_user,
+ title=_(u"%(name)s's profile", name=current_user.name), page="me",
+ kobo_support=kobo_support,
+ registered_oauth=local_oauth_check, oauth_status=oauth_status)
val = 0
for key, __ in to_save.items():
if key.startswith('show'):
val += int(key[5:])
current_user.sidebar_view = val
- if "Show_detail_random" in to_save:
+ if to_save.get("Show_detail_random"):
current_user.sidebar_view += constants.DETAIL_RANDOM
try:
@@ -1580,7 +1575,7 @@ def profile():
languages=languages,
content=current_user,
kobo_support=kobo_support,
- title=_(u"%(name)s's profile", name=current_user.nickname),
+ title=_(u"%(name)s's profile", name=current_user.name),
page="me",
registered_oauth=local_oauth_check,
oauth_status=oauth_status)
diff --git a/messages.pot b/messages.pot
index 78e9e1f1..c8f49da7 100644
--- a/messages.pot
+++ b/messages.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2021-03-27 12:16+0100\n"
+"POT-Creation-Date: 2021-04-06 18:07+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -29,310 +29,339 @@ msgstr ""
msgid "Statistics"
msgstr ""
-#: cps/admin.py:149
+#: cps/admin.py:151
msgid "Server restarted, please reload page"
msgstr ""
-#: cps/admin.py:151
+#: cps/admin.py:153
msgid "Performing shutdown of server, please close window"
msgstr ""
-#: cps/admin.py:159
+#: cps/admin.py:161
msgid "Reconnect successful"
msgstr ""
-#: cps/admin.py:162
+#: cps/admin.py:164
msgid "Unknown command"
msgstr ""
-#: cps/admin.py:172 cps/editbooks.py:662 cps/editbooks.py:674
-#: cps/editbooks.py:777 cps/editbooks.py:779 cps/editbooks.py:806
-#: cps/editbooks.py:822 cps/updater.py:521 cps/uploader.py:94
+#: cps/admin.py:174 cps/editbooks.py:659 cps/editbooks.py:673
+#: cps/editbooks.py:812 cps/editbooks.py:814 cps/editbooks.py:841
+#: cps/editbooks.py:857 cps/updater.py:539 cps/uploader.py:94
#: cps/uploader.py:104
msgid "Unknown"
msgstr ""
-#: cps/admin.py:193
+#: cps/admin.py:195
msgid "Admin page"
msgstr ""
-#: cps/admin.py:215
+#: cps/admin.py:217
msgid "UI Configuration"
msgstr ""
-#: cps/admin.py:247 cps/admin.py:936
+#: cps/admin.py:237 cps/templates/admin.html:46
+msgid "Edit Users"
+msgstr ""
+
+#: cps/admin.py:263
+msgid "all"
+msgstr ""
+
+#: cps/admin.py:298 cps/templates/user_edit.html:44
+#: cps/templates/user_table.html:52
+msgid "Show All"
+msgstr ""
+
+#: cps/admin.py:331 cps/admin.py:1233
+msgid "Guest Name can't be changed"
+msgstr ""
+
+#: cps/admin.py:345 cps/admin.py:1198
+msgid "No admin user remaining, can't remove admin role"
+msgstr ""
+
+#: cps/admin.py:416 cps/admin.py:1101
msgid "Calibre-Web configuration updated"
msgstr ""
-#: cps/admin.py:258
+#: cps/admin.py:427
msgid "Do you really want to delete the Kobo Token?"
msgstr ""
-#: cps/admin.py:260
+#: cps/admin.py:429
msgid "Do you really want to delete this domain?"
msgstr ""
-#: cps/admin.py:262
+#: cps/admin.py:431
msgid "Do you really want to delete this user?"
msgstr ""
-#: cps/admin.py:264 cps/templates/shelf.html:90
+#: cps/admin.py:433 cps/templates/shelf.html:90
msgid "Are you sure you want to delete this shelf?"
msgstr ""
-#: cps/admin.py:510 cps/admin.py:516 cps/admin.py:526 cps/admin.py:536
-#: cps/templates/modal_dialogs.html:29
+#: cps/admin.py:435
+msgid "Are you sure you want to change locales of selected user(s)?"
+msgstr ""
+
+#: cps/admin.py:437
+msgid "Are you sure you want to change visible book languages for selected user(s)?"
+msgstr ""
+
+#: cps/admin.py:439
+msgid "Are you sure you want to change the selected role for the selected user(s)?"
+msgstr ""
+
+#: cps/admin.py:441
+msgid "Are you sure you want to change the selected visibility restrictions for the selected user(s)?"
+msgstr ""
+
+#: cps/admin.py:687 cps/admin.py:693 cps/admin.py:703 cps/admin.py:713
+#: cps/templates/modal_dialogs.html:29 cps/templates/user_table.html:28
msgid "Deny"
msgstr ""
-#: cps/admin.py:512 cps/admin.py:518 cps/admin.py:528 cps/admin.py:538
-#: cps/templates/modal_dialogs.html:28
+#: cps/admin.py:689 cps/admin.py:695 cps/admin.py:705 cps/admin.py:715
+#: cps/templates/modal_dialogs.html:28 cps/templates/user_table.html:33
msgid "Allow"
msgstr ""
-#: cps/admin.py:681
+#: cps/admin.py:857
msgid "client_secrets.json Is Not Configured For Web Application"
msgstr ""
-#: cps/admin.py:723
+#: cps/admin.py:899
msgid "Logfile Location is not Valid, Please Enter Correct Path"
msgstr ""
-#: cps/admin.py:729
+#: cps/admin.py:905
msgid "Access Logfile Location is not Valid, Please Enter Correct Path"
msgstr ""
-#: cps/admin.py:738
+#: cps/admin.py:935
msgid "Please Enter a LDAP Provider, Port, DN and User Object Identifier"
msgstr ""
-#: cps/admin.py:752 cps/admin.py:760
+#: cps/admin.py:950
#, python-format
msgid "LDAP Group Object Filter Needs to Have One \"%s\" Format Identifier"
msgstr ""
-#: cps/admin.py:755 cps/admin.py:763
+#: cps/admin.py:953
msgid "LDAP Group Object Filter Has Unmatched Parenthesis"
msgstr ""
-#: cps/admin.py:768
+#: cps/admin.py:958
#, python-format
msgid "LDAP User Object Filter needs to Have One \"%s\" Format Identifier"
msgstr ""
-#: cps/admin.py:771
+#: cps/admin.py:961
msgid "LDAP User Object Filter Has Unmatched Parenthesis"
msgstr ""
-#: cps/admin.py:779
+#: cps/admin.py:969
#, python-format
msgid "LDAP Member User Filter needs to Have One \"%s\" Format Identifier"
msgstr ""
-#: cps/admin.py:782
+#: cps/admin.py:972
msgid "LDAP Member User Filter Has Unmatched Parenthesis"
msgstr ""
-#: cps/admin.py:790
+#: cps/admin.py:980
msgid "LDAP CACertificate, Certificate or Key Location is not Valid, Please Enter Correct Path"
msgstr ""
-#: cps/admin.py:841
+#: cps/admin.py:1006
msgid "Keyfile Location is not Valid, Please Enter Correct Path"
msgstr ""
-#: cps/admin.py:847
+#: cps/admin.py:1012
msgid "Certfile Location is not Valid, Please Enter Correct Path"
msgstr ""
-#: cps/admin.py:917 cps/admin.py:1024 cps/admin.py:1047 cps/admin.py:1168
+#: cps/admin.py:1082 cps/admin.py:1181 cps/admin.py:1257 cps/admin.py:1321
#: cps/shelf.py:102 cps/shelf.py:159 cps/shelf.py:200 cps/shelf.py:261
#: cps/shelf.py:314 cps/shelf.py:348 cps/shelf.py:418
msgid "Settings DB is not Writeable"
msgstr ""
-#: cps/admin.py:929
+#: cps/admin.py:1094
msgid "DB Location is not Valid, Please Enter Correct Path"
msgstr ""
-#: cps/admin.py:933
+#: cps/admin.py:1098
msgid "DB is not Writeable"
msgstr ""
-#: cps/admin.py:972
+#: cps/admin.py:1137
msgid "Basic Configuration"
msgstr ""
-#: cps/admin.py:987 cps/web.py:1358
+#: cps/admin.py:1153 cps/web.py:1383
msgid "Please fill out all fields!"
msgstr ""
-#: cps/admin.py:990 cps/admin.py:1002 cps/admin.py:1008 cps/admin.py:1138
-msgid "Add new user"
-msgstr ""
-
-#: cps/admin.py:999 cps/web.py:1496
+#: cps/admin.py:1161
msgid "E-mail is not from valid domain"
msgstr ""
-#: cps/admin.py:1006 cps/admin.py:1021
-msgid "Found an existing account for this e-mail address or nickname."
+#: cps/admin.py:1165 cps/admin.py:1275
+msgid "Add new user"
msgstr ""
-#: cps/admin.py:1017
+#: cps/admin.py:1174
#, python-format
msgid "User '%(user)s' created"
msgstr ""
-#: cps/admin.py:1031
+#: cps/admin.py:1178
+msgid "Found an existing account for this e-mail address or name."
+msgstr ""
+
+#: cps/admin.py:1190
#, python-format
msgid "User '%(nick)s' deleted"
msgstr ""
-#: cps/admin.py:1034
+#: cps/admin.py:1193
msgid "No admin user remaining, can't delete user"
msgstr ""
-#: cps/admin.py:1041
-#, python-format
-msgid "User '%(nick)s' updated"
-msgstr ""
-
-#: cps/admin.py:1044
-msgid "An unknown error occured."
-msgstr ""
-
-#: cps/admin.py:1056
-msgid "No admin user remaining, can't remove admin role"
-msgstr ""
-
-#: cps/admin.py:1092 cps/web.py:1554
-msgid "Found an existing account for this e-mail address."
-msgstr ""
-
-#: cps/admin.py:1101 cps/admin.py:1115 cps/admin.py:1209 cps/web.py:1516
+#: cps/admin.py:1247 cps/admin.py:1362
#, python-format
msgid "Edit User %(nick)s"
msgstr ""
-#: cps/admin.py:1107 cps/web.py:1509
-msgid "This username is already taken"
+#: cps/admin.py:1251
+#, python-format
+msgid "User '%(nick)s' updated"
msgstr ""
-#: cps/admin.py:1147 cps/templates/admin.html:78
+#: cps/admin.py:1254
+msgid "An unknown error occured."
+msgstr ""
+
+#: cps/admin.py:1284 cps/templates/admin.html:94
msgid "Edit E-mail Server Settings"
msgstr ""
-#: cps/admin.py:1175
+#: cps/admin.py:1303
+msgid "G-Mail Account Verification Successful"
+msgstr ""
+
+#: cps/admin.py:1328
#, python-format
msgid "Test e-mail queued for sending to %(email)s, please check Tasks for result"
msgstr ""
-#: cps/admin.py:1178
+#: cps/admin.py:1331
#, python-format
msgid "There was an error sending the Test e-mail: %(res)s"
msgstr ""
-#: cps/admin.py:1180
+#: cps/admin.py:1333
msgid "Please configure your e-mail address first..."
msgstr ""
-#: cps/admin.py:1182
+#: cps/admin.py:1335
msgid "E-mail server settings updated"
msgstr ""
-#: cps/admin.py:1193
+#: cps/admin.py:1346
msgid "User not found"
msgstr ""
-#: cps/admin.py:1220
+#: cps/admin.py:1374
#, python-format
msgid "Password for user %(user)s reset"
msgstr ""
-#: cps/admin.py:1223 cps/web.py:1388 cps/web.py:1452
+#: cps/admin.py:1377 cps/web.py:1408 cps/web.py:1469
msgid "An unknown error occurred. Please try again later."
msgstr ""
-#: cps/admin.py:1226 cps/web.py:1320
+#: cps/admin.py:1380 cps/web.py:1348
msgid "Please configure the SMTP mail settings first..."
msgstr ""
-#: cps/admin.py:1237
+#: cps/admin.py:1391
msgid "Logfile viewer"
msgstr ""
-#: cps/admin.py:1303
+#: cps/admin.py:1457
msgid "Requesting update package"
msgstr ""
-#: cps/admin.py:1304
+#: cps/admin.py:1458
msgid "Downloading update package"
msgstr ""
-#: cps/admin.py:1305
+#: cps/admin.py:1459
msgid "Unzipping update package"
msgstr ""
-#: cps/admin.py:1306
+#: cps/admin.py:1460
msgid "Replacing files"
msgstr ""
-#: cps/admin.py:1307
+#: cps/admin.py:1461
msgid "Database connections are closed"
msgstr ""
-#: cps/admin.py:1308
+#: cps/admin.py:1462
msgid "Stopping server"
msgstr ""
-#: cps/admin.py:1309
+#: cps/admin.py:1463
msgid "Update finished, please press okay and reload page"
msgstr ""
-#: cps/admin.py:1310 cps/admin.py:1311 cps/admin.py:1312 cps/admin.py:1313
-#: cps/admin.py:1314
+#: cps/admin.py:1464 cps/admin.py:1465 cps/admin.py:1466 cps/admin.py:1467
+#: cps/admin.py:1468
msgid "Update failed:"
msgstr ""
-#: cps/admin.py:1310 cps/updater.py:337 cps/updater.py:532 cps/updater.py:534
+#: cps/admin.py:1464 cps/updater.py:343 cps/updater.py:550 cps/updater.py:552
msgid "HTTP Error"
msgstr ""
-#: cps/admin.py:1311 cps/updater.py:339 cps/updater.py:536
+#: cps/admin.py:1465 cps/updater.py:345 cps/updater.py:554
msgid "Connection error"
msgstr ""
-#: cps/admin.py:1312 cps/updater.py:341 cps/updater.py:538
+#: cps/admin.py:1466 cps/updater.py:347 cps/updater.py:556
msgid "Timeout while establishing connection"
msgstr ""
-#: cps/admin.py:1313 cps/updater.py:343 cps/updater.py:540
+#: cps/admin.py:1467 cps/updater.py:349 cps/updater.py:558
msgid "General error"
msgstr ""
-#: cps/admin.py:1314
+#: cps/admin.py:1468
msgid "Update File Could Not be Saved in Temp Dir"
msgstr ""
-#: cps/admin.py:1376
+#: cps/admin.py:1529
msgid "Failed to Create at Least One LDAP User"
msgstr ""
-#: cps/admin.py:1389
+#: cps/admin.py:1542
#, python-format
msgid "Error: %(ldaperror)s"
msgstr ""
-#: cps/admin.py:1393
+#: cps/admin.py:1546
msgid "Error: No user returned in response of LDAP server"
msgstr ""
-#: cps/admin.py:1426
+#: cps/admin.py:1579
msgid "At Least One LDAP User Not Found in Database"
msgstr ""
-#: cps/admin.py:1428
+#: cps/admin.py:1581
msgid "{} User Successfully Imported"
msgstr ""
@@ -344,98 +373,98 @@ msgstr ""
msgid "Execution permissions missing"
msgstr ""
-#: cps/editbooks.py:267 cps/editbooks.py:269
+#: cps/editbooks.py:294 cps/editbooks.py:296
msgid "Book Format Successfully Deleted"
msgstr ""
-#: cps/editbooks.py:276 cps/editbooks.py:278
+#: cps/editbooks.py:303 cps/editbooks.py:305
msgid "Book Successfully Deleted"
msgstr ""
-#: cps/editbooks.py:325 cps/editbooks.py:646 cps/web.py:1598 cps/web.py:1634
-#: cps/web.py:1705
+#: cps/editbooks.py:352 cps/editbooks.py:715 cps/web.py:1593 cps/web.py:1629
+#: cps/web.py:1700
msgid "Error opening eBook. File does not exist or file is not accessible"
msgstr ""
-#: cps/editbooks.py:359
+#: cps/editbooks.py:386
msgid "edit metadata"
msgstr ""
-#: cps/editbooks.py:434
+#: cps/editbooks.py:464
#, python-format
msgid "%(langname)s is not a valid language"
msgstr ""
-#: cps/editbooks.py:556 cps/editbooks.py:905
+#: cps/editbooks.py:586 cps/editbooks.py:927
#, python-format
msgid "File extension '%(ext)s' is not allowed to be uploaded to this server"
msgstr ""
-#: cps/editbooks.py:560 cps/editbooks.py:909
+#: cps/editbooks.py:590 cps/editbooks.py:931
msgid "File to be uploaded must have an extension"
msgstr ""
-#: cps/editbooks.py:572
+#: cps/editbooks.py:602
#, python-format
msgid "Failed to create path %(path)s (Permission denied)."
msgstr ""
-#: cps/editbooks.py:577
+#: cps/editbooks.py:607
#, python-format
msgid "Failed to store file %(file)s."
msgstr ""
-#: cps/editbooks.py:595 cps/editbooks.py:972 cps/web.py:1559
+#: cps/editbooks.py:625 cps/editbooks.py:1018 cps/web.py:1554
#, python-format
msgid "Database error: %(error)s."
msgstr ""
-#: cps/editbooks.py:599
+#: cps/editbooks.py:629
#, python-format
msgid "File format %(ext)s added to %(book)s"
msgstr ""
-#: cps/editbooks.py:726
+#: cps/editbooks.py:766
msgid "Identifiers are not Case Sensitive, Overwriting Old Identifier"
msgstr ""
-#: cps/editbooks.py:763
+#: cps/editbooks.py:798
msgid "Metadata successfully updated"
msgstr ""
-#: cps/editbooks.py:772
+#: cps/editbooks.py:807
msgid "Error editing book, please check logfile for details"
msgstr ""
-#: cps/editbooks.py:810
+#: cps/editbooks.py:845
msgid "Uploaded book probably exists in the library, consider to change before upload new: "
msgstr ""
-#: cps/editbooks.py:917
+#: cps/editbooks.py:939
#, python-format
msgid "File %(filename)s could not saved to temp dir"
msgstr ""
-#: cps/editbooks.py:947
+#: cps/editbooks.py:958
#, python-format
msgid "Failed to Move Cover File %(file)s: %(error)s"
msgstr ""
-#: cps/editbooks.py:958
+#: cps/editbooks.py:1004
#, python-format
msgid "File %(file)s uploaded"
msgstr ""
-#: cps/editbooks.py:984
+#: cps/editbooks.py:1030
msgid "Source or destination format for conversion missing"
msgstr ""
-#: cps/editbooks.py:992
+#: cps/editbooks.py:1038
#, python-format
msgid "Book successfully queued for converting to %(book_format)s"
msgstr ""
-#: cps/editbooks.py:996
+#: cps/editbooks.py:1042
#, python-format
msgid "There was an error converting this book: %(res)s"
msgstr ""
@@ -543,55 +572,67 @@ msgstr ""
msgid "Book path %(path)s not found on Google Drive"
msgstr ""
-#: cps/helper.py:576
+#: cps/helper.py:511
+msgid "Found an existing account for this e-mail address"
+msgstr ""
+
+#: cps/helper.py:519
+msgid "This username is already taken"
+msgstr ""
+
+#: cps/helper.py:529
+msgid "Invalid e-mail address format"
+msgstr ""
+
+#: cps/helper.py:602
msgid "Error Downloading Cover"
msgstr ""
-#: cps/helper.py:579
+#: cps/helper.py:605
msgid "Cover Format Error"
msgstr ""
-#: cps/helper.py:589
+#: cps/helper.py:615
msgid "Failed to create path for cover"
msgstr ""
-#: cps/helper.py:605
+#: cps/helper.py:631
msgid "Cover-file is not a valid image file, or could not be stored"
msgstr ""
-#: cps/helper.py:616
+#: cps/helper.py:642
msgid "Only jpg/jpeg/png/webp/bmp files are supported as coverfile"
msgstr ""
-#: cps/helper.py:629
+#: cps/helper.py:655
msgid "Only jpg/jpeg files are supported as coverfile"
msgstr ""
-#: cps/helper.py:680
+#: cps/helper.py:706
msgid "Unrar binary file not found"
msgstr ""
-#: cps/helper.py:694
+#: cps/helper.py:720
msgid "Error excecuting UnRar"
msgstr ""
-#: cps/helper.py:743
+#: cps/helper.py:769
msgid "Waiting"
msgstr ""
-#: cps/helper.py:745
+#: cps/helper.py:771
msgid "Failed"
msgstr ""
-#: cps/helper.py:747
+#: cps/helper.py:773
msgid "Started"
msgstr ""
-#: cps/helper.py:749
+#: cps/helper.py:775
msgid "Finished"
msgstr ""
-#: cps/helper.py:751
+#: cps/helper.py:777
msgid "Unknown Status"
msgstr ""
@@ -603,36 +644,36 @@ msgstr ""
msgid "Kobo Setup"
msgstr ""
-#: cps/oauth_bb.py:76
+#: cps/oauth_bb.py:77
#, python-format
msgid "Register with %(provider)s"
msgstr ""
-#: cps/oauth_bb.py:137 cps/remotelogin.py:133 cps/web.py:1424
+#: cps/oauth_bb.py:138 cps/remotelogin.py:133 cps/web.py:1441
#, python-format
msgid "you are now logged in as: '%(nickname)s'"
msgstr ""
-#: cps/oauth_bb.py:147
+#: cps/oauth_bb.py:148
#, python-format
msgid "Link to %(oauth)s Succeeded"
msgstr ""
-#: cps/oauth_bb.py:153
+#: cps/oauth_bb.py:154
msgid "Login failed, No User Linked With OAuth Account"
msgstr ""
-#: cps/oauth_bb.py:195
+#: cps/oauth_bb.py:196
#, python-format
msgid "Unlink to %(oauth)s Succeeded"
msgstr ""
-#: cps/oauth_bb.py:199
+#: cps/oauth_bb.py:200
#, python-format
msgid "Unlink to %(oauth)s Failed"
msgstr ""
-#: cps/oauth_bb.py:202
+#: cps/oauth_bb.py:203
#, python-format
msgid "Not Linked to %(oauth)s"
msgstr ""
@@ -653,15 +694,24 @@ msgstr ""
msgid "Failed to fetch user info from Google."
msgstr ""
-#: cps/oauth_bb.py:312
+#: cps/oauth_bb.py:325
msgid "GitHub Oauth error, please retry later."
msgstr ""
-#: cps/oauth_bb.py:331
+#: cps/oauth_bb.py:344
msgid "Google Oauth error, please retry later."
msgstr ""
-#: cps/remotelogin.py:65 cps/web.py:1471
+#: cps/opds.py:110 cps/opds.py:199 cps/opds.py:276 cps/opds.py:328
+#: cps/templates/grid.html:14 cps/templates/list.html:14
+msgid "All"
+msgstr ""
+
+#: cps/opds.py:385
+msgid "{} Stars"
+msgstr ""
+
+#: cps/remotelogin.py:65 cps/web.py:1488
msgid "login"
msgstr ""
@@ -677,7 +727,7 @@ msgstr ""
msgid "Success! Please return to your device"
msgstr ""
-#: cps/render_template.py:39 cps/web.py:415
+#: cps/render_template.py:39 cps/web.py:413
msgid "Books"
msgstr ""
@@ -685,7 +735,7 @@ msgstr ""
msgid "Show recent books"
msgstr ""
-#: cps/render_template.py:42 cps/templates/index.xml:18
+#: cps/render_template.py:42 cps/templates/index.xml:25
msgid "Hot Books"
msgstr ""
@@ -693,123 +743,125 @@ msgstr ""
msgid "Show Hot Books"
msgstr ""
-#: cps/render_template.py:45
+#: cps/render_template.py:46 cps/render_template.py:51
msgid "Downloaded Books"
msgstr ""
-#: cps/render_template.py:47
+#: cps/render_template.py:48 cps/render_template.py:53
+#: cps/templates/user_table.html:133
msgid "Show Downloaded Books"
msgstr ""
-#: cps/render_template.py:50 cps/templates/index.xml:25 cps/web.py:425
+#: cps/render_template.py:56 cps/templates/index.xml:32 cps/web.py:423
msgid "Top Rated Books"
msgstr ""
-#: cps/render_template.py:52
+#: cps/render_template.py:58 cps/templates/user_table.html:127
msgid "Show Top Rated Books"
msgstr ""
-#: cps/render_template.py:53 cps/templates/index.xml:47
-#: cps/templates/index.xml:51 cps/web.py:642
+#: cps/render_template.py:59 cps/templates/index.xml:54
+#: cps/templates/index.xml:58 cps/web.py:649
msgid "Read Books"
msgstr ""
-#: cps/render_template.py:55
+#: cps/render_template.py:61
msgid "Show read and unread"
msgstr ""
-#: cps/render_template.py:57 cps/templates/index.xml:54
-#: cps/templates/index.xml:58 cps/web.py:645
+#: cps/render_template.py:63 cps/templates/index.xml:61
+#: cps/templates/index.xml:65 cps/web.py:652
msgid "Unread Books"
msgstr ""
-#: cps/render_template.py:59
+#: cps/render_template.py:65
msgid "Show unread"
msgstr ""
-#: cps/render_template.py:60
+#: cps/render_template.py:66
msgid "Discover"
msgstr ""
-#: cps/render_template.py:62
+#: cps/render_template.py:68 cps/templates/user_table.html:125
+#: cps/templates/user_table.html:128
msgid "Show random books"
msgstr ""
-#: cps/render_template.py:63 cps/templates/book_table.html:50
-#: cps/templates/index.xml:76 cps/web.py:950
+#: cps/render_template.py:69 cps/templates/book_table.html:50
+#: cps/templates/index.xml:83 cps/web.py:990
msgid "Categories"
msgstr ""
-#: cps/render_template.py:65
+#: cps/render_template.py:71 cps/templates/user_table.html:124
msgid "Show category selection"
msgstr ""
-#: cps/render_template.py:66 cps/templates/book_edit.html:84
-#: cps/templates/book_table.html:51 cps/templates/index.xml:83
-#: cps/templates/search_form.html:62 cps/web.py:854 cps/web.py:864
+#: cps/render_template.py:72 cps/templates/book_edit.html:84
+#: cps/templates/book_table.html:51 cps/templates/index.xml:90
+#: cps/templates/search_form.html:62 cps/web.py:887 cps/web.py:897
msgid "Series"
msgstr ""
-#: cps/render_template.py:68
+#: cps/render_template.py:74 cps/templates/user_table.html:123
msgid "Show series selection"
msgstr ""
-#: cps/render_template.py:69 cps/templates/book_table.html:49
-#: cps/templates/index.xml:62
+#: cps/render_template.py:75 cps/templates/book_table.html:49
+#: cps/templates/index.xml:69
msgid "Authors"
msgstr ""
-#: cps/render_template.py:71
+#: cps/render_template.py:77 cps/templates/user_table.html:126
msgid "Show author selection"
msgstr ""
-#: cps/render_template.py:73 cps/templates/book_table.html:55
-#: cps/templates/index.xml:69 cps/web.py:833
+#: cps/render_template.py:79 cps/templates/book_table.html:55
+#: cps/templates/index.xml:76 cps/web.py:864
msgid "Publishers"
msgstr ""
-#: cps/render_template.py:75
+#: cps/render_template.py:81 cps/templates/user_table.html:129
msgid "Show publisher selection"
msgstr ""
-#: cps/render_template.py:76 cps/templates/book_table.html:53
-#: cps/templates/index.xml:90 cps/templates/search_form.html:100 cps/web.py:929
+#: cps/render_template.py:82 cps/templates/book_table.html:53
+#: cps/templates/index.xml:97 cps/templates/search_form.html:100 cps/web.py:967
msgid "Languages"
msgstr ""
-#: cps/render_template.py:79
+#: cps/render_template.py:85 cps/templates/user_table.html:122
msgid "Show language selection"
msgstr ""
-#: cps/render_template.py:80 cps/templates/index.xml:97
+#: cps/render_template.py:86 cps/templates/index.xml:104
msgid "Ratings"
msgstr ""
-#: cps/render_template.py:82
+#: cps/render_template.py:88 cps/templates/user_table.html:130
msgid "Show ratings selection"
msgstr ""
-#: cps/render_template.py:83 cps/templates/index.xml:105
+#: cps/render_template.py:89 cps/templates/index.xml:112
msgid "File formats"
msgstr ""
-#: cps/render_template.py:85
+#: cps/render_template.py:91 cps/templates/user_table.html:131
msgid "Show file formats selection"
msgstr ""
-#: cps/render_template.py:87 cps/web.py:669
+#: cps/render_template.py:93 cps/web.py:676
msgid "Archived Books"
msgstr ""
-#: cps/render_template.py:89
+#: cps/render_template.py:95 cps/templates/user_table.html:132
msgid "Show archived books"
msgstr ""
-#: cps/render_template.py:91 cps/web.py:743
+#: cps/render_template.py:97 cps/web.py:750
msgid "Books List"
msgstr ""
-#: cps/render_template.py:93
+#: cps/render_template.py:99 cps/templates/user_table.html:134
msgid "Show Books List"
msgstr ""
@@ -908,222 +960,226 @@ msgstr ""
msgid "Error opening shelf. Shelf does not exist or is not accessible"
msgstr ""
-#: cps/updater.py:355 cps/updater.py:366 cps/updater.py:418 cps/updater.py:432
+#: cps/updater.py:361 cps/updater.py:372 cps/updater.py:470 cps/updater.py:484
msgid "Unexpected data while reading update information"
msgstr ""
-#: cps/updater.py:362 cps/updater.py:424
+#: cps/updater.py:368 cps/updater.py:476
msgid "No update available. You already have the latest version installed"
msgstr ""
-#: cps/updater.py:379
+#: cps/updater.py:385
msgid "A new update is available. Click on the button below to update to the latest version."
msgstr ""
-#: cps/updater.py:397
+#: cps/updater.py:403
msgid "Could not fetch update information"
msgstr ""
-#: cps/updater.py:411
-msgid "No release information available"
+#: cps/updater.py:412
+msgid "Click on the button below to update to the latest stable version."
msgstr ""
-#: cps/updater.py:468 cps/updater.py:479 cps/updater.py:498
+#: cps/updater.py:421 cps/updater.py:435 cps/updater.py:446
#, python-format
msgid "A new update is available. Click on the button below to update to version: %(version)s"
msgstr ""
-#: cps/updater.py:489
-msgid "Click on the button below to update to the latest stable version."
+#: cps/updater.py:463
+msgid "No release information available"
msgstr ""
-#: cps/templates/index.html:5 cps/web.py:435
+#: cps/templates/index.html:5 cps/web.py:433
msgid "Discover (Random Books)"
msgstr ""
-#: cps/web.py:461
+#: cps/web.py:459
msgid "Hot Books (Most Downloaded)"
msgstr ""
-#: cps/web.py:489
+#: cps/web.py:492
#, python-format
msgid "Downloaded books by %(user)s"
msgstr ""
-#: cps/web.py:503
+#: cps/web.py:507
msgid "Oops! Selected book title is unavailable. File does not exist or is not accessible"
msgstr ""
-#: cps/web.py:517
+#: cps/web.py:521
#, python-format
msgid "Author: %(name)s"
msgstr ""
-#: cps/web.py:531
+#: cps/web.py:536
#, python-format
msgid "Publisher: %(name)s"
msgstr ""
-#: cps/web.py:544
+#: cps/web.py:549
#, python-format
msgid "Series: %(serie)s"
msgstr ""
-#: cps/web.py:557
+#: cps/web.py:562
#, python-format
msgid "Rating: %(rating)s stars"
msgstr ""
-#: cps/web.py:570
+#: cps/web.py:575
#, python-format
msgid "File format: %(format)s"
msgstr ""
-#: cps/web.py:584
+#: cps/web.py:591
#, python-format
msgid "Category: %(name)s"
msgstr ""
-#: cps/web.py:603
+#: cps/web.py:610
#, python-format
msgid "Language: %(name)s"
msgstr ""
-#: cps/web.py:633
+#: cps/web.py:640
#, python-format
msgid "Custom Column No.%(column)d is not existing in calibre database"
msgstr ""
-#: cps/templates/layout.html:56 cps/web.py:703 cps/web.py:1248
+#: cps/templates/layout.html:56 cps/web.py:710 cps/web.py:1280
msgid "Advanced Search"
msgstr ""
#: cps/templates/book_edit.html:214 cps/templates/feed.xml:33
#: cps/templates/index.xml:11 cps/templates/layout.html:45
#: cps/templates/layout.html:48 cps/templates/search_form.html:194
-#: cps/web.py:715 cps/web.py:987
+#: cps/web.py:722 cps/web.py:1027
msgid "Search"
msgstr ""
-#: cps/web.py:882
+#: cps/templates/admin.html:16 cps/web.py:842
+msgid "Downloads"
+msgstr ""
+
+#: cps/web.py:918
msgid "Ratings list"
msgstr ""
-#: cps/web.py:901
+#: cps/web.py:939
msgid "File formats list"
msgstr ""
-#: cps/templates/layout.html:74 cps/templates/tasks.html:7 cps/web.py:964
+#: cps/templates/layout.html:74 cps/templates/tasks.html:7 cps/web.py:1004
msgid "Tasks"
msgstr ""
-#: cps/web.py:1108
+#: cps/web.py:1148
msgid "Published after "
msgstr ""
-#: cps/web.py:1115
+#: cps/web.py:1155
msgid "Published before "
msgstr ""
-#: cps/web.py:1145
+#: cps/web.py:1177
#, python-format
msgid "Rating <= %(rating)s"
msgstr ""
-#: cps/web.py:1147
+#: cps/web.py:1179
#, python-format
msgid "Rating >= %(rating)s"
msgstr ""
-#: cps/web.py:1149
+#: cps/web.py:1181
#, python-format
msgid "Read Status = %(status)s"
msgstr ""
-#: cps/web.py:1325
+#: cps/web.py:1353
#, python-format
msgid "Book successfully queued for sending to %(kindlemail)s"
msgstr ""
-#: cps/web.py:1329
+#: cps/web.py:1357
#, python-format
msgid "Oops! There was an error sending this book: %(res)s"
msgstr ""
-#: cps/web.py:1331
+#: cps/web.py:1359
msgid "Please update your profile with a valid Send to Kindle E-mail Address."
msgstr ""
-#: cps/web.py:1348
+#: cps/web.py:1376
msgid "E-Mail server is not configured, please contact your administrator!"
msgstr ""
-#: cps/web.py:1349 cps/web.py:1359 cps/web.py:1366 cps/web.py:1389
-#: cps/web.py:1393 cps/web.py:1398 cps/web.py:1402
+#: cps/web.py:1377 cps/web.py:1384 cps/web.py:1390 cps/web.py:1409
+#: cps/web.py:1413 cps/web.py:1419
msgid "register"
msgstr ""
-#: cps/web.py:1364
-msgid "Invalid e-mail address format"
-msgstr ""
-
-#: cps/web.py:1391
+#: cps/web.py:1411
msgid "Your e-mail is not allowed to register"
msgstr ""
-#: cps/web.py:1394
+#: cps/web.py:1414
msgid "Confirmation e-mail was send to your e-mail account."
msgstr ""
-#: cps/web.py:1397
-msgid "This username or e-mail address is already in use."
-msgstr ""
-
-#: cps/web.py:1414
+#: cps/web.py:1431
msgid "Cannot activate LDAP authentication"
msgstr ""
-#: cps/web.py:1431
+#: cps/web.py:1448
#, python-format
msgid "Fallback Login as: '%(nickname)s', LDAP Server not reachable, or user not known"
msgstr ""
-#: cps/web.py:1437
+#: cps/web.py:1454
#, python-format
msgid "Could not login: %(message)s"
msgstr ""
-#: cps/web.py:1441 cps/web.py:1465
+#: cps/web.py:1458 cps/web.py:1482
msgid "Wrong Username or Password"
msgstr ""
-#: cps/web.py:1448
+#: cps/web.py:1465
msgid "New Password was send to your email address"
msgstr ""
-#: cps/web.py:1454
+#: cps/web.py:1471
msgid "Please enter valid username to reset password"
msgstr ""
-#: cps/web.py:1460
+#: cps/web.py:1477
#, python-format
msgid "You are now logged in as: '%(nickname)s'"
msgstr ""
-#: cps/web.py:1498 cps/web.py:1583
+#: cps/web.py:1531 cps/web.py:1578
#, python-format
msgid "%(name)s's profile"
msgstr ""
-#: cps/web.py:1550
+#: cps/web.py:1545
msgid "Profile updated"
msgstr ""
-#: cps/web.py:1610 cps/web.py:1613 cps/web.py:1616 cps/web.py:1619
-#: cps/web.py:1626 cps/web.py:1631
+#: cps/web.py:1549
+msgid "Found an existing account for this e-mail address."
+msgstr ""
+
+#: cps/web.py:1605 cps/web.py:1608 cps/web.py:1611 cps/web.py:1614
+#: cps/web.py:1621 cps/web.py:1626
msgid "Read a Book"
msgstr ""
+#: cps/services/gmail.py:41
+msgid "Found no valid gmail.json file with OAuth information"
+msgstr ""
+
#: cps/tasks/convert.py:114
#, python-format
msgid "Calibre ebook-convert %(tool)s not found"
@@ -1162,221 +1218,231 @@ msgstr ""
msgid "Users"
msgstr ""
-#: cps/templates/admin.html:12 cps/templates/login.html:8
+#: cps/templates/admin.html:13 cps/templates/login.html:8
#: cps/templates/login.html:9 cps/templates/register.html:8
-#: cps/templates/user_edit.html:9
+#: cps/templates/user_edit.html:9 cps/templates/user_table.html:104
msgid "Username"
msgstr ""
-#: cps/templates/admin.html:13 cps/templates/register.html:13
-#: cps/templates/user_edit.html:14
+#: cps/templates/admin.html:14 cps/templates/register.html:13
+#: cps/templates/user_edit.html:14 cps/templates/user_table.html:105
msgid "E-mail Address"
msgstr ""
-#: cps/templates/admin.html:14 cps/templates/user_edit.html:27
+#: cps/templates/admin.html:15 cps/templates/user_edit.html:27
msgid "Send to Kindle E-mail Address"
msgstr ""
-#: cps/templates/admin.html:15
-msgid "Downloads"
-msgstr ""
-
-#: cps/templates/admin.html:16 cps/templates/layout.html:77
+#: cps/templates/admin.html:17 cps/templates/layout.html:77
+#: cps/templates/user_table.html:113
msgid "Admin"
msgstr ""
-#: cps/templates/admin.html:17 cps/templates/login.html:12
+#: cps/templates/admin.html:18 cps/templates/login.html:12
#: cps/templates/login.html:13 cps/templates/user_edit.html:22
msgid "Password"
msgstr ""
-#: cps/templates/admin.html:18 cps/templates/layout.html:66
+#: cps/templates/admin.html:19 cps/templates/layout.html:66
+#: cps/templates/user_table.html:114
msgid "Upload"
msgstr ""
-#: cps/templates/admin.html:19 cps/templates/detail.html:18
+#: cps/templates/admin.html:20 cps/templates/detail.html:18
#: cps/templates/detail.html:27 cps/templates/shelf.html:6
+#: cps/templates/user_table.html:115
msgid "Download"
msgstr ""
-#: cps/templates/admin.html:20
+#: cps/templates/admin.html:21
msgid "View Books"
msgstr ""
-#: cps/templates/admin.html:21
+#: cps/templates/admin.html:22 cps/templates/user_table.html:101
+#: cps/templates/user_table.html:116
msgid "Edit"
msgstr ""
-#: cps/templates/admin.html:22 cps/templates/book_edit.html:16
+#: cps/templates/admin.html:23 cps/templates/book_edit.html:16
#: cps/templates/book_table.html:57 cps/templates/modal_dialogs.html:63
#: cps/templates/modal_dialogs.html:116 cps/templates/user_edit.html:66
+#: cps/templates/user_table.html:119
msgid "Delete"
msgstr ""
-#: cps/templates/admin.html:23
+#: cps/templates/admin.html:24
msgid "Public Shelf"
msgstr ""
-#: cps/templates/admin.html:44
+#: cps/templates/admin.html:47
msgid "Add New User"
msgstr ""
-#: cps/templates/admin.html:46
+#: cps/templates/admin.html:49
msgid "Import LDAP Users"
msgstr ""
-#: cps/templates/admin.html:53
+#: cps/templates/admin.html:56
msgid "E-mail Server Settings"
msgstr ""
-#: cps/templates/admin.html:57 cps/templates/email_edit.html:11
+#: cps/templates/admin.html:61 cps/templates/email_edit.html:30
msgid "SMTP Hostname"
msgstr ""
-#: cps/templates/admin.html:61 cps/templates/email_edit.html:15
+#: cps/templates/admin.html:65 cps/templates/email_edit.html:34
msgid "SMTP Port"
msgstr ""
-#: cps/templates/admin.html:65 cps/templates/email_edit.html:19
+#: cps/templates/admin.html:69 cps/templates/email_edit.html:38
msgid "Encryption"
msgstr ""
-#: cps/templates/admin.html:69 cps/templates/email_edit.html:27
+#: cps/templates/admin.html:73 cps/templates/email_edit.html:46
msgid "SMTP Login"
msgstr ""
-#: cps/templates/admin.html:73 cps/templates/email_edit.html:35
+#: cps/templates/admin.html:77 cps/templates/admin.html:88
+#: cps/templates/email_edit.html:54
msgid "From E-mail"
msgstr ""
#: cps/templates/admin.html:84
-msgid "Configuration"
+msgid "E-Mail Service"
msgstr ""
-#: cps/templates/admin.html:87
-msgid "Calibre Database Directory"
-msgstr ""
-
-#: cps/templates/admin.html:91 cps/templates/config_edit.html:136
-msgid "Log Level"
-msgstr ""
-
-#: cps/templates/admin.html:95
-msgid "Port"
+#: cps/templates/admin.html:85
+msgid "Gmail via Oauth2"
msgstr ""
#: cps/templates/admin.html:100
-msgid "External Port"
+msgid "Configuration"
msgstr ""
-#: cps/templates/admin.html:107 cps/templates/config_view_edit.html:27
-msgid "Books per Page"
+#: cps/templates/admin.html:103
+msgid "Calibre Database Directory"
+msgstr ""
+
+#: cps/templates/admin.html:107 cps/templates/config_edit.html:136
+msgid "Log Level"
msgstr ""
#: cps/templates/admin.html:111
-msgid "Uploads"
+msgid "Port"
msgstr ""
-#: cps/templates/admin.html:115
-msgid "Anonymous Browsing"
+#: cps/templates/admin.html:116
+msgid "External Port"
msgstr ""
-#: cps/templates/admin.html:119
-msgid "Public Registration"
-msgstr ""
-
-#: cps/templates/admin.html:123
-msgid "Magic Link Remote Login"
+#: cps/templates/admin.html:123 cps/templates/config_view_edit.html:27
+msgid "Books per Page"
msgstr ""
#: cps/templates/admin.html:127
-msgid "Reverse Proxy Login"
+msgid "Uploads"
msgstr ""
-#: cps/templates/admin.html:132
-msgid "Reverse proxy header name"
+#: cps/templates/admin.html:131
+msgid "Anonymous Browsing"
msgstr ""
-#: cps/templates/admin.html:137
-msgid "Edit Basic Configuration"
+#: cps/templates/admin.html:135
+msgid "Public Registration"
msgstr ""
-#: cps/templates/admin.html:138
-msgid "Edit UI Configuration"
+#: cps/templates/admin.html:139
+msgid "Magic Link Remote Login"
msgstr ""
#: cps/templates/admin.html:143
-msgid "Administration"
-msgstr ""
-
-#: cps/templates/admin.html:144
-msgid "Download Debug Package"
-msgstr ""
-
-#: cps/templates/admin.html:145
-msgid "View Logs"
+msgid "Reverse Proxy Login"
msgstr ""
#: cps/templates/admin.html:148
-msgid "Reconnect Calibre Database"
+msgid "Reverse proxy header name"
msgstr ""
-#: cps/templates/admin.html:149
-msgid "Restart"
+#: cps/templates/admin.html:153
+msgid "Edit Basic Configuration"
msgstr ""
-#: cps/templates/admin.html:150
-msgid "Shutdown"
-msgstr ""
-
-#: cps/templates/admin.html:155
-msgid "Update"
+#: cps/templates/admin.html:154
+msgid "Edit UI Configuration"
msgstr ""
#: cps/templates/admin.html:159
-msgid "Version"
+msgid "Administration"
msgstr ""
#: cps/templates/admin.html:160
-msgid "Details"
+msgid "Download Debug Package"
+msgstr ""
+
+#: cps/templates/admin.html:161
+msgid "View Logs"
+msgstr ""
+
+#: cps/templates/admin.html:164
+msgid "Reconnect Calibre Database"
+msgstr ""
+
+#: cps/templates/admin.html:165
+msgid "Restart"
msgstr ""
#: cps/templates/admin.html:166
+msgid "Shutdown"
+msgstr ""
+
+#: cps/templates/admin.html:171
+msgid "Update"
+msgstr ""
+
+#: cps/templates/admin.html:175
+msgid "Version"
+msgstr ""
+
+#: cps/templates/admin.html:176
+msgid "Details"
+msgstr ""
+
+#: cps/templates/admin.html:182
msgid "Current version"
msgstr ""
-#: cps/templates/admin.html:173
+#: cps/templates/admin.html:189
msgid "Check for Update"
msgstr ""
-#: cps/templates/admin.html:174
+#: cps/templates/admin.html:190
msgid "Perform Update"
msgstr ""
-#: cps/templates/admin.html:187
+#: cps/templates/admin.html:203
msgid "Are you sure you want to restart?"
msgstr ""
-#: cps/templates/admin.html:192 cps/templates/admin.html:206
-#: cps/templates/admin.html:226 cps/templates/shelf.html:95
+#: cps/templates/admin.html:208 cps/templates/admin.html:222
+#: cps/templates/admin.html:242 cps/templates/shelf.html:95
msgid "OK"
msgstr ""
-#: cps/templates/admin.html:193 cps/templates/admin.html:207
+#: cps/templates/admin.html:209 cps/templates/admin.html:223
#: cps/templates/book_edit.html:192 cps/templates/book_table.html:84
#: cps/templates/config_edit.html:427 cps/templates/config_view_edit.html:151
-#: cps/templates/email_edit.html:47 cps/templates/modal_dialogs.html:64
-#: cps/templates/modal_dialogs.html:99 cps/templates/modal_dialogs.html:117
+#: cps/templates/modal_dialogs.html:64 cps/templates/modal_dialogs.html:99
+#: cps/templates/modal_dialogs.html:117 cps/templates/modal_dialogs.html:135
#: cps/templates/shelf.html:96 cps/templates/shelf_edit.html:19
#: cps/templates/user_edit.html:132
msgid "Cancel"
msgstr ""
-#: cps/templates/admin.html:205
+#: cps/templates/admin.html:221
msgid "Are you sure you want to shutdown?"
msgstr ""
-#: cps/templates/admin.html:217
+#: cps/templates/admin.html:233
msgid "Updating, please do not reload this page"
msgstr ""
@@ -1549,7 +1615,7 @@ msgid "Fetch Metadata"
msgstr ""
#: cps/templates/book_edit.html:191 cps/templates/config_edit.html:424
-#: cps/templates/config_view_edit.html:150 cps/templates/email_edit.html:45
+#: cps/templates/config_view_edit.html:150 cps/templates/email_edit.html:64
#: cps/templates/shelf_edit.html:17 cps/templates/shelf_order.html:40
#: cps/templates/user_edit.html:130
msgid "Save"
@@ -1590,6 +1656,8 @@ msgid "No Result(s) found! Please try another keyword."
msgstr ""
#: cps/templates/book_table.html:10 cps/templates/book_table.html:52
+#: cps/templates/user_table.html:13 cps/templates/user_table.html:49
+#: cps/templates/user_table.html:72
msgid "This Field is Required"
msgstr ""
@@ -1597,7 +1665,7 @@ msgstr ""
msgid "Merge selected books"
msgstr ""
-#: cps/templates/book_table.html:24
+#: cps/templates/book_table.html:24 cps/templates/user_table.html:94
msgid "Remove Selections"
msgstr ""
@@ -1863,7 +1931,7 @@ msgid "LDAP Encryption"
msgstr ""
#: cps/templates/config_edit.html:268 cps/templates/config_view_edit.html:61
-#: cps/templates/email_edit.html:21
+#: cps/templates/email_edit.html:40
msgid "None"
msgstr ""
@@ -2076,6 +2144,7 @@ msgid "Default Visibilities for New Users"
msgstr ""
#: cps/templates/config_view_edit.html:142 cps/templates/user_edit.html:82
+#: cps/templates/user_table.html:121
msgid "Show Random Books in Detail View"
msgstr ""
@@ -2149,43 +2218,68 @@ msgstr ""
msgid "Edit Metadata"
msgstr ""
-#: cps/templates/email_edit.html:22
-msgid "STARTTLS"
+#: cps/templates/email_edit.html:12
+msgid "Choose Server Type"
+msgstr ""
+
+#: cps/templates/email_edit.html:14
+msgid "Use Standard E-Mail Account"
+msgstr ""
+
+#: cps/templates/email_edit.html:15
+msgid "G-Mail Account with OAuth2 Verfification"
+msgstr ""
+
+#: cps/templates/email_edit.html:21
+msgid "Setup Gmail Account as E-Mail Server"
msgstr ""
#: cps/templates/email_edit.html:23
+msgid "Revoke G-Mail Access"
+msgstr ""
+
+#: cps/templates/email_edit.html:41
+msgid "STARTTLS"
+msgstr ""
+
+#: cps/templates/email_edit.html:42
msgid "SSL/TLS"
msgstr ""
-#: cps/templates/email_edit.html:31
+#: cps/templates/email_edit.html:50
msgid "SMTP Password"
msgstr ""
-#: cps/templates/email_edit.html:38
+#: cps/templates/email_edit.html:57
msgid "Attachment Size Limit"
msgstr ""
-#: cps/templates/email_edit.html:46
+#: cps/templates/email_edit.html:65
msgid "Save and Send Test E-mail"
msgstr ""
-#: cps/templates/email_edit.html:51
+#: cps/templates/email_edit.html:69 cps/templates/layout.html:29
+#: cps/templates/shelf_order.html:41
+msgid "Back"
+msgstr ""
+
+#: cps/templates/email_edit.html:73
msgid "Allowed Domains (Whitelist)"
msgstr ""
-#: cps/templates/email_edit.html:54 cps/templates/email_edit.html:80
+#: cps/templates/email_edit.html:76 cps/templates/email_edit.html:102
msgid "Add Domain"
msgstr ""
-#: cps/templates/email_edit.html:57 cps/templates/email_edit.html:83
+#: cps/templates/email_edit.html:79 cps/templates/email_edit.html:105
msgid "Add"
msgstr ""
-#: cps/templates/email_edit.html:62 cps/templates/email_edit.html:72
+#: cps/templates/email_edit.html:84 cps/templates/email_edit.html:94
msgid "Enter domainname"
msgstr ""
-#: cps/templates/email_edit.html:68
+#: cps/templates/email_edit.html:90
msgid "Denied Domains (Blacklist)"
msgstr ""
@@ -2197,10 +2291,6 @@ msgstr ""
msgid "Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):"
msgstr ""
-#: cps/templates/grid.html:14 cps/templates/list.html:14
-msgid "All"
-msgstr ""
-
#: cps/templates/http_error.html:38
msgid "Create Issue"
msgstr ""
@@ -2231,64 +2321,72 @@ msgstr ""
msgid "Start"
msgstr ""
+#: cps/templates/index.xml:18
+msgid "Alphabetical Books"
+msgstr ""
+
#: cps/templates/index.xml:22
-msgid "Popular publications from this catalog based on Downloads."
+msgid "Books sorted alphabetically"
msgstr ""
#: cps/templates/index.xml:29
-msgid "Popular publications from this catalog based on Rating."
-msgstr ""
-
-#: cps/templates/index.xml:32
-msgid "Recently added Books"
+msgid "Popular publications from this catalog based on Downloads."
msgstr ""
#: cps/templates/index.xml:36
-msgid "The latest Books"
+msgid "Popular publications from this catalog based on Rating."
msgstr ""
#: cps/templates/index.xml:39
-msgid "Random Books"
+msgid "Recently added Books"
msgstr ""
#: cps/templates/index.xml:43
+msgid "The latest Books"
+msgstr ""
+
+#: cps/templates/index.xml:46
+msgid "Random Books"
+msgstr ""
+
+#: cps/templates/index.xml:50
msgid "Show Random Books"
msgstr ""
-#: cps/templates/index.xml:66
+#: cps/templates/index.xml:73
msgid "Books ordered by Author"
msgstr ""
-#: cps/templates/index.xml:73
+#: cps/templates/index.xml:80
msgid "Books ordered by publisher"
msgstr ""
-#: cps/templates/index.xml:80
+#: cps/templates/index.xml:87
msgid "Books ordered by category"
msgstr ""
-#: cps/templates/index.xml:87
+#: cps/templates/index.xml:94
msgid "Books ordered by series"
msgstr ""
-#: cps/templates/index.xml:94
+#: cps/templates/index.xml:101
msgid "Books ordered by Languages"
msgstr ""
-#: cps/templates/index.xml:101
+#: cps/templates/index.xml:108
msgid "Books ordered by Rating"
msgstr ""
-#: cps/templates/index.xml:109
+#: cps/templates/index.xml:116
msgid "Books ordered by file formats"
msgstr ""
-#: cps/templates/index.xml:112 cps/templates/layout.html:135
+#: cps/templates/index.xml:119 cps/templates/layout.html:135
#: cps/templates/search_form.html:80
msgid "Shelves"
msgstr ""
-#: cps/templates/index.xml:116
+#: cps/templates/index.xml:123
msgid "Books organized in shelves"
msgstr ""
@@ -2296,10 +2394,6 @@ msgstr ""
msgid "Home"
msgstr ""
-#: cps/templates/layout.html:29 cps/templates/shelf_order.html:41
-msgid "Back"
-msgstr ""
-
#: cps/templates/layout.html:35
msgid "Toggle Navigation"
msgstr ""
@@ -2461,6 +2555,10 @@ msgstr ""
msgid "Select"
msgstr ""
+#: cps/templates/modal_dialogs.html:134
+msgid "Ok"
+msgstr ""
+
#: cps/templates/osd.xml:5
msgid "Calibre-Web eBook Catalog"
msgstr ""
@@ -2765,10 +2863,6 @@ msgstr ""
msgid "Language of Books"
msgstr ""
-#: cps/templates/user_edit.html:44
-msgid "Show All"
-msgstr ""
-
#: cps/templates/user_edit.html:53
msgid "OAuth Settings"
msgstr ""
@@ -2793,7 +2887,7 @@ msgstr ""
msgid "Add allowed/Denied Custom Column Values"
msgstr ""
-#: cps/templates/user_edit.html:135
+#: cps/templates/user_edit.html:135 cps/templates/user_table.html:135
msgid "Delete User"
msgstr ""
@@ -2801,3 +2895,79 @@ msgstr ""
msgid "Generate Kobo Auth URL"
msgstr ""
+#: cps/templates/user_table.html:75
+msgid "Select..."
+msgstr ""
+
+#: cps/templates/user_table.html:101
+msgid "Edit User"
+msgstr ""
+
+#: cps/templates/user_table.html:104
+msgid "Enter Username"
+msgstr ""
+
+#: cps/templates/user_table.html:105
+msgid "Enter E-mail Address"
+msgstr ""
+
+#: cps/templates/user_table.html:106
+msgid "Enter Kindle E-mail Address"
+msgstr ""
+
+#: cps/templates/user_table.html:106
+msgid "Kindle E-mail"
+msgstr ""
+
+#: cps/templates/user_table.html:107
+msgid "Locale"
+msgstr ""
+
+#: cps/templates/user_table.html:108
+msgid "Visible Book Languages"
+msgstr ""
+
+#: cps/templates/user_table.html:109
+msgid "Edit Denied Tags"
+msgstr ""
+
+#: cps/templates/user_table.html:109
+msgid "Denied Tags"
+msgstr ""
+
+#: cps/templates/user_table.html:110
+msgid "Edit Allowed Tags"
+msgstr ""
+
+#: cps/templates/user_table.html:110
+msgid "Allowed Tags"
+msgstr ""
+
+#: cps/templates/user_table.html:111
+msgid "Edit Allowed Column Values"
+msgstr ""
+
+#: cps/templates/user_table.html:111
+msgid "Allowed Column Values"
+msgstr ""
+
+#: cps/templates/user_table.html:112
+msgid "Edit Denied Column Values"
+msgstr ""
+
+#: cps/templates/user_table.html:112
+msgid "Denied Columns Values"
+msgstr ""
+
+#: cps/templates/user_table.html:117
+msgid "Change Password"
+msgstr ""
+
+#: cps/templates/user_table.html:118
+msgid "Edit Public Shelfs"
+msgstr ""
+
+#: cps/templates/user_table.html:120
+msgid "View"
+msgstr ""
+
diff --git a/optional-requirements.txt b/optional-requirements.txt
index 3283777b..b6fee806 100644
--- a/optional-requirements.txt
+++ b/optional-requirements.txt
@@ -1,5 +1,4 @@
# GDrive Integration
-google-api-python-client>=1.7.11,<1.13.0
gevent>20.6.0,<21.2.0
greenlet>=0.4.17,<1.1.0
httplib2>=0.9.2,<0.18.0
@@ -12,6 +11,12 @@ PyYAML>=3.12
rsa>=3.4.2,<4.1.0
six>=1.10.0,<1.15.0
+# Gdrive and Gmail integration
+google-api-python-client>=1.7.11,<2.1.0
+
+# Gmail
+google-auth-oauthlib>=0.4.3,<0.5.0
+
# goodreads
goodreads>=0.3.2,<0.4.0
python-Levenshtein>=0.12.0,<0.13.0
diff --git a/requirements.txt b/requirements.txt
index 04aaa000..a90226b2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,7 +9,7 @@ iso-639>=0.4.5,<0.5.0
PyPDF3>=1.0.0,<1.0.4
pytz>=2016.10
requests>=2.11.1,<2.25.0
-SQLAlchemy>=1.3.0,<1.4.0
+SQLAlchemy>=1.3.0,<1.4.0 # oauth fails on 1.4+ due to import problems in flask_dance
tornado>=4.1,<6.2
Wand>=0.4.4,<0.7.0
unidecode>=0.04.19,<1.2.0
diff --git a/test/Calibre-Web TestSummary_Linux.html b/test/Calibre-Web TestSummary_Linux.html
index 6a955da8..2d41ea3a 100644
--- a/test/Calibre-Web TestSummary_Linux.html
+++ b/test/Calibre-Web TestSummary_Linux.html
@@ -37,20 +37,20 @@
-
Start Time: 2021-03-20 19:40:16
+
Start Time: 2021-04-05 18:59:35
-
Stop Time: 2021-03-20 22:11:28
+
Stop Time: 2021-04-05 21:34:25
-
Duration: 2h 2 min
+
Duration: 2h 5 min