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 ...@@ -33,7 +33,7 @@ from flask_login import LoginManager
from flask_babel import Babel from flask_babel import Babel
from flask_principal import Principal 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 .reverseproxy import ReverseProxied
from .server import WebServer from .server import WebServer
try: try:
...@@ -65,7 +65,6 @@ lm = LoginManager() ...@@ -65,7 +65,6 @@ lm = LoginManager()
lm.login_view = 'web.login' lm.login_view = 'web.login'
lm.anonymous_user = ub.Anonymous lm.anonymous_user = ub.Anonymous
ub.init_db(cli.settingspath) ub.init_db(cli.settingspath)
# pylint: disable=no-member # pylint: disable=no-member
config = config_sql.load_configuration(ub.session) config = config_sql.load_configuration(ub.session)
...@@ -78,11 +77,12 @@ _BABEL_TRANSLATIONS = set() ...@@ -78,11 +77,12 @@ _BABEL_TRANSLATIONS = set()
log = logger.create() log = logger.create()
from . import services
def create_app(): def create_app():
try: try:
app.wsgi_app = ReverseProxied(ProxyFix(app.wsgi_app, x_for=1, x_host=1)) 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)) app.wsgi_app = ReverseProxied(ProxyFix(app.wsgi_app))
# For python2 convert path to unicode # For python2 convert path to unicode
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
......
...@@ -817,6 +817,9 @@ def update_mailsettings(): ...@@ -817,6 +817,9 @@ def update_mailsettings():
@admin_required @admin_required
def edit_user(user_id): def edit_user(user_id):
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User 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() downloads = list()
languages = speaking_language() languages = speaking_language()
translations = babel.list_translations() + [LC('en')] translations = babel.list_translations() + [LC('en')]
...@@ -933,8 +936,6 @@ def edit_user(user_id): ...@@ -933,8 +936,6 @@ def edit_user(user_id):
@login_required @login_required
@admin_required @admin_required
def reset_user_password(user_id): 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: if current_user is not None and current_user.is_authenticated:
ret, message = reset_password(user_id) ret, message = reset_password(user_id)
if ret == 1: if ret == 1:
......
...@@ -34,7 +34,7 @@ def version_info(): ...@@ -34,7 +34,7 @@ def version_info():
parser = argparse.ArgumentParser(description='Calibre Web is a web app' 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('-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('-g', metavar='path', help='path and name to gdrive db, e.g. /opt/gd.db')
parser.add_argument('-c', metavar='path', parser.add_argument('-c', metavar='path',
......
...@@ -37,8 +37,6 @@ _Base = declarative_base() ...@@ -37,8 +37,6 @@ _Base = declarative_base()
class _Settings(_Base): class _Settings(_Base):
__tablename__ = 'settings' __tablename__ = 'settings'
# config_is_initial = Column(Boolean, default=True)
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
mail_server = Column(String, default=constants.DEFAULT_MAIL_SERVER) mail_server = Column(String, default=constants.DEFAULT_MAIL_SERVER)
mail_port = Column(Integer, default=25) mail_port = Column(Integer, default=25)
......
...@@ -80,9 +80,10 @@ MATURE_CONTENT = 1 << 11 ...@@ -80,9 +80,10 @@ MATURE_CONTENT = 1 << 11
SIDEBAR_PUBLISHER = 1 << 12 SIDEBAR_PUBLISHER = 1 << 12
SIDEBAR_RATING = 1 << 13 SIDEBAR_RATING = 1 << 13
SIDEBAR_FORMAT = 1 << 14 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_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS
ADMIN_USER_SIDEBAR = (SIDEBAR_FORMAT << 1) - 1 ADMIN_USER_SIDEBAR = (SIDEBAR_ARCHIVED << 1) - 1
UPDATE_STABLE = 0 << 0 UPDATE_STABLE = 0 << 0
AUTO_UPDATE_STABLE = 1 << 0 AUTO_UPDATE_STABLE = 1 << 0
...@@ -112,7 +113,7 @@ del env_CALIBRE_PORT ...@@ -112,7 +113,7 @@ del env_CALIBRE_PORT
EXTENSIONS_AUDIO = {'mp3', 'm4a', 'm4b'} EXTENSIONS_AUDIO = {'mp3', 'm4a', 'm4b'}
EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt'} 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', 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'] + # EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] +
# (['rar','cbr'] if feature_support['rar'] else [])) # (['rar','cbr'] if feature_support['rar'] else []))
......
...@@ -25,7 +25,7 @@ import ast ...@@ -25,7 +25,7 @@ import ast
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy import Table, Column, ForeignKey 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.orm import relationship, sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
......
This diff is collapsed.
This diff is collapsed.
...@@ -80,9 +80,13 @@ def formatdate_filter(val): ...@@ -80,9 +80,13 @@ def formatdate_filter(val):
formatdate = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S") formatdate = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S")
return format_date(formatdate, format='medium', locale=get_locale()) return format_date(formatdate, format='medium', locale=get_locale())
except AttributeError as e: except AttributeError as e:
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e, 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 return formatdate
@jinjia.app_template_filter('formatdateinput') @jinjia.app_template_filter('formatdateinput')
def format_date_input(val): def format_date_input(val):
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val) 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): ...@@ -67,6 +67,8 @@ def get_level_name(level):
def is_valid_logfile(file_path): def is_valid_logfile(file_path):
if file_path == LOG_TO_STDERR or file_path == LOG_TO_STDOUT:
return True
if not file_path: if not file_path:
return True return True
if os.path.isdir(file_path): if os.path.isdir(file_path):
...@@ -105,7 +107,9 @@ def setup(log_file, log_level=None): ...@@ -105,7 +107,9 @@ def setup(log_file, log_level=None):
# avoid spamming the log with debug messages from libraries # avoid spamming the log with debug messages from libraries
r.setLevel(log_level) 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 previous_handler = r.handlers[0] if r.handlers else None
if previous_handler: if previous_handler:
...@@ -119,7 +123,7 @@ def setup(log_file, log_level=None): ...@@ -119,7 +123,7 @@ def setup(log_file, log_level=None):
file_handler = StreamHandler(sys.stdout) file_handler = StreamHandler(sys.stdout)
file_handler.baseFilename = log_file file_handler.baseFilename = log_file
else: else:
file_handler = StreamHandler() file_handler = StreamHandler(sys.stderr)
file_handler.baseFilename = log_file file_handler.baseFilename = log_file
else: else:
try: try:
......
...@@ -30,7 +30,7 @@ except ImportError: ...@@ -30,7 +30,7 @@ except ImportError:
from flask_dance.consumer.storage.sqla import SQLAlchemyStorage as SQLAlchemyBackend from flask_dance.consumer.storage.sqla import SQLAlchemyStorage as SQLAlchemyBackend
from flask_dance.consumer.storage.sqla import first, _get_real_user from flask_dance.consumer.storage.sqla import first, _get_real_user
from sqlalchemy.orm.exc import NoResultFound 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: except ImportError:
pass pass
...@@ -97,7 +97,7 @@ try: ...@@ -97,7 +97,7 @@ try:
def set(self, blueprint, token, user=None, user_id=None): def set(self, blueprint, token, user=None, user_id=None):
uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user) 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: if self.user_required and not u and not uid:
raise ValueError("Cannot set OAuth token without an associated user") raise ValueError("Cannot set OAuth token without an associated user")
......
This diff is collapsed.
...@@ -43,7 +43,6 @@ from . import logger ...@@ -43,7 +43,6 @@ from . import logger
log = logger.create() log = logger.create()
def _readable_listen_address(address, port): def _readable_listen_address(address, port):
if ':' in address: if ':' in address:
address = "[" + address + "]" address = "[" + address + "]"
...@@ -84,7 +83,8 @@ class WebServer(object): ...@@ -84,7 +83,8 @@ class WebServer(object):
if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path): if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path):
self.ssl_args = dict(certfile=certfile_path, keyfile=keyfile_path) self.ssl_args = dict(certfile=certfile_path, keyfile=keyfile_path)
else: 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('Cert path: %s', certfile_path)
log.warning('Key path: %s', keyfile_path) log.warning('Key path: %s', keyfile_path)
......
...@@ -42,10 +42,18 @@ def to_epoch_timestamp(datetime_object): ...@@ -42,10 +42,18 @@ def to_epoch_timestamp(datetime_object):
return (datetime_object - datetime(1970, 1, 1)).total_seconds() 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. """ 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. When serialized over the response headers, the Kobo device will propagate the token onto following
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. 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: Attributes:
books_last_created: Datetime representing the newest book that the device knows about. books_last_created: Datetime representing the newest book that the device knows about.
...@@ -53,21 +61,26 @@ class SyncToken(): ...@@ -53,21 +61,26 @@ class SyncToken():
""" """
SYNC_TOKEN_HEADER = "x-kobo-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" MIN_VERSION = "1-0-0"
token_schema = { token_schema = {
"type": "object", "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. # 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 = { data_schema_v1 = {
"type": "object", "type": "object",
"properties": { "properties": {
"raw_kobo_store_token": {"type": "string"}, "raw_kobo_store_token": {"type": "string"},
"books_last_modified": {"type": "string"}, "books_last_modified": {"type": "string"},
"books_last_created": {"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(): ...@@ -76,10 +89,16 @@ class SyncToken():
raw_kobo_store_token="", raw_kobo_store_token="",
books_last_created=datetime.min, books_last_created=datetime.min,
books_last_modified=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.raw_kobo_store_token = raw_kobo_store_token
self.books_last_created = books_last_created self.books_last_created = books_last_created
self.books_last_modified = books_last_modified 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 @staticmethod
def from_headers(headers): def from_headers(headers):
...@@ -109,12 +128,11 @@ class SyncToken(): ...@@ -109,12 +128,11 @@ class SyncToken():
raw_kobo_store_token = data_json["raw_kobo_store_token"] raw_kobo_store_token = data_json["raw_kobo_store_token"]
try: try:
books_last_modified = datetime.utcfromtimestamp( books_last_modified = get_datetime_from_json(data_json, "books_last_modified")
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")
books_last_created = datetime.utcfromtimestamp( reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
data_json["books_last_created"] tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
)
except TypeError: except TypeError:
log.error("SyncToken timestamps don't parse to a datetime.") log.error("SyncToken timestamps don't parse to a datetime.")
return SyncToken(raw_kobo_store_token=raw_kobo_store_token) return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
...@@ -123,6 +141,9 @@ class SyncToken(): ...@@ -123,6 +141,9 @@ class SyncToken():
raw_kobo_store_token=raw_kobo_store_token, raw_kobo_store_token=raw_kobo_store_token,
books_last_created=books_last_created, books_last_created=books_last_created,
books_last_modified=books_last_modified, 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): def set_kobo_store_header(self, store_headers):
...@@ -143,6 +164,9 @@ class SyncToken(): ...@@ -143,6 +164,9 @@ class SyncToken():
"raw_kobo_store_token": self.raw_kobo_store_token, "raw_kobo_store_token": self.raw_kobo_store_token,
"books_last_modified": to_epoch_timestamp(self.books_last_modified), "books_last_modified": to_epoch_timestamp(self.books_last_modified),
"books_last_created": to_epoch_timestamp(self.books_last_created), "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) return b64encode_json(token)
This diff is collapsed.
...@@ -216,6 +216,8 @@ if ( $( 'body.book' ).length > 0 ) { ...@@ -216,6 +216,8 @@ if ( $( 'body.book' ).length > 0 ) {
.prependTo( '[aria-label^="Download, send"]' ); .prependTo( '[aria-label^="Download, send"]' );
$( '#have_read_cb' ) $( '#have_read_cb' )
.after( '<label class="block-label readLbl" for="#have_read_cb"></label>' ); .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"]' ); $( '#shelf-actions' ).prependTo( '[aria-label^="Download, send"]' );
...@@ -586,6 +588,20 @@ $( '#have_read_cb:checked' ).attr({ ...@@ -586,6 +588,20 @@ $( '#have_read_cb:checked' ).attr({
'data-viewport': '.btn-toolbar' }) 'data-viewport': '.btn-toolbar' })
.addClass('readunread-btn-tooltip'); .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({ $( 'button#delete' ).attr({
'data-toggle-two': 'tooltip', 'data-toggle-two': 'tooltip',
'title': $( 'button#delete' ).text(), //'Delete' 'title': $( 'button#delete' ).text(), //'Delete'
...@@ -601,6 +617,14 @@ $( '#have_read_cb' ).click(function() { ...@@ -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({ $( '.btn-group[aria-label="Edit/Delete book"] a' ).attr({
'data-toggle': 'tooltip', 'data-toggle': 'tooltip',
'title': $( '#edit_book' ).text(), // 'Edit' 'title': $( '#edit_book' ).text(), // 'Edit'
......
...@@ -25,6 +25,14 @@ $("#have_read_cb").on("change", function() { ...@@ -25,6 +25,14 @@ $("#have_read_cb").on("change", function() {
$(this).closest("form").submit(); $(this).closest("form").submit();
}); });
$(function() {
$("#archived_form").ajaxForm();
});
$("#archived_cb").on("change", function() {
$(this).closest("form").submit();
});
(function() { (function() {
var templates = { var templates = {
add: _.template( add: _.template(
......
...@@ -45,10 +45,10 @@ def process_open(command, quotes=(), env=None, sout=subprocess.PIPE, serr=subpro ...@@ -45,10 +45,10 @@ def process_open(command, quotes=(), env=None, sout=subprocess.PIPE, serr=subpro
def process_wait(command, serr=subprocess.PIPE): 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 = process_open(command, serr=serr)
p.wait() p.wait()
for l in p.stdout.readlines(): for line in p.stdout.readlines():
if isinstance(l, bytes): if isinstance(line, bytes):
l = l.decode('utf-8') line = line.decode('utf-8')
yield l yield line
...@@ -61,6 +61,21 @@ ...@@ -61,6 +61,21 @@
<label for="description">{{_('Description')}}</label> <label for="description">{{_('Description')}}</label>
<textarea class="form-control" name="description" id="description" rows="7">{% if book.comments %}{{book.comments[0].text}}{%endif%}</textarea> <textarea class="form-control" name="description" id="description" rows="7">{% if book.comments %}{{book.comments[0].text}}{%endif%}</textarea>
</div> </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"> <div class="form-group">
<label for="tags">{{_('Tags')}}</label> <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 %}"> <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 @@ ...@@ -169,7 +184,7 @@
</div> </div>
<a href="#" id="get_meta" class="btn btn-default" data-toggle="modal" data-target="#metaModal">{{_('Fetch Metadata')}}</a> <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> <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> </div>
</form> </form>
...@@ -185,12 +200,20 @@ ...@@ -185,12 +200,20 @@
<span>{{_('Are you really sure?')}}</span> <span>{{_('Are you really sure?')}}</span>
</div> </div>
<div class="modal-body text-center"> <div class="modal-body text-center">
<p>
<span>{{_('This book will be permanently erased from database')}}</span> <span>{{_('This book will be permanently erased from database')}}</span>
<span>{{_('and hard disk')}}</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>
<div class="modal-footer"> <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> <button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div> </div>
</div> </div>
...@@ -277,6 +300,21 @@ ...@@ -277,6 +300,21 @@
'source': {{_('Source')|safe|tojson}}, 'source': {{_('Source')|safe|tojson}},
}; };
var language = '{{ g.user.locale }}'; 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>
<script src="{{ url_for('static', filename='js/libs/typeahead.bundle.js') }}"></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> <script src="{{ url_for('static', filename='js/libs/bootstrap-rating-input.min.js') }}"></script>
......
...@@ -202,6 +202,14 @@ ...@@ -202,6 +202,14 @@
</label> </label>
</form> </form>
</p> </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> </div>
{% endif %} {% endif %}
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<div class="filterheader hidden-xs hidden-sm"> <div class="filterheader hidden-xs hidden-sm">
{% if entries.__len__() %} {% 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> <button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button>
{% endif %} {% endif %}
{% endif %} {% endif %}
......
This diff is collapsed.
...@@ -69,7 +69,7 @@ class Updater(threading.Thread): ...@@ -69,7 +69,7 @@ class Updater(threading.Thread):
def get_available_updates(self, request_method, locale): def get_available_updates(self, request_method, locale):
if config.config_updatechannel == constants.UPDATE_STABLE: if config.config_updatechannel == constants.UPDATE_STABLE:
return self._stable_available_updates(request_method) 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): def do_work(self):
try: try:
...@@ -132,7 +132,7 @@ class Updater(threading.Thread): ...@@ -132,7 +132,7 @@ class Updater(threading.Thread):
def pause(self): def pause(self):
self.can_run.clear() self.can_run.clear()
#should just resume the thread # should just resume the thread
def resume(self): def resume(self):
self.can_run.set() self.can_run.set()
...@@ -268,7 +268,7 @@ class Updater(threading.Thread): ...@@ -268,7 +268,7 @@ class Updater(threading.Thread):
def is_venv(self): def is_venv(self):
if (hasattr(sys, 'real_prefix')) or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix): 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: else:
return False return False
...@@ -280,7 +280,7 @@ class Updater(threading.Thread): ...@@ -280,7 +280,7 @@ class Updater(threading.Thread):
@classmethod @classmethod
def _stable_version_info(cls): 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): def _nightly_available_updates(self, request_method, locale):
tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone) tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
...@@ -436,7 +436,7 @@ class Updater(threading.Thread): ...@@ -436,7 +436,7 @@ class Updater(threading.Thread):
patch_version_update > current_version[2]) or \ patch_version_update > current_version[2]) or \
minor_version_update > current_version[1]: minor_version_update > current_version[1]:
parents.append([commit[i]['tag_name'], commit[i]['body'].replace('\r\n', '<p>')]) parents.append([commit[i]['tag_name'], commit[i]['body'].replace('\r\n', '<p>')])
newer=True newer = True
i -= 1 i -= 1
continue continue
if major_version_update < current_version[0]: 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