This commit is contained in:
cbartondock 2021-05-13 11:25:59 -04:00
commit e0fac8d2c0
51 changed files with 4280 additions and 3929 deletions

View File

@ -59,7 +59,7 @@ except ImportError:
log = logger.create() log = logger.create()
cc_exceptions = ['datetime', 'comments', 'composite', 'series'] cc_exceptions = ['composite', 'series']
cc_classes = {} cc_classes = {}
Base = declarative_base() Base = declarative_base()
@ -473,7 +473,7 @@ class CalibreDB():
} }
books_custom_column_links[row.id] = type(str('books_custom_column_' + str(row.id) + '_link'), books_custom_column_links[row.id] = type(str('books_custom_column_' + str(row.id) + '_link'),
(Base,), dicttable) (Base,), dicttable)
else: if row.datatype in ['rating', 'text', 'enumeration']:
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link',
Base.metadata, Base.metadata,
Column('book', Integer, ForeignKey('books.id'), Column('book', Integer, ForeignKey('books.id'),
@ -491,23 +491,25 @@ class CalibreDB():
ccdict['value'] = Column(Float) ccdict['value'] = Column(Float)
elif row.datatype == 'int': elif row.datatype == 'int':
ccdict['value'] = Column(Integer) ccdict['value'] = Column(Integer)
elif row.datatype == 'datetime':
ccdict['value'] = Column(TIMESTAMP)
elif row.datatype == 'bool': elif row.datatype == 'bool':
ccdict['value'] = Column(Boolean) ccdict['value'] = Column(Boolean)
else: else:
ccdict['value'] = Column(String) ccdict['value'] = Column(String)
if row.datatype in ['float', 'int', 'bool']: if row.datatype in ['float', 'int', 'bool', 'datetime', 'comments']:
ccdict['book'] = Column(Integer, ForeignKey('books.id')) ccdict['book'] = Column(Integer, ForeignKey('books.id'))
cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict) cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict)
for cc_id in cc_ids: for cc_id in cc_ids:
if (cc_id[1] == 'bool') or (cc_id[1] == 'int') or (cc_id[1] == 'float'): if cc_id[1] in ['bool', 'int', 'float', 'datetime', 'comments']:
setattr(Books, setattr(Books,
'custom_column_' + str(cc_id[0]), 'custom_column_' + str(cc_id[0]),
relationship(cc_classes[cc_id[0]], relationship(cc_classes[cc_id[0]],
primaryjoin=( primaryjoin=(
Books.id == cc_classes[cc_id[0]].book), Books.id == cc_classes[cc_id[0]].book),
backref='books')) backref='books'))
elif (cc_id[1] == 'series'): elif cc_id[1] == 'series':
setattr(Books, setattr(Books,
'custom_column_' + str(cc_id[0]), 'custom_column_' + str(cc_id[0]),
relationship(books_custom_column_links[cc_id[0]], relationship(books_custom_column_links[cc_id[0]],

View File

@ -504,12 +504,17 @@ def edit_book_publisher(publishers, book):
return changed return changed
def edit_cc_data_number(book_id, book, c, to_save, cc_db_value, cc_string): def edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string):
changed = False changed = False
if to_save[cc_string] == 'None': if to_save[cc_string] == 'None':
to_save[cc_string] = None to_save[cc_string] = None
elif c.datatype == 'bool': elif c.datatype == 'bool':
to_save[cc_string] = 1 if to_save[cc_string] == 'True' else 0 to_save[cc_string] = 1 if to_save[cc_string] == 'True' else 0
elif c.datatype == 'datetime':
try:
to_save[cc_string] = datetime.strptime(to_save[cc_string], "%Y-%m-%d")
except ValueError:
to_save[cc_string] = db.Books.DEFAULT_PUBDATE
if to_save[cc_string] != cc_db_value: if to_save[cc_string] != cc_db_value:
if cc_db_value is not None: if cc_db_value is not None:
@ -568,8 +573,8 @@ def edit_cc_data(book_id, book, to_save):
else: else:
cc_db_value = None cc_db_value = None
if to_save[cc_string].strip(): if to_save[cc_string].strip():
if c.datatype == 'int' or c.datatype == 'bool' or c.datatype == 'float': if c.datatype in ['int', 'bool', 'float', "datetime", "comments"]:
changed, to_save = edit_cc_data_number(book_id, book, c, to_save, cc_db_value, cc_string) changed, to_save = edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string)
else: else:
changed, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string) changed, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string)
else: else:

View File

@ -158,6 +158,7 @@ def HandleSyncRequest():
.filter(db.Books.last_modified >= sync_token.books_last_modified) .filter(db.Books.last_modified >= sync_token.books_last_modified)
.filter(db.Books.id>sync_token.books_last_id) .filter(db.Books.id>sync_token.books_last_id)
.filter(db.Data.format.in_(KOBO_FORMATS)) .filter(db.Data.format.in_(KOBO_FORMATS))
.filter(calibre_db.common_filters())
.order_by(db.Books.last_modified) .order_by(db.Books.last_modified)
.order_by(db.Books.id) .order_by(db.Books.id)
.limit(SYNC_ITEM_LIMIT) .limit(SYNC_ITEM_LIMIT)
@ -168,6 +169,7 @@ def HandleSyncRequest():
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
.filter(db.Books.last_modified > sync_token.books_last_modified) .filter(db.Books.last_modified > sync_token.books_last_modified)
.filter(db.Data.format.in_(KOBO_FORMATS)) .filter(db.Data.format.in_(KOBO_FORMATS))
.filter(calibre_db.common_filters())
.order_by(db.Books.last_modified) .order_by(db.Books.last_modified)
.order_by(db.Books.id) .order_by(db.Books.id)
.limit(SYNC_ITEM_LIMIT) .limit(SYNC_ITEM_LIMIT)

View File

@ -10,8 +10,19 @@ if ($("#description").length) {
menubar: "edit view format", menubar: "edit view format",
language: language language: language
}); });
}
if (!Modernizr.inputtypes.date) { if ($(".tiny_editor").length) {
tinymce.init({
selector: ".tiny_editor",
branding: false,
menubar: "edit view format",
language: language
});
}
tiny_editor
if (!Modernizr.inputtypes.date) {
$("#pubdate").datepicker({ $("#pubdate").datepicker({
format: "yyyy-mm-dd", format: "yyyy-mm-dd",
language: language language: language
@ -26,9 +37,9 @@ if ($("#description").length) {
.removeClass("hidden"); .removeClass("hidden");
} }
}).trigger("change"); }).trigger("change");
}
} }
if (!Modernizr.inputtypes.date) { if (!Modernizr.inputtypes.date) {
$("#Publishstart").datepicker({ $("#Publishstart").datepicker({
format: "yyyy-mm-dd", format: "yyyy-mm-dd",
@ -63,6 +74,7 @@ if (!Modernizr.inputtypes.date) {
}).trigger("change"); }).trigger("change");
} }
/* /*
Takes a prefix, query typeahead callback, Bloodhound typeahead adapter Takes a prefix, query typeahead callback, Bloodhound typeahead adapter
and returns the completions it gets from the bloodhound engine prefixed. and returns the completions it gets from the bloodhound engine prefixed.
@ -78,11 +90,6 @@ function prefixedSource(prefix, query, cb, bhAdapter) {
}); });
} }
/*function getPath() {
var jsFileLocation = $("script[src*=edit_books]").attr("src"); // the js file path
return jsFileLocation.substr(0, jsFileLocation.search("/static/js/edit_books.js")); // the js folder path
}*/
var authors = new Bloodhound({ var authors = new Bloodhound({
name: "authors", name: "authors",
datumTokenizer: function datumTokenizer(datum) { datumTokenizer: function datumTokenizer(datum) {

View File

@ -533,7 +533,7 @@ $(function() {
$("#pub_new").toggleClass("disabled"); $("#pub_new").toggleClass("disabled");
$("#pub_old").toggleClass("disabled"); $("#pub_old").toggleClass("disabled");
var alternative_text = $("#toggle_order_shelf").data('alt-text'); var alternative_text = $("#toggle_order_shelf").data('alt-text');
$("#toggle_order_shelf")[0].attributes['data-alt-text'].value = $("#toggle_order_shelf").html(); $("#toggle_order_shelf").data('alt-text', $("#toggle_order_shelf").html());
$("#toggle_order_shelf").html(alternative_text); $("#toggle_order_shelf").html(alternative_text);
}); });

View File

@ -149,7 +149,22 @@
{% endif %}> {% endif %}>
{% endif %} {% endif %}
{% if c.datatype == 'datetime' %}
<div style="position: relative">
<input type="date" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}"
{% if book['custom_column_' ~ c.id]|length > 0 %}
value="{% if book['custom_column_' ~ c.id][0].value %}{{ book['custom_column_' ~ c.id][0].value|formatdateinput}}{% endif %}"
{% endif %}>
<input type="text" class="fake_custom_column_{{ c.id }} form-control fake-input hidden "
{% if book['custom_column_' ~ c.id]|length > 0 %}
value="{% if book['custom_column_' ~ c.id][0].value %}{{book['custom_column_' ~ c.id][0].value|formatdate}}{% endif %}"
{% endif %}>
</div>
{% endif %}
{% if c.datatype == 'comments' %}
<textarea class="form-control tiny_editor" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" rows="7">{% if book['custom_column_' ~ c.id]|length > 0 %}{{book['custom_column_' ~ c.id][0].value}}{%endif%}</textarea>
{% endif %}
{% if c.datatype == 'enumeration' %} {% if c.datatype == 'enumeration' %}
<select class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}"> <select class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}">
<option></option> <option></option>

View File

@ -100,7 +100,7 @@
<h2 id="title">{{entry.title}}</h2> <h2 id="title">{{entry.title}}</h2>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a> <a href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a>
{% if not loop.last %} {% if not loop.last %}
&amp; &amp;
{% endif %} {% endif %}
@ -122,7 +122,7 @@
{% endif %} {% endif %}
{% if entry.series|length > 0 %} {% if entry.series|length > 0 %}
<p>{{_('Book')}} {{entry.series_index}} {{_('of')}} <a href="{{url_for('web.books_list', data='series', sort_param='abc', book_id=entry.series[0].id)}}">{{entry.series[0].name}}</a></p> <p>{{_('Book')}} {{entry.series_index}} {{_('of')}} <a href="{{url_for('web.books_list', data='series', sort_param='stored', book_id=entry.series[0].id)}}">{{entry.series[0].name}}</a></p>
{% endif %} {% endif %}
{% if entry.languages.__len__() > 0 %} {% if entry.languages.__len__() > 0 %}
@ -151,7 +151,7 @@
<span class="glyphicon glyphicon-tags"></span> <span class="glyphicon glyphicon-tags"></span>
{% for tag in entry.tags %} {% for tag in entry.tags %}
<a href="{{ url_for('web.books_list', data='category', sort_param='new', book_id=tag.id) }}" class="btn btn-xs btn-info" role="button">{{tag.name}}</a> <a href="{{ url_for('web.books_list', data='category', sort_param='stored', book_id=tag.id) }}" class="btn btn-xs btn-info" role="button">{{tag.name}}</a>
{%endfor%} {%endfor%}
</p> </p>
@ -162,7 +162,7 @@
<div class="publishers"> <div class="publishers">
<p> <p>
<span>{{_('Publisher')}}: <span>{{_('Publisher')}}:
<a href="{{url_for('web.books_list', data='publisher', sort_param='new', book_id=entry.publishers[0].id ) }}">{{entry.publishers[0].name}}</a> <a href="{{url_for('web.books_list', data='publisher', sort_param='stored', book_id=entry.publishers[0].id ) }}">{{entry.publishers[0].name}}</a>
</span> </span>
</p> </p>
</div> </div>
@ -193,15 +193,17 @@
{% else %} {% else %}
{% if c.datatype == 'float' %} {% if c.datatype == 'float' %}
{{ column.value|formatfloat(2) }} {{ column.value|formatfloat(2) }}
{% else %} {% elif c.datatype == 'datetime' %}
{% if c.datatype == 'series' %} {{ column.value|formatdate }}
{% elif c.datatype == 'comments' %}
{{column.value|safe}}
{% elif c.datatype == 'series' %}
{{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }} {{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }}
{% else %} {% else %}
{{ column.value }} {{ column.value }}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>

View File

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

View File

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

View File

@ -2,7 +2,7 @@
{% block body %} {% block body %}
<div class="discover"> <div class="discover">
{% if entries|length < 1 %} {% if entries|length < 1 %}
<h2>{{_('No Results Found')}} {{adv_searchterm}}</h2> <h2>{{_('No Results Found')}}</h2>
<p>{{_('Search Term:')}} {{adv_searchterm}}</p> <p>{{_('Search Term:')}} {{adv_searchterm}}</p>
{% else %} {% else %}
<h2>{{result_count}} {{_('Results for:')}} {{adv_searchterm}}</h2> <h2>{{result_count}} {{_('Results for:')}} {{adv_searchterm}}</h2>

View File

@ -167,7 +167,26 @@
<input type="number" step="0.01" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" value=""> <input type="number" step="0.01" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" value="">
{% endif %} {% endif %}
{% if c.datatype in ['text', 'series'] and not c.is_multiple %} {% if c.datatype == 'datetime' %}
<div class="row">
<div class="form-group col-sm-6">
<label for="{{ 'custom_column_' ~ c.id }}">{{_('From:')}}</label>
<div style="position: relative">
<input type="date" class="form-control" name="{{ 'custom_column_' ~ c.id }}_start" id="{{ 'custom_column_' ~ c.id }}_start" value="">
<input type="text" class="form-control fake-input hidden" id="fake_{{ 'custom_column_' ~ c.id }}_start" value="">
</div>
</div>
<div class="form-group col-sm-6">
<label for="{{ 'custom_column_' ~ c.id }}">{{_('To:')}}</label>
<div style="position: relative">
<input type="date" class="form-control" name="{{ 'custom_column_' ~ c.id }}_end" id="{{ 'custom_column_' ~ c.id }}_end" value="">
<input type="text" class="form-control fake-input hidden" id="fake_{{ 'custom_column_' ~ c.id }}_end" value="">
</div>
</div>
</div>
{% endif %}
{% if c.datatype in ['text', 'series', 'comments'] and not c.is_multiple %}
<input type="text" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" value=""> <input type="text" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" value="">
{% endif %} {% endif %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1067,8 +1067,8 @@ def search():
@login_required_if_no_ano @login_required_if_no_ano
def advanced_search(): def advanced_search():
values = dict(request.form) values = dict(request.form)
params = ['include_tag', 'exclude_tag', 'include_serie', 'exclude_serie', 'include_shelf','exclude_shelf','include_language', params = ['include_tag', 'exclude_tag', 'include_serie', 'exclude_serie', 'include_shelf', 'exclude_shelf',
'exclude_language', 'include_extension', 'exclude_extension'] 'include_language', 'exclude_language', 'include_extension', 'exclude_extension']
for param in params: for param in params:
values[param] = list(request.form.getlist(param)) values[param] = list(request.form.getlist(param))
flask_session['query'] = json.dumps(values) flask_session['query'] = json.dumps(values)
@ -1077,6 +1077,16 @@ def advanced_search():
def adv_search_custom_columns(cc, term, q): def adv_search_custom_columns(cc, term, q):
for c in cc: for c in cc:
if c.datatype == "datetime":
custom_start = term.get('custom_column_' + str(c.id) + '_start')
custom_end = term.get('custom_column_' + str(c.id) + '_end')
if custom_start:
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
db.cc_classes[c.id].value >= custom_start))
if custom_end:
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
db.cc_classes[c.id].value <= custom_end))
else:
custom_query = term.get('custom_column_' + str(c.id)) custom_query = term.get('custom_column_' + str(c.id))
if custom_query != '' and custom_query is not None: if custom_query != '' and custom_query is not None:
if c.datatype == 'bool': if c.datatype == 'bool':
@ -1262,9 +1272,19 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
searchterm = [] searchterm = []
cc_present = False cc_present = False
for c in cc: for c in cc:
if term.get('custom_column_' + str(c.id)): if c.datatype == "datetime":
searchterm.extend([(u"%s: %s" % (c.name, term.get('custom_column_' + str(c.id))))]) column_start = term.get('custom_column_' + str(c.id) + '_start')
column_end = term.get('custom_column_' + str(c.id) + '_end')
if column_start:
searchterm.extend([u"{} >= {}".format(c.name, column_start)])
cc_present = True cc_present = True
if column_end:
searchterm.extend([u"{} <= {}".format(c.name, column_end)])
cc_present = True
elif term.get('custom_column_' + str(c.id)):
searchterm.extend([(u"{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
cc_present = True
if any(tags.values()) or author_name or book_title or publisher or pub_start or pub_end or rating_low \ if any(tags.values()) or author_name or book_title or publisher or pub_start or pub_end or rating_low \
or rating_high or description or cc_present or read_status: or rating_high or description or cc_present or read_status:

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,6 @@ Babel>=1.3, <2.9
Flask-Babel>=0.11.1,<2.1.0 Flask-Babel>=0.11.1,<2.1.0
Flask-Login>=0.3.2,<0.5.1 Flask-Login>=0.3.2,<0.5.1
Flask-Principal>=0.3.2,<0.5.1 Flask-Principal>=0.3.2,<0.5.1
singledispatch>=3.4.0.0,<3.5.0.0
backports_abc>=0.4 backports_abc>=0.4
Flask>=1.0.2,<1.2.0 Flask>=1.0.2,<1.2.0
iso-639>=0.4.5,<0.5.0 iso-639>=0.4.5,<0.5.0

View File

@ -38,7 +38,6 @@ install_requires =
Flask-Babel>=0.11.1,<2.1.0 Flask-Babel>=0.11.1,<2.1.0
Flask-Login>=0.3.2,<0.5.1 Flask-Login>=0.3.2,<0.5.1
Flask-Principal>=0.3.2,<0.5.1 Flask-Principal>=0.3.2,<0.5.1
singledispatch>=3.4.0.0,<3.5.0.0
backports_abc>=0.4 backports_abc>=0.4
Flask>=1.0.2,<1.2.0 Flask>=1.0.2,<1.2.0
iso-639>=0.4.5,<0.5.0 iso-639>=0.4.5,<0.5.0