diff --git a/cps/db.py b/cps/db.py index fe4b4b51..ccc055e9 100755 --- a/cps/db.py +++ b/cps/db.py @@ -11,10 +11,8 @@ from ub import config import ub session = None -cc_exceptions = None +cc_exceptions = ['datetime', 'int', 'comments', 'float', 'composite', 'series'] cc_classes = None -cc_ids = None -books_custom_column_links = None engine = None @@ -283,12 +281,9 @@ class Custom_Columns(Base): def setup_db(): - global session - global cc_exceptions - global cc_classes - global cc_ids - global books_custom_column_links global engine + global session + global cc_classes if config.config_calibre_dir is None or config.config_calibre_dir == u'': return False @@ -298,7 +293,6 @@ def setup_db(): engine = create_engine('sqlite:///'+ dbpath, echo=False, isolation_level="SERIALIZABLE") try: conn = engine.connect() - except Exception as e: content = ub.session.query(ub.Settings).first() content.config_calibre_dir = None @@ -312,43 +306,43 @@ def setup_db(): config.loadSettings() conn.connection.create_function('title_sort', 1, title_sort) - cc = conn.execute("SELECT id, datatype FROM custom_columns") + if not cc_classes: + cc = conn.execute("SELECT id, datatype FROM custom_columns") - cc_ids = [] - cc_exceptions = ['datetime', 'int', 'comments', 'float', 'composite', 'series'] - books_custom_column_links = {} - cc_classes = {} - for row in cc: - if row.datatype not in cc_exceptions: - books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', Base.metadata, - Column('book', Integer, ForeignKey('books.id'), - primary_key=True), - Column('value', Integer, - ForeignKey('custom_column_' + str(row.id) + '.id'), - primary_key=True) - ) - cc_ids.append([row.id, row.datatype]) - if row.datatype == 'bool': - ccdict = {'__tablename__': 'custom_column_' + str(row.id), - 'id': Column(Integer, primary_key=True), - 'book': Column(Integer, ForeignKey('books.id')), - 'value': Column(Boolean)} + cc_ids = [] + books_custom_column_links = {} + cc_classes = {} + for row in cc: + if row.datatype not in cc_exceptions: + books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', Base.metadata, + Column('book', Integer, ForeignKey('books.id'), + primary_key=True), + Column('value', Integer, + ForeignKey('custom_column_' + str(row.id) + '.id'), + primary_key=True) + ) + cc_ids.append([row.id, row.datatype]) + if row.datatype == 'bool': + ccdict = {'__tablename__': 'custom_column_' + str(row.id), + 'id': Column(Integer, primary_key=True), + 'book': Column(Integer, ForeignKey('books.id')), + 'value': Column(Boolean)} + else: + ccdict = {'__tablename__': 'custom_column_' + str(row.id), + 'id': Column(Integer, primary_key=True), + 'value': Column(String)} + cc_classes[row.id] = type('Custom_Column_' + str(row.id), (Base,), ccdict) + + for id in cc_ids: + if id[1] == 'bool': + setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]], + primaryjoin=( + Books.id == cc_classes[id[0]].book), + backref='books')) else: - ccdict = {'__tablename__': 'custom_column_' + str(row.id), - 'id': Column(Integer, primary_key=True), - 'value': Column(String)} - cc_classes[row.id] = type('Custom_Column_' + str(row.id), (Base,), ccdict) - - for id in cc_ids: - if id[1] == 'bool': - setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]], - primaryjoin=( - Books.id == cc_classes[id[0]].book), - backref='books')) - else: - setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]], - secondary=books_custom_column_links[id[0]], - backref='books')) + setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]], + secondary=books_custom_column_links[id[0]], + backref='books')) # Base.metadata.create_all(engine) Session = sessionmaker() diff --git a/cps/epub.py b/cps/epub.py index 455b1ab8..368eadf5 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -41,11 +41,14 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension): p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0] epub_metadata = {} +<<<<<<< HEAD try:#maybe description isn't present comments = tree.xpath("//*[local-name() = 'description']/text()")[0] epub_metadata['comments'] = comments except IndexError as e: epub_metadata['comments'] = "" +======= +>>>>>>> master for s in ['title', 'description', 'creator', 'language']: tmp = p.xpath('dc:%s/text()' % s, namespaces=ns) @@ -71,7 +74,11 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension): epub_metadata['language'] = isoLanguages.get(part3=lang).name else: epub_metadata['language'] = "" +<<<<<<< HEAD +======= + +>>>>>>> master coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns) coverfile = None if len(coversection) > 0: diff --git a/cps/helper.py b/cps/helper.py index 3430de97..299fb962 100755 --- a/cps/helper.py +++ b/cps/helper.py @@ -14,15 +14,16 @@ import traceback import re import unicodedata try: - from io import StringIO - from email.mime.base import MIMEBase - from email.mime.multipart import MIMEMultipart - from email.mime.text import MIMEText -except ImportError as e: from StringIO import StringIO from email.MIMEBase import MIMEBase from email.MIMEMultipart import MIMEMultipart from email.MIMEText import MIMEText +except ImportError as e: + from io import StringIO + from email.mime.base import MIMEBase + from email.mime.multipart import MIMEMultipart + from email.mime.text import MIMEText + from email import encoders from email.generator import Generator from email.utils import formatdate diff --git a/cps/static/js/get_meta.js b/cps/static/js/get_meta.js new file mode 100644 index 00000000..2cec1252 --- /dev/null +++ b/cps/static/js/get_meta.js @@ -0,0 +1,180 @@ +/* + * Get Metadata from Douban Books api and Google Books api + * Created by idalin + * Google Books api document: https://developers.google.com/books/docs/v1/using + * Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only) + */ + +$(document).ready(function () { + var msg = i18n_msg; + var douban = 'https://api.douban.com'; + var db_search = '/v2/book/search'; + var db_get_info = '/v2/book/'; + var db_get_info_by_isbn = '/v2/book/isbn/ '; + var db_done = false; + + var google = 'https://www.googleapis.com/'; + var gg_search = '/books/v1/volumes'; + var gg_get_info = '/books/v1/volumes/'; + var gg_done = false; + + var db_results = []; + var gg_results = []; + var show_flag = 0; + String.prototype.replaceAll = function (s1, s2) {   + return this.replace(new RegExp(s1, "gm"), s2);   + } + + gg_search_book = function (title) { + title = title.replaceAll(/\s+/, '+'); + var url = google + gg_search + '?q=' + title; + $.ajax({ + url: url, + type: "GET", + dataType: "jsonp", + jsonp: 'callback', + success: function (data) { + gg_results = data.items; + }, + complete: function () { + gg_done = true; + show_result(); + } + }); + } + + get_meta = function (source, id) { + var meta; + if (source == 'google') {; + meta = gg_results[id]; + $('#description').val(meta.volumeInfo.description); + $('#bookAuthor').val(meta.volumeInfo.authors.join(' & ')); + $('#book_title').val(meta.volumeInfo.title); + if (meta.volumeInfo.categories) { + var tags = meta.volumeInfo.categories.join(','); + $('#tags').val(tags); + } + if (meta.volumeInfo.averageRating) { + $('#rating').val(Math.round(meta.volumeInfo.averageRating)); + } + return; + } + if (source == 'douban') { + meta = db_results[id]; + $('#description').val(meta.summary); + $('#bookAuthor').val(meta.author.join(' & ')); + $('#book_title').val(meta.title); + var tags = ''; + for (var i = 0; i < meta.tags.length; i++) { + tags = tags + meta.tags[i].title + ','; + } + $('#tags').val(tags); + $('#rating').val(Math.round(meta.rating.average / 2)); + return; + } + } + do_search = function (keyword) { + show_flag = 0; + $('#meta-info').text(msg.loading); + var keyword = $('#keyword').val(); + if (keyword) { + db_search_book(keyword); + gg_search_book(keyword); + } + } + + db_search_book = function (title) { + var url = douban + db_search + '?q=' + title + '&fields=all&count=10'; + $.ajax({ + url: url, + type: "GET", + dataType: "jsonp", + jsonp: 'callback', + success: function (data) { + db_results = data.books; + }, + error: function () { + $('#meta-info').html('

'+ msg.search_error+'!

'); + }, + complete: function () { + db_done = true; + show_result(); + } + }); + } + + show_result = function () { + show_flag++; + if (show_flag == 1) { + $('#meta-info').html(''); + } + if (gg_done && db_done) { + if (!gg_results && !db_results) { + $('#meta-info').html('

'+ msg.no_result +'

'); + return; + } + } + if (gg_done && gg_results.length > 0) { + for (var i = 0; i < gg_results.length; i++) { + var book = gg_results[i]; + var book_cover; + if (book.volumeInfo.imageLinks) { + book_cover = book.volumeInfo.imageLinks.thumbnail; + } else { + book_cover = '/static/generic_cover.jpg'; + } + var book_html = '
  • ' + + 'Cover' + + '
    ' + + '

    ' + book.volumeInfo.title + '

    ' + + '

    '+ msg.author +':' + book.volumeInfo.authors + '

    ' + + '

    '+ msg.publisher + ':' + book.volumeInfo.publisher + '

    ' + + '

    '+ msg.description + ':' + book.volumeInfo.description + '

    ' + + '

    '+ msg.source + ':Google Books

    ' + + '
    ' + + '
  • '; + $("#book-list").append(book_html); + } + gg_done = false; + } + if (db_done && db_results.length > 0) { + for (var i = 0; i < db_results.length; i++) { + var book = db_results[i]; + var book_html = '
  • ' + + 'Cover' + + '
    ' + + '

    ' + book.title + '

    ' + + '

    ' + msg.author + ':' + book.author + '

    ' + + '

    ' + msg.publisher + ':' + book.publisher + '

    ' + + '

    ' + msg.description + ':' + book.summary + '

    ' + + '

    ' + msg.source + ':Douban Books

    ' + + '
    ' + + '
  • '; + $("#book-list").append(book_html); + } + db_done = false; + } + } + + $('#do-search').click(function () { + var keyword = $('#keyword').val(); + if (keyword) { + do_search(keyword); + } + }); + + $('#get_meta').click(function () { + var book_title = $('#book_title').val(); + if (book_title) { + $('#keyword').val(book_title); + do_search(book_title); + } + }); + +}); \ No newline at end of file diff --git a/cps/static/js/libs/bootstrap-rating-input.min.js b/cps/static/js/libs/bootstrap-rating-input.min.js new file mode 100644 index 00000000..0398742e --- /dev/null +++ b/cps/static/js/libs/bootstrap-rating-input.min.js @@ -0,0 +1 @@ +!function(a){"use strict";function b(a){return"[data-value"+(a?"="+a:"")+"]"}function c(a,b,c){var d=c.activeIcon,e=c.inactiveIcon;a.removeClass(b?e:d).addClass(b?d:e)}function d(b,c){var d=a.extend({},i,b.data(),c);return d.inline=""===d.inline||d.inline,d.readonly=""===d.readonly||d.readonly,d.clearable===!1?d.clearableLabel="":d.clearableLabel=d.clearable,d.clearable=""===d.clearable||d.clearable,d}function e(b,c){if(c.inline)var d=a('');else var d=a('
    ');d.addClass(b.attr("class")),d.removeClass("rating");for(var e=c.min;e<=c.max;e++)d.append('');return c.clearable&&!c.readonly&&d.append(" ").append(''+c.clearableLabel+""),d}var f="rating-clear",g="."+f,h="hidden",i={min:1,max:5,"empty-value":0,iconLib:"glyphicon",activeIcon:"glyphicon-star",inactiveIcon:"glyphicon-star-empty",clearable:!1,clearableIcon:"glyphicon-remove",clearableRemain:!1,inline:!1,readonly:!1},j=function(a,b){var c=this.$input=a;this.options=d(c,b);var f=this.$el=e(c,this.options);c.addClass(h).before(f),c.attr("type","hidden"),this.highlight(c.val())};j.VERSION="0.4.0",j.DEFAULTS=i,j.prototype={clear:function(){this.setValue(this.options["empty-value"])},setValue:function(a){this.highlight(a),this.updateInput(a)},highlight:function(a,d){var e=this.options,f=this.$el;if(a>=this.options.min&&a<=this.options.max){var i=f.find(b(a));c(i.prevAll("i").andSelf(),!0,e),c(i.nextAll("i"),!1,e)}else c(f.find(b()),!1,e);d||(this.options.clearableRemain?f.find(g).removeClass(h):a&&a!=this.options["empty-value"]?f.find(g).removeClass(h):f.find(g).addClass(h))},updateInput:function(a){var b=this.$input;b.val()!=a&&b.val(a).change()}};var k=a.fn.rating=function(c){return this.filter("input[type=number]").each(function(){var d=a(this),e="object"==typeof c&&c||{},f=new j(d,e);f.options.readonly||f.$el.on("mouseenter",b(),function(){f.highlight(a(this).data("value"),!0)}).on("mouseleave",b(),function(){f.highlight(d.val(),!0)}).on("click",b(),function(){f.setValue(a(this).data("value"))}).on("click",g,function(){f.clear()})})};k.Constructor=j,a(function(){a("input.rating[type=number]").each(function(){a(this).rating()})})}(jQuery); \ No newline at end of file diff --git a/cps/static/js/main.js b/cps/static/js/main.js index 8b0b099f..7a498f36 100644 --- a/cps/static/js/main.js +++ b/cps/static/js/main.js @@ -65,6 +65,13 @@ $(function() { } }); }); + $("#restart_database").click(function() { + $.ajax({ + dataType: 'json', + url: window.location.pathname+"/../../shutdown", + data: {"parameter":2} + }); + }); $("#perform_update").click(function() { $('#spinner2').show(); $.ajax({ diff --git a/cps/templates/admin.html b/cps/templates/admin.html index b386e4ff..18d99ab8 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -80,6 +80,7 @@
    {{_('Current commit timestamp')}}: {{commit}}

    +
    {{_('Reconnect to Calibre DB')}}
    {{_('Restart Calibre-web')}}
    {{_('Stop Calibre-web')}}
    {{_('Check for update')}}
    diff --git a/cps/templates/book_edit.html b/cps/templates/book_edit.html index 32452dd8..86b7281e 100644 --- a/cps/templates/book_edit.html +++ b/cps/templates/book_edit.html @@ -39,7 +39,7 @@
    - +
    @@ -104,16 +104,56 @@ {{_('view book after edit')}}
    + {{_('Get Metadata')}} {{_('Back')}} {% endif %} + {% endblock %} {% block js %} + +<<<<<<< HEAD + +======= + {% endblock %} {% block header %} diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index 7824ddd0..0f6a88ee 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -23,7 +23,10 @@ - +
    + + +
    diff --git a/cps/templates/feed.xml b/cps/templates/feed.xml index 5bda14f9..52fb2ba3 100644 --- a/cps/templates/feed.xml +++ b/cps/templates/feed.xml @@ -35,6 +35,7 @@ https://github.com/janeczku/calibre-web + {% if entries[0] %} {% for entry in entries %} {{entry.title}} @@ -60,6 +61,7 @@ {% endfor %} {% endfor %} + {% endif %} {% for entry in listelements %} {{entry.name}} diff --git a/cps/templates/index.html b/cps/templates/index.html index 7ab46a7b..744fea89 100755 --- a/cps/templates/index.html +++ b/cps/templates/index.html @@ -40,6 +40,7 @@

    {{title}}

    + {% if entries[0] %} {% for entry in entries %}
    @@ -76,6 +77,7 @@
    {% endfor %} + {% endif %}
    {% endblock %} diff --git a/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo b/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo index c4c7af27..43ea4912 100644 Binary files a/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo and b/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po b/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po index 1dca04ac..1bc17782 100644 --- a/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po +++ b/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po @@ -325,7 +325,7 @@ msgstr "发送测试邮件时发生错误: %(res)s" #: cps/web.py:1816 msgid "E-Mail settings updated" -msgstr "" +msgstr "E-Mail 设置已更新" #: cps/web.py:1817 msgid "Edit mail settings" @@ -357,11 +357,11 @@ msgstr "编辑元数据" #: cps/web.py:2162 #, python-format msgid "File extension \"%s\" is not allowed to be uploaded to this server" -msgstr "" +msgstr "不能上传后缀为 \"%s\" 的文件到此服务器" #: cps/web.py:2168 msgid "File to be uploaded must have an extension" -msgstr "" +msgstr "要上传的文件必须有一个后缀" #: cps/web.py:2185 #, python-format diff --git a/cps/ub.py b/cps/ub.py index ad638337..6c941aa3 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -254,6 +254,7 @@ class Settings(Base): config_anonbrowse = Column(SmallInteger, default=0) config_public_reg = Column(SmallInteger, default=0) config_default_role = Column(SmallInteger, default=0) + config_columns_to_ignore = Column(String) def __repr__(self): pass @@ -280,6 +281,7 @@ class Config: self.config_anonbrowse = data.config_anonbrowse self.config_public_reg = data.config_public_reg self.config_default_role = data.config_default_role + self.config_columns_to_ignore = data.config_columns_to_ignore if self.config_calibre_dir is not None: self.db_configured = True else: @@ -361,6 +363,12 @@ def migrate_Database(): conn.execute("ALTER TABLE Settings ADD column `config_anonbrowse` SmallInteger DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_public_reg` SmallInteger DEFAULT 0") session.commit() + try: + session.query(exists().where(Settings.config_columns_to_ignore)).scalar() + except exc.OperationalError: + conn = engine.connect() + conn.execute("ALTER TABLE Settings ADD column `config_columns_to_ignore` String DEFAULT ''") + session.commit() try: session.query(exists().where(Settings.config_default_role)).scalar() session.commit() diff --git a/cps/web.py b/cps/web.py index 1c7dd1b1..06e0b037 100755 --- a/cps/web.py +++ b/cps/web.py @@ -18,7 +18,6 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy import __version__ as sqlalchemyVersion from math import ceil from flask_login import LoginManager, login_user, logout_user, login_required, current_user -from flask_login.__about__ import __version__ as flask_loginVersion from flask_principal import Principal, Identity, AnonymousIdentity, identity_changed from flask_principal import __version__ as flask_principalVersion from flask_babel import Babel @@ -47,7 +46,6 @@ import db from shutil import move, copyfile from tornado.ioloop import IOLoop from tornado import version as tornadoVersion -#from builtins import str try: from urllib.parse import quote @@ -56,6 +54,11 @@ try: except ImportError as e: from urllib import quote +try: + from flask_login import __version__ as flask_loginVersion +except ImportError, e: + from flask_login.__about__ import __version__ as flask_loginVersion + try: from wand.image import Image @@ -144,6 +147,15 @@ lm.anonymous_user = ub.Anonymous app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' db.setup_db() +if config.config_log_level == logging.DEBUG : + logging.getLogger("sqlalchemy.engine").addHandler(file_handler) + logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) + logging.getLogger("sqlalchemy.pool").addHandler(file_handler) + logging.getLogger("sqlalchemy.pool").setLevel(config.config_log_level) + logging.getLogger("sqlalchemy.orm").addHandler(file_handler) + logging.getLogger("sqlalchemy.orm").setLevel(config.config_log_level) + + @babel.localeselector def get_locale(): # if a user is logged in, use the locale from the user settings @@ -248,8 +260,6 @@ class Pagination(object): def iter_pages(self, left_edge=2, left_current=2, right_current=5, right_edge=2): last = 0 - if sys.version_info.major >= 3: - xrange = range for num in xrange(1, self.pages + 1): # ToDo: can be simplified if num <= left_edge or (num > self.page - left_current - 1 and num < self.page + right_current) \ or num > self.pages - right_edge: @@ -560,7 +570,13 @@ def feed_hot(): hot_books = all_books.offset(off).limit(config.config_books_per_page) entries = list() for book in hot_books: - entries.append(db.session.query(db.Books).filter(filter).filter(db.Books.id == book.Downloads.book_id).first()) + downloadBook = db.session.query(db.Books).filter(db.Books.id == book.Downloads.book_id).first() + if downloadBook: + entries.append( + db.session.query(db.Books).filter(filter).filter(db.Books.id == book.Downloads.book_id).first()) + else: + ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete() + ub.session.commit() numBooks = entries.__len__() pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, numBooks) xml = render_title_template('feed.xml', entries=entries, pagination=pagination) @@ -849,7 +865,13 @@ def hot_books(page): hot_books = all_books.offset(off).limit(config.config_books_per_page) entries = list() for book in hot_books: - entries.append(db.session.query(db.Books).filter(filter).filter(db.Books.id == book.Downloads.book_id).first()) + downloadBook = db.session.query(db.Books).filter(db.Books.id == book.Downloads.book_id).first() + if downloadBook: + entries.append( + db.session.query(db.Books).filter(filter).filter(db.Books.id == book.Downloads.book_id).first()) + else: + ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete() + ub.session.commit() numBooks = entries.__len__() pagination = Pagination(page, config.config_books_per_page, numBooks) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, @@ -1016,7 +1038,16 @@ def show_book(id): except Exception as e: entries.languages[index].language_name = _( isoLanguages.get(part3=entries.languages[index].lang_code).name) - cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() + tmpcc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() + + if config.config_columns_to_ignore: + cc=[] + for col in tmpcc: + r= re.compile(config.config_columns_to_ignore) + if r.match(col.label): + cc.append(col) + else: + cc=tmpcc book_in_shelfs = [] shelfs = ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == id).all() for entry in shelfs: @@ -1097,6 +1128,11 @@ def shutdown(): showtext['text'] = _(u'Performing shutdown of server, please close window') return json.dumps(showtext) else: + if task == 2: + db.session.close() + db.engine.dispose() + db.setup_db() + return json.dumps({}) abort(404) @app.route("/update") @@ -1248,22 +1284,22 @@ def read_book(book_id, format): zfile.close() return render_title_template('read.html', bookid=book_id, title=_(u"Read a Book")) elif format.lower() == "pdf": - all_name = str(book_id) + "/" + quote(book.data[0].name) + ".pdf" - tmp_file = os.path.join(book_dir, quote(book.data[0].name)) + ".pdf" + all_name = str(book_id) + "/" + book.data[0].name + ".pdf" + tmp_file = os.path.join(book_dir, book.data[0].name) + ".pdf" if not os.path.exists(tmp_file): pdf_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".pdf" copyfile(pdf_file, tmp_file) return render_title_template('readpdf.html', pdffile=all_name, title=_(u"Read a Book")) elif format.lower() == "txt": - all_name = str(book_id) + "/" + quote(book.data[0].name) + ".txt" - tmp_file = os.path.join(book_dir, quote(book.data[0].name)) + ".txt" + all_name = str(book_id) + "/" + book.data[0].name + ".txt" + tmp_file = os.path.join(book_dir, book.data[0].name) + ".txt" if not os.path.exists(all_name): txt_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".txt" copyfile(txt_file, tmp_file) return render_title_template('readtxt.html', txtfile=all_name, title=_(u"Read a Book")) elif format.lower() == "cbr": - all_name = str(book_id) + "/" + quote(book.data[0].name) + ".cbr" - tmp_file = os.path.join(book_dir, quote(book.data[0].name)) + ".cbr" + all_name = str(book_id) + "/" + book.data[0].name + ".cbr" + tmp_file = os.path.join(book_dir, book.data[0].name) + ".cbr" if not os.path.exists(all_name): cbr_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".cbr" copyfile(cbr_file, tmp_file) @@ -1295,7 +1331,11 @@ def get_download_link(book_id, format): response.headers["Content-Type"] = mimetypes.types_map['.' + format] except Exception as e: pass +<<<<<<< HEAD response.headers["Content-Disposition"] = "attachment; filename*=UTF-8''%s.%s" % (urllib.quote(file_name.encode('utf-8')), format) +======= + response.headers["Content-Disposition"] = "attachment; filename*=UTF-8''%s.%s" % (quote(file_name.encode('utf-8')), format) +>>>>>>> master return response else: abort(404) @@ -1666,6 +1706,8 @@ def configuration_helper(origin): reboot_required = True if "config_calibre_web_title" in to_save: content.config_calibre_web_title = to_save["config_calibre_web_title"] + if "config_columns_to_ignore" in to_save: + content.config_columns_to_ignore = to_save["config_columns_to_ignore"] if "config_title_regex" in to_save: if content.config_title_regex != to_save["config_title_regex"]: content.config_title_regex = to_save["config_title_regex"] diff --git a/readme.md b/readme.md index 8b36e132..89f1c84d 100755 --- a/readme.md +++ b/readme.md @@ -7,7 +7,8 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d ![screenshot](https://raw.githubusercontent.com/janeczku/docker-calibre-web/master/screenshot.png) -##Features +## Features + - Bootstrap 3 HTML5 interface - full graphical setup - User management @@ -29,13 +30,14 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d ## Quick start -1. Execute the command: `python cps.py` -2. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog -3. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button -4. Go to Login page +1. Install required dependencies by executing `pip install -r requirements.txt` +2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window) +3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog +4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button +5. Go to Login page -**Default admin login:** -*Username:* admin +**Default admin login:** +*Username:* admin *Password:* admin123 ## Runtime Configuration Options @@ -57,10 +59,10 @@ Tick to enable uploading of PDF, epub, FB2. This requires the imagemagick librar ## Requirements Python 2.7+ - -Optionally, to enable on-the-fly conversion from EPUB to MOBI when using the send-to-kindle feature: -[Download](http://www.amazon.com/gp/feature.html?docId=1000765211) Amazon's KindleGen tool for your platform and place the binary named as `kindlegen` in the `vendor` folder. +Optionally, to enable on-the-fly conversion from EPUB to MOBI when using the send-to-kindle feature: + +[Download](http://www.amazon.com/gp/feature.html?docId=1000765211) Amazon's KindleGen tool for your platform and place the binary named as `kindlegen` in the `vendor` folder. ## Docker image @@ -132,4 +134,4 @@ Replace the user and ExecStart with your user and foldernames. `sudo systemctl enable cps.service` -enables the service. +enables the service. diff --git a/requirements.txt b/requirements.txt index d2fe9152..1ac4b39d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,25 +1,14 @@ -future -#sqlalchemy +Babel>=1.3 +Flask>=0.11 +Flask-Babel==0.11.1 +Flask-Login>=0.3.2 +Flask-Principal>=0.3.2 +iso-639>=0.4.5 +PyPDF2==1.26.0 +pytz>=2016.10 +requests>=2.11.1 +SQLAlchemy>=0.8.4 +tornado>=4.1 +Wand>=0.4.4 +#future -PyPDF2 -babel -blinker -click -flask -flask_babel -flask_login -flask_principal -iso-639 -itsdangerous -jinja2 -markupsafe -pytz -requests -singledispatch -six -sqlalchemy -tornado -#https://pypi.python.org/packages/02/f8/97105237d0ba693b6f0bdcd94da0504e9a4433988c4393d8d3049094be7a/validate-1.0.1.tar.gz -#validate -wand -werkzeug diff --git a/vendor/.gitempty b/vendor/.gitempty new file mode 100644 index 00000000..e69de29b diff --git a/vendor/validate.py b/vendor/validate.py deleted file mode 100644 index b7a964c4..00000000 --- a/vendor/validate.py +++ /dev/null @@ -1,1472 +0,0 @@ -# validate.py -# A Validator object -# Copyright (C) 2005-2014: -# (name) : (email) -# Michael Foord: fuzzyman AT voidspace DOT org DOT uk -# Mark Andrews: mark AT la-la DOT com -# Nicola Larosa: nico AT tekNico DOT net -# Rob Dennis: rdennis AT gmail DOT com -# Eli Courtwright: eli AT courtwright DOT org - -# This software is licensed under the terms of the BSD license. -# http://opensource.org/licenses/BSD-3-Clause - -# ConfigObj 5 - main repository for documentation and issue tracking: -# https://github.com/DiffSK/configobj - -""" - The Validator object is used to check that supplied values - conform to a specification. - - The value can be supplied as a string - e.g. from a config file. - In this case the check will also *convert* the value to - the required type. This allows you to add validation - as a transparent layer to access data stored as strings. - The validation checks that the data is correct *and* - converts it to the expected type. - - Some standard checks are provided for basic data types. - Additional checks are easy to write. They can be - provided when the ``Validator`` is instantiated or - added afterwards. - - The standard functions work with the following basic data types : - - * integers - * floats - * booleans - * strings - * ip_addr - - plus lists of these datatypes - - Adding additional checks is done through coding simple functions. - - The full set of standard checks are : - - * 'integer': matches integer values (including negative) - Takes optional 'min' and 'max' arguments : :: - - integer() - integer(3, 9) # any value from 3 to 9 - integer(min=0) # any positive value - integer(max=9) - - * 'float': matches float values - Has the same parameters as the integer check. - - * 'boolean': matches boolean values - ``True`` or ``False`` - Acceptable string values for True are : - true, on, yes, 1 - Acceptable string values for False are : - false, off, no, 0 - - Any other value raises an error. - - * 'ip_addr': matches an Internet Protocol address, v.4, represented - by a dotted-quad string, i.e. '1.2.3.4'. - - * 'string': matches any string. - Takes optional keyword args 'min' and 'max' - to specify min and max lengths of the string. - - * 'list': matches any list. - Takes optional keyword args 'min', and 'max' to specify min and - max sizes of the list. (Always returns a list.) - - * 'tuple': matches any tuple. - Takes optional keyword args 'min', and 'max' to specify min and - max sizes of the tuple. (Always returns a tuple.) - - * 'int_list': Matches a list of integers. - Takes the same arguments as list. - - * 'float_list': Matches a list of floats. - Takes the same arguments as list. - - * 'bool_list': Matches a list of boolean values. - Takes the same arguments as list. - - * 'ip_addr_list': Matches a list of IP addresses. - Takes the same arguments as list. - - * 'string_list': Matches a list of strings. - Takes the same arguments as list. - - * 'mixed_list': Matches a list with different types in - specific positions. List size must match - the number of arguments. - - Each position can be one of : - 'integer', 'float', 'ip_addr', 'string', 'boolean' - - So to specify a list with two strings followed - by two integers, you write the check as : :: - - mixed_list('string', 'string', 'integer', 'integer') - - * 'pass': This check matches everything ! It never fails - and the value is unchanged. - - It is also the default if no check is specified. - - * 'option': This check matches any from a list of options. - You specify this check with : :: - - option('option 1', 'option 2', 'option 3') - - You can supply a default value (returned if no value is supplied) - using the default keyword argument. - - You specify a list argument for default using a list constructor syntax in - the check : :: - - checkname(arg1, arg2, default=list('val 1', 'val 2', 'val 3')) - - A badly formatted set of arguments will raise a ``VdtParamError``. -""" - -__version__ = '1.0.1' - - -__all__ = ( - '__version__', - 'dottedQuadToNum', - 'numToDottedQuad', - 'ValidateError', - 'VdtUnknownCheckError', - 'VdtParamError', - 'VdtTypeError', - 'VdtValueError', - 'VdtValueTooSmallError', - 'VdtValueTooBigError', - 'VdtValueTooShortError', - 'VdtValueTooLongError', - 'VdtMissingValue', - 'Validator', - 'is_integer', - 'is_float', - 'is_boolean', - 'is_list', - 'is_tuple', - 'is_ip_addr', - 'is_string', - 'is_int_list', - 'is_bool_list', - 'is_float_list', - 'is_string_list', - 'is_ip_addr_list', - 'is_mixed_list', - 'is_option', - '__docformat__', -) - - -import re -import sys -from pprint import pprint - -#TODO - #21 - six is part of the repo now, but we didn't switch over to it here -# this could be replaced if six is used for compatibility, or there are no -# more assertions about items being a string -if sys.version_info < (3,): - string_type = basestring -else: - string_type = str - # so tests that care about unicode on 2.x can specify unicode, and the same - # tests when run on 3.x won't complain about a undefined name "unicode" - # since all strings are unicode on 3.x we just want to pass it through - # unchanged - unicode = lambda x: x - # in python 3, all ints are equivalent to python 2 longs, and they'll - # never show "L" in the repr - long = int - -_list_arg = re.compile(r''' - (?: - ([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*list\( - ( - (?: - \s* - (?: - (?:".*?")| # double quotes - (?:'.*?')| # single quotes - (?:[^'",\s\)][^,\)]*?) # unquoted - ) - \s*,\s* - )* - (?: - (?:".*?")| # double quotes - (?:'.*?')| # single quotes - (?:[^'",\s\)][^,\)]*?) # unquoted - )? # last one - ) - \) - ) -''', re.VERBOSE | re.DOTALL) # two groups - -_list_members = re.compile(r''' - ( - (?:".*?")| # double quotes - (?:'.*?')| # single quotes - (?:[^'",\s=][^,=]*?) # unquoted - ) - (?: - (?:\s*,\s*)|(?:\s*$) # comma - ) -''', re.VERBOSE | re.DOTALL) # one group - -_paramstring = r''' - (?: - ( - (?: - [a-zA-Z_][a-zA-Z0-9_]*\s*=\s*list\( - (?: - \s* - (?: - (?:".*?")| # double quotes - (?:'.*?')| # single quotes - (?:[^'",\s\)][^,\)]*?) # unquoted - ) - \s*,\s* - )* - (?: - (?:".*?")| # double quotes - (?:'.*?')| # single quotes - (?:[^'",\s\)][^,\)]*?) # unquoted - )? # last one - \) - )| - (?: - (?:".*?")| # double quotes - (?:'.*?')| # single quotes - (?:[^'",\s=][^,=]*?)| # unquoted - (?: # keyword argument - [a-zA-Z_][a-zA-Z0-9_]*\s*=\s* - (?: - (?:".*?")| # double quotes - (?:'.*?')| # single quotes - (?:[^'",\s=][^,=]*?) # unquoted - ) - ) - ) - ) - (?: - (?:\s*,\s*)|(?:\s*$) # comma - ) - ) - ''' - -_matchstring = '^%s*' % _paramstring - -# Python pre 2.2.1 doesn't have bool -try: - bool -except NameError: - def bool(val): - """Simple boolean equivalent function. """ - if val: - return 1 - else: - return 0 - - -def dottedQuadToNum(ip): - """ - Convert decimal dotted quad string to long integer - - >>> int(dottedQuadToNum('1 ')) - 1 - >>> int(dottedQuadToNum(' 1.2')) - 16777218 - >>> int(dottedQuadToNum(' 1.2.3 ')) - 16908291 - >>> int(dottedQuadToNum('1.2.3.4')) - 16909060 - >>> dottedQuadToNum('255.255.255.255') - 4294967295 - >>> dottedQuadToNum('255.255.255.256') - Traceback (most recent call last): - ValueError: Not a good dotted-quad IP: 255.255.255.256 - """ - - # import here to avoid it when ip_addr values are not used - import socket, struct - - try: - return struct.unpack('!L', - socket.inet_aton(ip.strip()))[0] - except socket.error: - raise ValueError('Not a good dotted-quad IP: %s' % ip) - return - - -def numToDottedQuad(num): - """ - Convert int or long int to dotted quad string - - >>> numToDottedQuad(long(-1)) - Traceback (most recent call last): - ValueError: Not a good numeric IP: -1 - >>> numToDottedQuad(long(1)) - '0.0.0.1' - >>> numToDottedQuad(long(16777218)) - '1.0.0.2' - >>> numToDottedQuad(long(16908291)) - '1.2.0.3' - >>> numToDottedQuad(long(16909060)) - '1.2.3.4' - >>> numToDottedQuad(long(4294967295)) - '255.255.255.255' - >>> numToDottedQuad(long(4294967296)) - Traceback (most recent call last): - ValueError: Not a good numeric IP: 4294967296 - >>> numToDottedQuad(-1) - Traceback (most recent call last): - ValueError: Not a good numeric IP: -1 - >>> numToDottedQuad(1) - '0.0.0.1' - >>> numToDottedQuad(16777218) - '1.0.0.2' - >>> numToDottedQuad(16908291) - '1.2.0.3' - >>> numToDottedQuad(16909060) - '1.2.3.4' - >>> numToDottedQuad(4294967295) - '255.255.255.255' - >>> numToDottedQuad(4294967296) - Traceback (most recent call last): - ValueError: Not a good numeric IP: 4294967296 - - """ - - # import here to avoid it when ip_addr values are not used - import socket, struct - - # no need to intercept here, 4294967295L is fine - if num > long(4294967295) or num < 0: - raise ValueError('Not a good numeric IP: %s' % num) - try: - return socket.inet_ntoa( - struct.pack('!L', long(num))) - except (socket.error, struct.error, OverflowError): - raise ValueError('Not a good numeric IP: %s' % num) - - -class ValidateError(Exception): - """ - This error indicates that the check failed. - It can be the base class for more specific errors. - - Any check function that fails ought to raise this error. - (or a subclass) - - >>> raise ValidateError - Traceback (most recent call last): - ValidateError - """ - - -class VdtMissingValue(ValidateError): - """No value was supplied to a check that needed one.""" - - -class VdtUnknownCheckError(ValidateError): - """An unknown check function was requested""" - - def __init__(self, value): - """ - >>> raise VdtUnknownCheckError('yoda') - Traceback (most recent call last): - VdtUnknownCheckError: the check "yoda" is unknown. - """ - ValidateError.__init__(self, 'the check "%s" is unknown.' % (value,)) - - -class VdtParamError(SyntaxError): - """An incorrect parameter was passed""" - - def __init__(self, name, value): - """ - >>> raise VdtParamError('yoda', 'jedi') - Traceback (most recent call last): - VdtParamError: passed an incorrect value "jedi" for parameter "yoda". - """ - SyntaxError.__init__(self, 'passed an incorrect value "%s" for parameter "%s".' % (value, name)) - - -class VdtTypeError(ValidateError): - """The value supplied was of the wrong type""" - - def __init__(self, value): - """ - >>> raise VdtTypeError('jedi') - Traceback (most recent call last): - VdtTypeError: the value "jedi" is of the wrong type. - """ - ValidateError.__init__(self, 'the value "%s" is of the wrong type.' % (value,)) - - -class VdtValueError(ValidateError): - """The value supplied was of the correct type, but was not an allowed value.""" - - def __init__(self, value): - """ - >>> raise VdtValueError('jedi') - Traceback (most recent call last): - VdtValueError: the value "jedi" is unacceptable. - """ - ValidateError.__init__(self, 'the value "%s" is unacceptable.' % (value,)) - - -class VdtValueTooSmallError(VdtValueError): - """The value supplied was of the correct type, but was too small.""" - - def __init__(self, value): - """ - >>> raise VdtValueTooSmallError('0') - Traceback (most recent call last): - VdtValueTooSmallError: the value "0" is too small. - """ - ValidateError.__init__(self, 'the value "%s" is too small.' % (value,)) - - -class VdtValueTooBigError(VdtValueError): - """The value supplied was of the correct type, but was too big.""" - - def __init__(self, value): - """ - >>> raise VdtValueTooBigError('1') - Traceback (most recent call last): - VdtValueTooBigError: the value "1" is too big. - """ - ValidateError.__init__(self, 'the value "%s" is too big.' % (value,)) - - -class VdtValueTooShortError(VdtValueError): - """The value supplied was of the correct type, but was too short.""" - - def __init__(self, value): - """ - >>> raise VdtValueTooShortError('jed') - Traceback (most recent call last): - VdtValueTooShortError: the value "jed" is too short. - """ - ValidateError.__init__( - self, - 'the value "%s" is too short.' % (value,)) - - -class VdtValueTooLongError(VdtValueError): - """The value supplied was of the correct type, but was too long.""" - - def __init__(self, value): - """ - >>> raise VdtValueTooLongError('jedie') - Traceback (most recent call last): - VdtValueTooLongError: the value "jedie" is too long. - """ - ValidateError.__init__(self, 'the value "%s" is too long.' % (value,)) - - -class Validator(object): - """ - Validator is an object that allows you to register a set of 'checks'. - These checks take input and test that it conforms to the check. - - This can also involve converting the value from a string into - the correct datatype. - - The ``check`` method takes an input string which configures which - check is to be used and applies that check to a supplied value. - - An example input string would be: - 'int_range(param1, param2)' - - You would then provide something like: - - >>> def int_range_check(value, min, max): - ... # turn min and max from strings to integers - ... min = int(min) - ... max = int(max) - ... # check that value is of the correct type. - ... # possible valid inputs are integers or strings - ... # that represent integers - ... if not isinstance(value, (int, long, string_type)): - ... raise VdtTypeError(value) - ... elif isinstance(value, string_type): - ... # if we are given a string - ... # attempt to convert to an integer - ... try: - ... value = int(value) - ... except ValueError: - ... raise VdtValueError(value) - ... # check the value is between our constraints - ... if not min <= value: - ... raise VdtValueTooSmallError(value) - ... if not value <= max: - ... raise VdtValueTooBigError(value) - ... return value - - >>> fdict = {'int_range': int_range_check} - >>> vtr1 = Validator(fdict) - >>> vtr1.check('int_range(20, 40)', '30') - 30 - >>> vtr1.check('int_range(20, 40)', '60') - Traceback (most recent call last): - VdtValueTooBigError: the value "60" is too big. - - New functions can be added with : :: - - >>> vtr2 = Validator() - >>> vtr2.functions['int_range'] = int_range_check - - Or by passing in a dictionary of functions when Validator - is instantiated. - - Your functions *can* use keyword arguments, - but the first argument should always be 'value'. - - If the function doesn't take additional arguments, - the parentheses are optional in the check. - It can be written with either of : :: - - keyword = function_name - keyword = function_name() - - The first program to utilise Validator() was Michael Foord's - ConfigObj, an alternative to ConfigParser which supports lists and - can validate a config file using a config schema. - For more details on using Validator with ConfigObj see: - https://configobj.readthedocs.org/en/latest/configobj.html - """ - - # this regex does the initial parsing of the checks - _func_re = re.compile(r'(.+?)\((.*)\)', re.DOTALL) - - # this regex takes apart keyword arguments - _key_arg = re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$', re.DOTALL) - - - # this regex finds keyword=list(....) type values - _list_arg = _list_arg - - # this regex takes individual values out of lists - in one pass - _list_members = _list_members - - # These regexes check a set of arguments for validity - # and then pull the members out - _paramfinder = re.compile(_paramstring, re.VERBOSE | re.DOTALL) - _matchfinder = re.compile(_matchstring, re.VERBOSE | re.DOTALL) - - - def __init__(self, functions=None): - """ - >>> vtri = Validator() - """ - self.functions = { - '': self._pass, - 'integer': is_integer, - 'float': is_float, - 'boolean': is_boolean, - 'ip_addr': is_ip_addr, - 'string': is_string, - 'list': is_list, - 'tuple': is_tuple, - 'int_list': is_int_list, - 'float_list': is_float_list, - 'bool_list': is_bool_list, - 'ip_addr_list': is_ip_addr_list, - 'string_list': is_string_list, - 'mixed_list': is_mixed_list, - 'pass': self._pass, - 'option': is_option, - 'force_list': force_list, - } - if functions is not None: - self.functions.update(functions) - # tekNico: for use by ConfigObj - self.baseErrorClass = ValidateError - self._cache = {} - - - def check(self, check, value, missing=False): - """ - Usage: check(check, value) - - Arguments: - check: string representing check to apply (including arguments) - value: object to be checked - Returns value, converted to correct type if necessary - - If the check fails, raises a ``ValidateError`` subclass. - - >>> vtor.check('yoda', '') - Traceback (most recent call last): - VdtUnknownCheckError: the check "yoda" is unknown. - >>> vtor.check('yoda()', '') - Traceback (most recent call last): - VdtUnknownCheckError: the check "yoda" is unknown. - - >>> vtor.check('string(default="")', '', missing=True) - '' - """ - fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check) - - if missing: - if default is None: - # no information needed here - to be handled by caller - raise VdtMissingValue() - value = self._handle_none(default) - - if value is None: - return None - - return self._check_value(value, fun_name, fun_args, fun_kwargs) - - - def _handle_none(self, value): - if value == 'None': - return None - elif value in ("'None'", '"None"'): - # Special case a quoted None - value = self._unquote(value) - return value - - - def _parse_with_caching(self, check): - if check in self._cache: - fun_name, fun_args, fun_kwargs, default = self._cache[check] - # We call list and dict below to work with *copies* of the data - # rather than the original (which are mutable of course) - fun_args = list(fun_args) - fun_kwargs = dict(fun_kwargs) - else: - fun_name, fun_args, fun_kwargs, default = self._parse_check(check) - fun_kwargs = dict([(str(key), value) for (key, value) in list(fun_kwargs.items())]) - self._cache[check] = fun_name, list(fun_args), dict(fun_kwargs), default - return fun_name, fun_args, fun_kwargs, default - - - def _check_value(self, value, fun_name, fun_args, fun_kwargs): - try: - fun = self.functions[fun_name] - except KeyError: - raise VdtUnknownCheckError(fun_name) - else: - return fun(value, *fun_args, **fun_kwargs) - - - def _parse_check(self, check): - fun_match = self._func_re.match(check) - if fun_match: - fun_name = fun_match.group(1) - arg_string = fun_match.group(2) - arg_match = self._matchfinder.match(arg_string) - if arg_match is None: - # Bad syntax - raise VdtParamError('Bad syntax in check "%s".' % check) - fun_args = [] - fun_kwargs = {} - # pull out args of group 2 - for arg in self._paramfinder.findall(arg_string): - # args may need whitespace removing (before removing quotes) - arg = arg.strip() - listmatch = self._list_arg.match(arg) - if listmatch: - key, val = self._list_handle(listmatch) - fun_kwargs[key] = val - continue - keymatch = self._key_arg.match(arg) - if keymatch: - val = keymatch.group(2) - if not val in ("'None'", '"None"'): - # Special case a quoted None - val = self._unquote(val) - fun_kwargs[keymatch.group(1)] = val - continue - - fun_args.append(self._unquote(arg)) - else: - # allows for function names without (args) - return check, (), {}, None - - # Default must be deleted if the value is specified too, - # otherwise the check function will get a spurious "default" keyword arg - default = fun_kwargs.pop('default', None) - return fun_name, fun_args, fun_kwargs, default - - - def _unquote(self, val): - """Unquote a value if necessary.""" - if (len(val) >= 2) and (val[0] in ("'", '"')) and (val[0] == val[-1]): - val = val[1:-1] - return val - - - def _list_handle(self, listmatch): - """Take apart a ``keyword=list('val, 'val')`` type string.""" - out = [] - name = listmatch.group(1) - args = listmatch.group(2) - for arg in self._list_members.findall(args): - out.append(self._unquote(arg)) - return name, out - - - def _pass(self, value): - """ - Dummy check that always passes - - >>> vtor.check('', 0) - 0 - >>> vtor.check('', '0') - '0' - """ - return value - - - def get_default_value(self, check): - """ - Given a check, return the default value for the check - (converted to the right type). - - If the check doesn't specify a default value then a - ``KeyError`` will be raised. - """ - fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check) - if default is None: - raise KeyError('Check "%s" has no default value.' % check) - value = self._handle_none(default) - if value is None: - return value - return self._check_value(value, fun_name, fun_args, fun_kwargs) - - -def _is_num_param(names, values, to_float=False): - """ - Return numbers from inputs or raise VdtParamError. - - Lets ``None`` pass through. - Pass in keyword argument ``to_float=True`` to - use float for the conversion rather than int. - - >>> _is_num_param(('', ''), (0, 1.0)) - [0, 1] - >>> _is_num_param(('', ''), (0, 1.0), to_float=True) - [0.0, 1.0] - >>> _is_num_param(('a'), ('a')) - Traceback (most recent call last): - VdtParamError: passed an incorrect value "a" for parameter "a". - """ - fun = to_float and float or int - out_params = [] - for (name, val) in zip(names, values): - if val is None: - out_params.append(val) - elif isinstance(val, (int, long, float, string_type)): - try: - out_params.append(fun(val)) - except ValueError as e: - raise VdtParamError(name, val) - else: - raise VdtParamError(name, val) - return out_params - - -# built in checks -# you can override these by setting the appropriate name -# in Validator.functions -# note: if the params are specified wrongly in your input string, -# you will also raise errors. - -def is_integer(value, min=None, max=None): - """ - A check that tests that a given value is an integer (int, or long) - and optionally, between bounds. A negative value is accepted, while - a float will fail. - - If the value is a string, then the conversion is done - if possible. - Otherwise a VdtError is raised. - - >>> vtor.check('integer', '-1') - -1 - >>> vtor.check('integer', '0') - 0 - >>> vtor.check('integer', 9) - 9 - >>> vtor.check('integer', 'a') - Traceback (most recent call last): - VdtTypeError: the value "a" is of the wrong type. - >>> vtor.check('integer', '2.2') - Traceback (most recent call last): - VdtTypeError: the value "2.2" is of the wrong type. - >>> vtor.check('integer(10)', '20') - 20 - >>> vtor.check('integer(max=20)', '15') - 15 - >>> vtor.check('integer(10)', '9') - Traceback (most recent call last): - VdtValueTooSmallError: the value "9" is too small. - >>> vtor.check('integer(10)', 9) - Traceback (most recent call last): - VdtValueTooSmallError: the value "9" is too small. - >>> vtor.check('integer(max=20)', '35') - Traceback (most recent call last): - VdtValueTooBigError: the value "35" is too big. - >>> vtor.check('integer(max=20)', 35) - Traceback (most recent call last): - VdtValueTooBigError: the value "35" is too big. - >>> vtor.check('integer(0, 9)', False) - 0 - """ - (min_val, max_val) = _is_num_param(('min', 'max'), (min, max)) - if not isinstance(value, (int, long, string_type)): - raise VdtTypeError(value) - if isinstance(value, string_type): - # if it's a string - does it represent an integer ? - try: - value = int(value) - except ValueError: - raise VdtTypeError(value) - if (min_val is not None) and (value < min_val): - raise VdtValueTooSmallError(value) - if (max_val is not None) and (value > max_val): - raise VdtValueTooBigError(value) - return value - - -def is_float(value, min=None, max=None): - """ - A check that tests that a given value is a float - (an integer will be accepted), and optionally - that it is between bounds. - - If the value is a string, then the conversion is done - if possible. - Otherwise a VdtError is raised. - - This can accept negative values. - - >>> vtor.check('float', '2') - 2.0 - - From now on we multiply the value to avoid comparing decimals - - >>> vtor.check('float', '-6.8') * 10 - -68.0 - >>> vtor.check('float', '12.2') * 10 - 122.0 - >>> vtor.check('float', 8.4) * 10 - 84.0 - >>> vtor.check('float', 'a') - Traceback (most recent call last): - VdtTypeError: the value "a" is of the wrong type. - >>> vtor.check('float(10.1)', '10.2') * 10 - 102.0 - >>> vtor.check('float(max=20.2)', '15.1') * 10 - 151.0 - >>> vtor.check('float(10.0)', '9.0') - Traceback (most recent call last): - VdtValueTooSmallError: the value "9.0" is too small. - >>> vtor.check('float(max=20.0)', '35.0') - Traceback (most recent call last): - VdtValueTooBigError: the value "35.0" is too big. - """ - (min_val, max_val) = _is_num_param( - ('min', 'max'), (min, max), to_float=True) - if not isinstance(value, (int, long, float, string_type)): - raise VdtTypeError(value) - if not isinstance(value, float): - # if it's a string - does it represent a float ? - try: - value = float(value) - except ValueError: - raise VdtTypeError(value) - if (min_val is not None) and (value < min_val): - raise VdtValueTooSmallError(value) - if (max_val is not None) and (value > max_val): - raise VdtValueTooBigError(value) - return value - - -bool_dict = { - True: True, 'on': True, '1': True, 'true': True, 'yes': True, - False: False, 'off': False, '0': False, 'false': False, 'no': False, -} - - -def is_boolean(value): - """ - Check if the value represents a boolean. - - >>> vtor.check('boolean', 0) - 0 - >>> vtor.check('boolean', False) - 0 - >>> vtor.check('boolean', '0') - 0 - >>> vtor.check('boolean', 'off') - 0 - >>> vtor.check('boolean', 'false') - 0 - >>> vtor.check('boolean', 'no') - 0 - >>> vtor.check('boolean', 'nO') - 0 - >>> vtor.check('boolean', 'NO') - 0 - >>> vtor.check('boolean', 1) - 1 - >>> vtor.check('boolean', True) - 1 - >>> vtor.check('boolean', '1') - 1 - >>> vtor.check('boolean', 'on') - 1 - >>> vtor.check('boolean', 'true') - 1 - >>> vtor.check('boolean', 'yes') - 1 - >>> vtor.check('boolean', 'Yes') - 1 - >>> vtor.check('boolean', 'YES') - 1 - >>> vtor.check('boolean', '') - Traceback (most recent call last): - VdtTypeError: the value "" is of the wrong type. - >>> vtor.check('boolean', 'up') - Traceback (most recent call last): - VdtTypeError: the value "up" is of the wrong type. - - """ - if isinstance(value, string_type): - try: - return bool_dict[value.lower()] - except KeyError: - raise VdtTypeError(value) - # we do an equality test rather than an identity test - # this ensures Python 2.2 compatibilty - # and allows 0 and 1 to represent True and False - if value == False: - return False - elif value == True: - return True - else: - raise VdtTypeError(value) - - -def is_ip_addr(value): - """ - Check that the supplied value is an Internet Protocol address, v.4, - represented by a dotted-quad string, i.e. '1.2.3.4'. - - >>> vtor.check('ip_addr', '1 ') - '1' - >>> vtor.check('ip_addr', ' 1.2') - '1.2' - >>> vtor.check('ip_addr', ' 1.2.3 ') - '1.2.3' - >>> vtor.check('ip_addr', '1.2.3.4') - '1.2.3.4' - >>> vtor.check('ip_addr', '0.0.0.0') - '0.0.0.0' - >>> vtor.check('ip_addr', '255.255.255.255') - '255.255.255.255' - >>> vtor.check('ip_addr', '255.255.255.256') - Traceback (most recent call last): - VdtValueError: the value "255.255.255.256" is unacceptable. - >>> vtor.check('ip_addr', '1.2.3.4.5') - Traceback (most recent call last): - VdtValueError: the value "1.2.3.4.5" is unacceptable. - >>> vtor.check('ip_addr', 0) - Traceback (most recent call last): - VdtTypeError: the value "0" is of the wrong type. - """ - if not isinstance(value, string_type): - raise VdtTypeError(value) - value = value.strip() - try: - dottedQuadToNum(value) - except ValueError: - raise VdtValueError(value) - return value - - -def is_list(value, min=None, max=None): - """ - Check that the value is a list of values. - - You can optionally specify the minimum and maximum number of members. - - It does no check on list members. - - >>> vtor.check('list', ()) - [] - >>> vtor.check('list', []) - [] - >>> vtor.check('list', (1, 2)) - [1, 2] - >>> vtor.check('list', [1, 2]) - [1, 2] - >>> vtor.check('list(3)', (1, 2)) - Traceback (most recent call last): - VdtValueTooShortError: the value "(1, 2)" is too short. - >>> vtor.check('list(max=5)', (1, 2, 3, 4, 5, 6)) - Traceback (most recent call last): - VdtValueTooLongError: the value "(1, 2, 3, 4, 5, 6)" is too long. - >>> vtor.check('list(min=3, max=5)', (1, 2, 3, 4)) - [1, 2, 3, 4] - >>> vtor.check('list', 0) - Traceback (most recent call last): - VdtTypeError: the value "0" is of the wrong type. - >>> vtor.check('list', '12') - Traceback (most recent call last): - VdtTypeError: the value "12" is of the wrong type. - """ - (min_len, max_len) = _is_num_param(('min', 'max'), (min, max)) - if isinstance(value, string_type): - raise VdtTypeError(value) - try: - num_members = len(value) - except TypeError: - raise VdtTypeError(value) - if min_len is not None and num_members < min_len: - raise VdtValueTooShortError(value) - if max_len is not None and num_members > max_len: - raise VdtValueTooLongError(value) - return list(value) - - -def is_tuple(value, min=None, max=None): - """ - Check that the value is a tuple of values. - - You can optionally specify the minimum and maximum number of members. - - It does no check on members. - - >>> vtor.check('tuple', ()) - () - >>> vtor.check('tuple', []) - () - >>> vtor.check('tuple', (1, 2)) - (1, 2) - >>> vtor.check('tuple', [1, 2]) - (1, 2) - >>> vtor.check('tuple(3)', (1, 2)) - Traceback (most recent call last): - VdtValueTooShortError: the value "(1, 2)" is too short. - >>> vtor.check('tuple(max=5)', (1, 2, 3, 4, 5, 6)) - Traceback (most recent call last): - VdtValueTooLongError: the value "(1, 2, 3, 4, 5, 6)" is too long. - >>> vtor.check('tuple(min=3, max=5)', (1, 2, 3, 4)) - (1, 2, 3, 4) - >>> vtor.check('tuple', 0) - Traceback (most recent call last): - VdtTypeError: the value "0" is of the wrong type. - >>> vtor.check('tuple', '12') - Traceback (most recent call last): - VdtTypeError: the value "12" is of the wrong type. - """ - return tuple(is_list(value, min, max)) - - -def is_string(value, min=None, max=None): - """ - Check that the supplied value is a string. - - You can optionally specify the minimum and maximum number of members. - - >>> vtor.check('string', '0') - '0' - >>> vtor.check('string', 0) - Traceback (most recent call last): - VdtTypeError: the value "0" is of the wrong type. - >>> vtor.check('string(2)', '12') - '12' - >>> vtor.check('string(2)', '1') - Traceback (most recent call last): - VdtValueTooShortError: the value "1" is too short. - >>> vtor.check('string(min=2, max=3)', '123') - '123' - >>> vtor.check('string(min=2, max=3)', '1234') - Traceback (most recent call last): - VdtValueTooLongError: the value "1234" is too long. - """ - if not isinstance(value, string_type): - raise VdtTypeError(value) - (min_len, max_len) = _is_num_param(('min', 'max'), (min, max)) - try: - num_members = len(value) - except TypeError: - raise VdtTypeError(value) - if min_len is not None and num_members < min_len: - raise VdtValueTooShortError(value) - if max_len is not None and num_members > max_len: - raise VdtValueTooLongError(value) - return value - - -def is_int_list(value, min=None, max=None): - """ - Check that the value is a list of integers. - - You can optionally specify the minimum and maximum number of members. - - Each list member is checked that it is an integer. - - >>> vtor.check('int_list', ()) - [] - >>> vtor.check('int_list', []) - [] - >>> vtor.check('int_list', (1, 2)) - [1, 2] - >>> vtor.check('int_list', [1, 2]) - [1, 2] - >>> vtor.check('int_list', [1, 'a']) - Traceback (most recent call last): - VdtTypeError: the value "a" is of the wrong type. - """ - return [is_integer(mem) for mem in is_list(value, min, max)] - - -def is_bool_list(value, min=None, max=None): - """ - Check that the value is a list of booleans. - - You can optionally specify the minimum and maximum number of members. - - Each list member is checked that it is a boolean. - - >>> vtor.check('bool_list', ()) - [] - >>> vtor.check('bool_list', []) - [] - >>> check_res = vtor.check('bool_list', (True, False)) - >>> check_res == [True, False] - 1 - >>> check_res = vtor.check('bool_list', [True, False]) - >>> check_res == [True, False] - 1 - >>> vtor.check('bool_list', [True, 'a']) - Traceback (most recent call last): - VdtTypeError: the value "a" is of the wrong type. - """ - return [is_boolean(mem) for mem in is_list(value, min, max)] - - -def is_float_list(value, min=None, max=None): - """ - Check that the value is a list of floats. - - You can optionally specify the minimum and maximum number of members. - - Each list member is checked that it is a float. - - >>> vtor.check('float_list', ()) - [] - >>> vtor.check('float_list', []) - [] - >>> vtor.check('float_list', (1, 2.0)) - [1.0, 2.0] - >>> vtor.check('float_list', [1, 2.0]) - [1.0, 2.0] - >>> vtor.check('float_list', [1, 'a']) - Traceback (most recent call last): - VdtTypeError: the value "a" is of the wrong type. - """ - return [is_float(mem) for mem in is_list(value, min, max)] - - -def is_string_list(value, min=None, max=None): - """ - Check that the value is a list of strings. - - You can optionally specify the minimum and maximum number of members. - - Each list member is checked that it is a string. - - >>> vtor.check('string_list', ()) - [] - >>> vtor.check('string_list', []) - [] - >>> vtor.check('string_list', ('a', 'b')) - ['a', 'b'] - >>> vtor.check('string_list', ['a', 1]) - Traceback (most recent call last): - VdtTypeError: the value "1" is of the wrong type. - >>> vtor.check('string_list', 'hello') - Traceback (most recent call last): - VdtTypeError: the value "hello" is of the wrong type. - """ - if isinstance(value, string_type): - raise VdtTypeError(value) - return [is_string(mem) for mem in is_list(value, min, max)] - - -def is_ip_addr_list(value, min=None, max=None): - """ - Check that the value is a list of IP addresses. - - You can optionally specify the minimum and maximum number of members. - - Each list member is checked that it is an IP address. - - >>> vtor.check('ip_addr_list', ()) - [] - >>> vtor.check('ip_addr_list', []) - [] - >>> vtor.check('ip_addr_list', ('1.2.3.4', '5.6.7.8')) - ['1.2.3.4', '5.6.7.8'] - >>> vtor.check('ip_addr_list', ['a']) - Traceback (most recent call last): - VdtValueError: the value "a" is unacceptable. - """ - return [is_ip_addr(mem) for mem in is_list(value, min, max)] - - -def force_list(value, min=None, max=None): - """ - Check that a value is a list, coercing strings into - a list with one member. Useful where users forget the - trailing comma that turns a single value into a list. - - You can optionally specify the minimum and maximum number of members. - A minumum of greater than one will fail if the user only supplies a - string. - - >>> vtor.check('force_list', ()) - [] - >>> vtor.check('force_list', []) - [] - >>> vtor.check('force_list', 'hello') - ['hello'] - """ - if not isinstance(value, (list, tuple)): - value = [value] - return is_list(value, min, max) - - - -fun_dict = { - 'integer': is_integer, - 'float': is_float, - 'ip_addr': is_ip_addr, - 'string': is_string, - 'boolean': is_boolean, -} - - -def is_mixed_list(value, *args): - """ - Check that the value is a list. - Allow specifying the type of each member. - Work on lists of specific lengths. - - You specify each member as a positional argument specifying type - - Each type should be one of the following strings : - 'integer', 'float', 'ip_addr', 'string', 'boolean' - - So you can specify a list of two strings, followed by - two integers as : - - mixed_list('string', 'string', 'integer', 'integer') - - The length of the list must match the number of positional - arguments you supply. - - >>> mix_str = "mixed_list('integer', 'float', 'ip_addr', 'string', 'boolean')" - >>> check_res = vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a', True)) - >>> check_res == [1, 2.0, '1.2.3.4', 'a', True] - 1 - >>> check_res = vtor.check(mix_str, ('1', '2.0', '1.2.3.4', 'a', 'True')) - >>> check_res == [1, 2.0, '1.2.3.4', 'a', True] - 1 - >>> vtor.check(mix_str, ('b', 2.0, '1.2.3.4', 'a', True)) - Traceback (most recent call last): - VdtTypeError: the value "b" is of the wrong type. - >>> vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a')) - Traceback (most recent call last): - VdtValueTooShortError: the value "(1, 2.0, '1.2.3.4', 'a')" is too short. - >>> vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a', 1, 'b')) - Traceback (most recent call last): - VdtValueTooLongError: the value "(1, 2.0, '1.2.3.4', 'a', 1, 'b')" is too long. - >>> vtor.check(mix_str, 0) - Traceback (most recent call last): - VdtTypeError: the value "0" is of the wrong type. - - >>> vtor.check('mixed_list("yoda")', ('a')) - Traceback (most recent call last): - VdtParamError: passed an incorrect value "KeyError('yoda',)" for parameter "'mixed_list'" - """ - try: - length = len(value) - except TypeError: - raise VdtTypeError(value) - if length < len(args): - raise VdtValueTooShortError(value) - elif length > len(args): - raise VdtValueTooLongError(value) - try: - return [fun_dict[arg](val) for arg, val in zip(args, value)] - except KeyError as e: - raise VdtParamError('mixed_list', e) - - -def is_option(value, *options): - """ - This check matches the value to any of a set of options. - - >>> vtor.check('option("yoda", "jedi")', 'yoda') - 'yoda' - >>> vtor.check('option("yoda", "jedi")', 'jed') - Traceback (most recent call last): - VdtValueError: the value "jed" is unacceptable. - >>> vtor.check('option("yoda", "jedi")', 0) - Traceback (most recent call last): - VdtTypeError: the value "0" is of the wrong type. - """ - if not isinstance(value, string_type): - raise VdtTypeError(value) - if not value in options: - raise VdtValueError(value) - return value - - -def _test(value, *args, **keywargs): - """ - A function that exists for test purposes. - - >>> checks = [ - ... '3, 6, min=1, max=3, test=list(a, b, c)', - ... '3', - ... '3, 6', - ... '3,', - ... 'min=1, test="a b c"', - ... 'min=5, test="a, b, c"', - ... 'min=1, max=3, test="a, b, c"', - ... 'min=-100, test=-99', - ... 'min=1, max=3', - ... '3, 6, test="36"', - ... '3, 6, test="a, b, c"', - ... '3, max=3, test=list("a", "b", "c")', - ... '''3, max=3, test=list("'a'", 'b', "x=(c)")''', - ... "test='x=fish(3)'", - ... ] - >>> v = Validator({'test': _test}) - >>> for entry in checks: - ... pprint(v.check(('test(%s)' % entry), 3)) - (3, ('3', '6'), {'max': '3', 'min': '1', 'test': ['a', 'b', 'c']}) - (3, ('3',), {}) - (3, ('3', '6'), {}) - (3, ('3',), {}) - (3, (), {'min': '1', 'test': 'a b c'}) - (3, (), {'min': '5', 'test': 'a, b, c'}) - (3, (), {'max': '3', 'min': '1', 'test': 'a, b, c'}) - (3, (), {'min': '-100', 'test': '-99'}) - (3, (), {'max': '3', 'min': '1'}) - (3, ('3', '6'), {'test': '36'}) - (3, ('3', '6'), {'test': 'a, b, c'}) - (3, ('3',), {'max': '3', 'test': ['a', 'b', 'c']}) - (3, ('3',), {'max': '3', 'test': ["'a'", 'b', 'x=(c)']}) - (3, (), {'test': 'x=fish(3)'}) - - >>> v = Validator() - >>> v.check('integer(default=6)', '3') - 3 - >>> v.check('integer(default=6)', None, True) - 6 - >>> v.get_default_value('integer(default=6)') - 6 - >>> v.get_default_value('float(default=6)') - 6.0 - >>> v.get_default_value('pass(default=None)') - >>> v.get_default_value("string(default='None')") - 'None' - >>> v.get_default_value('pass') - Traceback (most recent call last): - KeyError: 'Check "pass" has no default value.' - >>> v.get_default_value('pass(default=list(1, 2, 3, 4))') - ['1', '2', '3', '4'] - - >>> v = Validator() - >>> v.check("pass(default=None)", None, True) - >>> v.check("pass(default='None')", None, True) - 'None' - >>> v.check('pass(default="None")', None, True) - 'None' - >>> v.check('pass(default=list(1, 2, 3, 4))', None, True) - ['1', '2', '3', '4'] - - Bug test for unicode arguments - >>> v = Validator() - >>> v.check(unicode('string(min=4)'), unicode('test')) == unicode('test') - True - - >>> v = Validator() - >>> v.get_default_value(unicode('string(min=4, default="1234")')) == unicode('1234') - True - >>> v.check(unicode('string(min=4, default="1234")'), unicode('test')) == unicode('test') - True - - >>> v = Validator() - >>> default = v.get_default_value('string(default=None)') - >>> default == None - 1 - """ - return (value, args, keywargs) - - -def _test2(): - """ - >>> - >>> v = Validator() - >>> v.get_default_value('string(default="#ff00dd")') - '#ff00dd' - >>> v.get_default_value('integer(default=3) # comment') - 3 - """ - -def _test3(): - r""" - >>> vtor.check('string(default="")', '', missing=True) - '' - >>> vtor.check('string(default="\n")', '', missing=True) - '\n' - >>> print(vtor.check('string(default="\n")', '', missing=True)) - - - >>> vtor.check('string()', '\n') - '\n' - >>> vtor.check('string(default="\n\n\n")', '', missing=True) - '\n\n\n' - >>> vtor.check('string()', 'random \n text goes here\n\n') - 'random \n text goes here\n\n' - >>> vtor.check('string(default=" \nrandom text\ngoes \n here\n\n ")', - ... '', missing=True) - ' \nrandom text\ngoes \n here\n\n ' - >>> vtor.check("string(default='\n\n\n')", '', missing=True) - '\n\n\n' - >>> vtor.check("option('\n','a','b',default='\n')", '', missing=True) - '\n' - >>> vtor.check("string_list()", ['foo', '\n', 'bar']) - ['foo', '\n', 'bar'] - >>> vtor.check("string_list(default=list('\n'))", '', missing=True) - ['\n'] - """ - - -if __name__ == '__main__': - # run the code tests in doctest format - import sys - import doctest - m = sys.modules.get('__main__') - globs = m.__dict__.copy() - globs.update({ - 'vtor': Validator(), - }) - - failures, tests = doctest.testmod( - m, globs=globs, - optionflags=doctest.IGNORE_EXCEPTION_DETAIL | doctest.ELLIPSIS) - assert not failures, '{} failures out of {} tests'.format(failures, tests)