Commit 8646f8f2 authored by Ozzieisaacs's avatar Ozzieisaacs

Merge branch 'Develop'

# Conflicts:
#	cps/__init__.py
parents 02aaf17a 99cc69c6
......@@ -33,7 +33,7 @@ from flask_login import LoginManager
from flask_babel import Babel
from flask_principal import Principal
from . import logger, cache_buster, cli, config_sql, ub, db, services
from . import config_sql, logger, cache_buster, cli, ub, db
from .reverseproxy import ReverseProxied
from .server import WebServer
try:
......@@ -65,7 +65,6 @@ lm = LoginManager()
lm.login_view = 'web.login'
lm.anonymous_user = ub.Anonymous
ub.init_db(cli.settingspath)
# pylint: disable=no-member
config = config_sql.load_configuration(ub.session)
......@@ -78,11 +77,12 @@ _BABEL_TRANSLATIONS = set()
log = logger.create()
from . import services
def create_app():
try:
app.wsgi_app = ReverseProxied(ProxyFix(app.wsgi_app, x_for=1, x_host=1))
except ValueError:
except (ValueError, TypeError):
app.wsgi_app = ReverseProxied(ProxyFix(app.wsgi_app))
# For python2 convert path to unicode
if sys.version_info < (3, 0):
......
......@@ -817,6 +817,9 @@ def update_mailsettings():
@admin_required
def edit_user(user_id):
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
if not content:
flash(_(u"User not found"), category="error")
return redirect(url_for('admin.admin'))
downloads = list()
languages = speaking_language()
translations = babel.list_translations() + [LC('en')]
......@@ -933,8 +936,6 @@ def edit_user(user_id):
@login_required
@admin_required
def reset_user_password(user_id):
if not config.config_public_reg:
abort(404)
if current_user is not None and current_user.is_authenticated:
ret, message = reset_password(user_id)
if ret == 1:
......
......@@ -34,7 +34,7 @@ def version_info():
parser = argparse.ArgumentParser(description='Calibre Web is a web app'
' providing a interface for browsing, reading and downloading eBooks\n', prog='cps.py')
' providing a interface for browsing, reading and downloading eBooks\n', prog='cps.py')
parser.add_argument('-p', metavar='path', help='path and name to settings db, e.g. /opt/cw.db')
parser.add_argument('-g', metavar='path', help='path and name to gdrive db, e.g. /opt/gd.db')
parser.add_argument('-c', metavar='path',
......
......@@ -37,8 +37,6 @@ _Base = declarative_base()
class _Settings(_Base):
__tablename__ = 'settings'
# config_is_initial = Column(Boolean, default=True)
id = Column(Integer, primary_key=True)
mail_server = Column(String, default=constants.DEFAULT_MAIL_SERVER)
mail_port = Column(Integer, default=25)
......
......@@ -80,9 +80,10 @@ MATURE_CONTENT = 1 << 11
SIDEBAR_PUBLISHER = 1 << 12
SIDEBAR_RATING = 1 << 13
SIDEBAR_FORMAT = 1 << 14
SIDEBAR_ARCHIVED = 1 << 15
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_EDIT_SHELFS & ~ROLE_ANONYMOUS
ADMIN_USER_SIDEBAR = (SIDEBAR_FORMAT << 1) - 1
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS
ADMIN_USER_SIDEBAR = (SIDEBAR_ARCHIVED << 1) - 1
UPDATE_STABLE = 0 << 0
AUTO_UPDATE_STABLE = 1 << 0
......@@ -112,7 +113,7 @@ del env_CALIBRE_PORT
EXTENSIONS_AUDIO = {'mp3', 'm4a', 'm4b'}
EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt'}
EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx',
'fb2', 'html', 'rtf', 'odt', 'mp3', 'm4a', 'm4b'}
'fb2', 'html', 'rtf', 'lit', 'odt', 'mp3', 'm4a', 'm4b'}
# EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] +
# (['rar','cbr'] if feature_support['rar'] else []))
......
......@@ -25,7 +25,7 @@ import ast
from sqlalchemy import create_engine
from sqlalchemy import Table, Column, ForeignKey
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float, DateTime
from sqlalchemy.orm import relationship, sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base
......
This diff is collapsed.
This diff is collapsed.
......@@ -80,9 +80,13 @@ def formatdate_filter(val):
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:
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e, current_user.locale, current_user.nickname)
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e,
current_user.locale,
current_user.nickname
)
return formatdate
@jinjia.app_template_filter('formatdateinput')
def format_date_input(val):
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val)
......
This diff is collapsed.
......@@ -67,6 +67,8 @@ def get_level_name(level):
def is_valid_logfile(file_path):
if file_path == LOG_TO_STDERR or file_path == LOG_TO_STDOUT:
return True
if not file_path:
return True
if os.path.isdir(file_path):
......@@ -105,7 +107,9 @@ def setup(log_file, log_level=None):
# avoid spamming the log with debug messages from libraries
r.setLevel(log_level)
log_file = _absolute_log_file(log_file, DEFAULT_LOG_FILE)
# Otherwise name get's destroyed on windows
if log_file != LOG_TO_STDERR and log_file != LOG_TO_STDOUT:
log_file = _absolute_log_file(log_file, DEFAULT_LOG_FILE)
previous_handler = r.handlers[0] if r.handlers else None
if previous_handler:
......@@ -119,7 +123,7 @@ def setup(log_file, log_level=None):
file_handler = StreamHandler(sys.stdout)
file_handler.baseFilename = log_file
else:
file_handler = StreamHandler()
file_handler = StreamHandler(sys.stderr)
file_handler.baseFilename = log_file
else:
try:
......
......@@ -30,7 +30,7 @@ except ImportError:
from flask_dance.consumer.storage.sqla import SQLAlchemyStorage as SQLAlchemyBackend
from flask_dance.consumer.storage.sqla import first, _get_real_user
from sqlalchemy.orm.exc import NoResultFound
backend_resultcode = True # prevent storing values with this resultcode
backend_resultcode = True # prevent storing values with this resultcode
except ImportError:
pass
......@@ -97,7 +97,7 @@ try:
def set(self, blueprint, token, user=None, user_id=None):
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
for ref in (user, self.user, blueprint.config.get("user")))
if self.user_required and not u and not uid:
raise ValueError("Cannot set OAuth token without an associated user")
......
This diff is collapsed.
......@@ -43,7 +43,6 @@ from . import logger
log = logger.create()
def _readable_listen_address(address, port):
if ':' in address:
address = "[" + address + "]"
......@@ -84,7 +83,8 @@ class WebServer(object):
if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path):
self.ssl_args = dict(certfile=certfile_path, keyfile=keyfile_path)
else:
log.warning('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl.')
log.warning('The specified paths for the ssl certificate file and/or key file seem to be broken. '
'Ignoring ssl.')
log.warning('Cert path: %s', certfile_path)
log.warning('Key path: %s', keyfile_path)
......
......@@ -42,10 +42,18 @@ def to_epoch_timestamp(datetime_object):
return (datetime_object - datetime(1970, 1, 1)).total_seconds()
class SyncToken():
def get_datetime_from_json(json_object, field_name):
try:
return datetime.utcfromtimestamp(json_object[field_name])
except KeyError:
return datetime.min
class SyncToken:
""" The SyncToken is used to persist state accross requests.
When serialized over the response headers, the Kobo device will propagate the token onto following requests to the service.
As an example use-case, the SyncToken is used to detect books that have been added to the library since the last time the device synced to the server.
When serialized over the response headers, the Kobo device will propagate the token onto following
requests to the service. As an example use-case, the SyncToken is used to detect books that have been added
to the library since the last time the device synced to the server.
Attributes:
books_last_created: Datetime representing the newest book that the device knows about.
......@@ -53,21 +61,26 @@ class SyncToken():
"""
SYNC_TOKEN_HEADER = "x-kobo-synctoken"
VERSION = "1-0-0"
VERSION = "1-1-0"
LAST_MODIFIED_ADDED_VERSION = "1-1-0"
MIN_VERSION = "1-0-0"
token_schema = {
"type": "object",
"properties": {"version": {"type": "string"}, "data": {"type": "object"},},
"properties": {"version": {"type": "string"}, "data": {"type": "object"}, },
}
# This Schema doesn't contain enough information to detect and propagate book deletions from Calibre to the device.
# A potential solution might be to keep a list of all known book uuids in the token, and look for any missing from the db.
# A potential solution might be to keep a list of all known book uuids in the token, and look for any missing
# from the db.
data_schema_v1 = {
"type": "object",
"properties": {
"raw_kobo_store_token": {"type": "string"},
"books_last_modified": {"type": "string"},
"books_last_created": {"type": "string"},
"archive_last_modified": {"type": "string"},
"reading_state_last_modified": {"type": "string"},
"tags_last_modified": {"type": "string"},
},
}
......@@ -76,10 +89,16 @@ class SyncToken():
raw_kobo_store_token="",
books_last_created=datetime.min,
books_last_modified=datetime.min,
archive_last_modified=datetime.min,
reading_state_last_modified=datetime.min,
tags_last_modified=datetime.min,
):
self.raw_kobo_store_token = raw_kobo_store_token
self.books_last_created = books_last_created
self.books_last_modified = books_last_modified
self.archive_last_modified = archive_last_modified
self.reading_state_last_modified = reading_state_last_modified
self.tags_last_modified = tags_last_modified
@staticmethod
def from_headers(headers):
......@@ -109,12 +128,11 @@ class SyncToken():
raw_kobo_store_token = data_json["raw_kobo_store_token"]
try:
books_last_modified = datetime.utcfromtimestamp(
data_json["books_last_modified"]
)
books_last_created = datetime.utcfromtimestamp(
data_json["books_last_created"]
)
books_last_modified = get_datetime_from_json(data_json, "books_last_modified")
books_last_created = get_datetime_from_json(data_json, "books_last_created")
archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified")
reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
except TypeError:
log.error("SyncToken timestamps don't parse to a datetime.")
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
......@@ -123,6 +141,9 @@ class SyncToken():
raw_kobo_store_token=raw_kobo_store_token,
books_last_created=books_last_created,
books_last_modified=books_last_modified,
archive_last_modified=archive_last_modified,
reading_state_last_modified=reading_state_last_modified,
tags_last_modified=tags_last_modified
)
def set_kobo_store_header(self, store_headers):
......@@ -143,6 +164,9 @@ class SyncToken():
"raw_kobo_store_token": self.raw_kobo_store_token,
"books_last_modified": to_epoch_timestamp(self.books_last_modified),
"books_last_created": to_epoch_timestamp(self.books_last_created),
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified),
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified)
},
}
return b64encode_json(token)
This diff is collapsed.
......@@ -216,6 +216,8 @@ if ( $( 'body.book' ).length > 0 ) {
.prependTo( '[aria-label^="Download, send"]' );
$( '#have_read_cb' )
.after( '<label class="block-label readLbl" for="#have_read_cb"></label>' );
$( '#archived_cb' )
.after( '<label class="block-label readLbl" for="#archived_cb"></label>' );
$( '#shelf-actions' ).prependTo( '[aria-label^="Download, send"]' );
......@@ -586,6 +588,20 @@ $( '#have_read_cb:checked' ).attr({
'data-viewport': '.btn-toolbar' })
.addClass('readunread-btn-tooltip');
$( '#archived_cb' ).attr({
'data-toggle': 'tooltip',
'title': $( '#archived_cb').attr('data-unchecked'),
'data-placement': 'bottom',
'data-viewport': '.btn-toolbar' })
.addClass('readunread-btn-tooltip');
$( '#archived_cb:checked' ).attr({
'data-toggle': 'tooltip',
'title': $( '#archived_cb').attr('data-checked'),
'data-placement': 'bottom',
'data-viewport': '.btn-toolbar' })
.addClass('readunread-btn-tooltip');
$( 'button#delete' ).attr({
'data-toggle-two': 'tooltip',
'title': $( 'button#delete' ).text(), //'Delete'
......@@ -601,6 +617,14 @@ $( '#have_read_cb' ).click(function() {
}
});
$( '#archived_cb' ).click(function() {
if ( $( '#archived_cb:checked' ).length > 0 ) {
$( this ).attr('data-original-title', $('#archived_cb').attr('data-checked'));
} else {
$( this).attr('data-original-title', $('#archived_cb').attr('data-unchecked'));
}
});
$( '.btn-group[aria-label="Edit/Delete book"] a' ).attr({
'data-toggle': 'tooltip',
'title': $( '#edit_book' ).text(), // 'Edit'
......
......@@ -25,6 +25,14 @@ $("#have_read_cb").on("change", function() {
$(this).closest("form").submit();
});
$(function() {
$("#archived_form").ajaxForm();
});
$("#archived_cb").on("change", function() {
$(this).closest("form").submit();
});
(function() {
var templates = {
add: _.template(
......
......@@ -45,10 +45,10 @@ def process_open(command, quotes=(), env=None, sout=subprocess.PIPE, serr=subpro
def process_wait(command, serr=subprocess.PIPE):
'''Run command, wait for process to terminate, and return an iterator over lines of its output.'''
# Run command, wait for process to terminate, and return an iterator over lines of its output.
p = process_open(command, serr=serr)
p.wait()
for l in p.stdout.readlines():
if isinstance(l, bytes):
l = l.decode('utf-8')
yield l
for line in p.stdout.readlines():
if isinstance(line, bytes):
line = line.decode('utf-8')
yield line
......@@ -61,6 +61,21 @@
<label for="description">{{_('Description')}}</label>
<textarea class="form-control" name="description" id="description" rows="7">{% if book.comments %}{{book.comments[0].text}}{%endif%}</textarea>
</div>
<div class="form-group">
<label>{{_('Identifiers')}}</label>
<table class="table" id="identifier-table">
{% for identifier in book.identifiers %}
<tr>
<td><input type="text" class="form-control" name="identifier-type-{{identifier.type}}" value="{{identifier.type}}" required="required" placeholder="{{_('Identifier Type')}}"></td>
<td><input type="text" class="form-control" name="identifier-val-{{identifier.type}}" value="{{identifier.val}}" required="required" placeholder="{{_('Identifier Value')}}"></td>
<td><a class="btn btn-default" onclick="removeIdentifierLine(this)">{{_('Remove')}}</a></td>
</tr>
{% endfor %}
</table>
<a id="add-identifier-line" class="btn btn-default">{{_('Add Identifier')}}</a>
</div>
<div class="form-group">
<label for="tags">{{_('Tags')}}</label>
<input type="text" class="form-control typeahead" name="tags" id="tags" value="{% for tag in book.tags %}{{tag.name.strip()}}{% if not loop.last %}, {% endif %}{% endfor %}">
......@@ -169,7 +184,7 @@
</div>
<a href="#" id="get_meta" class="btn btn-default" data-toggle="modal" data-target="#metaModal">{{_('Fetch Metadata')}}</a>
<button type="submit" id="submit" class="btn btn-default">{{_('Save')}}</button>
<a href="{{ url_for('web.show_book', book_id=book.id) }}" class="btn btn-default">{{_('Cancel')}}</a>
<a href="{{ url_for('web.show_book', book_id=book.id) }}" id="edit_cancel" class="btn btn-default">{{_('Cancel')}}</a>
</div>
</form>
......@@ -185,12 +200,20 @@
<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) }}" class="btn btn-danger">{{_('Delete')}}</a>
<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>
......@@ -277,6 +300,21 @@
'source': {{_('Source')|safe|tojson}},
};
var language = '{{ g.user.locale }}';
$("#add-identifier-line").click(function() {
// create a random identifier type to have a valid name in form. This will not be used when dealing with the form
var rand_id = Math.floor(Math.random() * 1000000).toString();
var line = '<tr>';
line += '<td><input type="text" class="form-control" name="identifier-type-'+ rand_id +'" required="required" placeholder="{{_('Identifier Type')}}"></td>';
line += '<td><input type="text" class="form-control" name="identifier-val-'+ rand_id +'" required="required" placeholder="{{_('Identifier Value')}}"></td>';
line += '<td><a class="btn btn-default" onclick="removeIdentifierLine(this)">{{_('Remove')}}</a></td>';
line += '</tr>';
$("#identifier-table").append(line);
});
function removeIdentifierLine(el) {
$(el).parent().parent().remove();
}
</script>
<script src="{{ url_for('static', filename='js/libs/typeahead.bundle.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-rating-input.min.js') }}"></script>
......
......@@ -202,6 +202,14 @@
</label>
</form>
</p>
<p>
<form id="archived_form" action="{{ url_for('web.toggle_archived', book_id=entry.id)}}" method="POST">
<label class="block-label">
<input id="archived_cb" data-checked="{{_('Restore from archive')}}" data-unchecked="{{_('Add to archive')}}" type="checkbox" {% if is_archived %}checked{% endif %} >
<span>{{_('Archived')}}</span>
</label>
</form>
</p>
</div>
{% endif %}
......
......@@ -4,7 +4,7 @@
<div class="filterheader hidden-xs hidden-sm">
{% if entries.__len__() %}
{% if entries[0][0].sort %}
{% if data == 'author' %}
<button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button>
{% endif %}
{% endif %}
......
This diff is collapsed.
......@@ -69,7 +69,7 @@ class Updater(threading.Thread):
def get_available_updates(self, request_method, locale):
if config.config_updatechannel == constants.UPDATE_STABLE:
return self._stable_available_updates(request_method)
return self._nightly_available_updates(request_method,locale)
return self._nightly_available_updates(request_method, locale)
def do_work(self):
try:
......@@ -132,7 +132,7 @@ class Updater(threading.Thread):
def pause(self):
self.can_run.clear()
#should just resume the thread
# should just resume the thread
def resume(self):
self.can_run.set()
......@@ -268,7 +268,7 @@ class Updater(threading.Thread):
def is_venv(self):
if (hasattr(sys, 'real_prefix')) or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
return os.sep + os.path.relpath(sys.prefix,constants.BASE_DIR)
return os.sep + os.path.relpath(sys.prefix, constants.BASE_DIR)
else:
return False
......@@ -280,7 +280,7 @@ class Updater(threading.Thread):
@classmethod
def _stable_version_info(cls):
return constants.STABLE_VERSION # Current version
return constants.STABLE_VERSION # Current version
def _nightly_available_updates(self, request_method, locale):
tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
......@@ -436,7 +436,7 @@ class Updater(threading.Thread):
patch_version_update > current_version[2]) or \
minor_version_update > current_version[1]:
parents.append([commit[i]['tag_name'], commit[i]['body'].replace('\r\n', '<p>')])
newer=True
newer = True
i -= 1
continue
if major_version_update < current_version[0]:
......
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment