The template functions for the new custom column search template feature. There are 4 new functions:

* make_url(path, [query_name, query_value]+). This is the easiest to use.
* make_url_extended(...). This gives the user more control over constructing the URL, including user-built query strings.
* query_string([query_name, query_value, how_to_encode]+). Constructs a query string, giving more control over how the values are encoded.
* encode_for_url(value, use_plus). URL-encodes a single value.

As you said earlier, most people will use make_url(). However, I have seen cases where query values must be inserted into the path, and make_url_extended() helps with that. I have also seen cases where query args must not be encoded. And so on. These 4 functions plus other text functions like re() let the user do whatever is necessary.

Note that 'item_value' is no longer encoded. There are two more values available: 'item_value_quoted' and 'item_value_no_plus'. I'm not convinced these are particularly useful but it doesn't hurt anything to have them.
This commit is contained in:
Charles Haley 2025-02-11 19:21:19 +00:00
parent 3be9172c51
commit 33af676b11
3 changed files with 232 additions and 18 deletions

View File

@ -70,7 +70,8 @@ def search_action_with_data(search_term, value, book_id, field=None, **k):
def web_search_link(template, mi, value): def web_search_link(template, mi, value):
formatter = SafeFormat() formatter = SafeFormat()
iv = str(value) iv = str(value)
mi.set('item_value', qquote(iv, True)) mi.set('item_value', iv)
mi.set('item_value_quoted', qquote(iv, True))
mi.set('item_value_no_plus', qquote(iv, False)) mi.set('item_value_no_plus', qquote(iv, False))
u = formatter.safe_format(template, mi, 'BOOK DETAILS WEB LINK', mi) u = formatter.safe_format(template, mi, 'BOOK DETAILS WEB LINK', mi)
if u: if u:

View File

@ -542,12 +542,17 @@ class CreateCustomColumn(QDialog):
l.addWidget(self.web_search_label) l.addWidget(self.web_search_label)
wst = self.web_search_template = QLineEdit() wst = self.web_search_template = QLineEdit()
wst.setToolTip('<p>' + _( wst.setToolTip('<p>' + _(
'Fill in this box if you want clicking on the value in book details to do a ' "Fill in this box if you want clicking on the value in book details to do a "
"web search instead of searching your calibre library. The book's metadata is " "web search instead of searching your calibre library. The book's metadata is "
"available to the template. Additional fields '{0}' and '{1}' are also available to the " "available to the template.</p><p>Additional fields '{0}', `{1}`, and '{2}' are also available "
'template. For multiple-valued (tags-like) columns they are the value being examined, ' "to the template. For multiple-valued (tags-like) columns they are the value being examined, "
'telling you which value to use to generate the link. These two values are automatically escaped for use in URLs.').format( "telling you which value to use to generate the link. The two values '{1}' and '{2}' are "
'item_value', 'item_value_no_plus') + '</p>') "automatically escaped for use in URLs. In '{1}', spaces are replaced by plus signs. In '{2}' "
"spaces are replaced by '%20'.</p><p> The template functions '{3}' (the easiest to use), "
"'{4}', '{5}', and '{6}' are useful for constructing the desired URL. There are examples in "
"the template function documentation.").format(
'item_value', 'item_value_quoted', 'item_value_no_plus', 'make_url()', 'make_url_extended()',
'query_string()', 'quote_for_url()') + '</p>')
l.addWidget(wst) l.addWidget(wst)
self.web_search_label.setBuddy(wst) self.web_search_label.setBuddy(wst)
wst_tb = self.web_search_toolbutton = QToolButton() wst_tb = self.web_search_toolbutton = QToolButton()
@ -563,17 +568,21 @@ class CreateCustomColumn(QDialog):
db = self.gui.current_db.new_api db = self.gui.current_db.new_api
lv = self.gui.library_view lv = self.gui.library_view
rows = lv.selectionModel().selectedRows() rows = lv.selectionModel().selectedRows()
from calibre.ebooks.metadata.search_internet import qquote
if not self.editing_col or not rows: if not self.editing_col or not rows:
vals = [{'value': _('Value'), 'lookup_name': _('Lookup name'), 'author': _('Author'), vals = [{'item_value': _('Item Value'),
'item_value_quoted': qquote(_('Item Value'), True),
'item_value_no_plus': qquote(_('Item Value'), False),
'lookup_name': _('Lookup name'),'author': _('Author'),
'title': _('Title'), 'author_sort': _('Author sort')}] 'title': _('Title'), 'author_sort': _('Author sort')}]
else: else:
from calibre.ebooks.metadata.search_internet import qquote
vals = [] vals = []
for row in rows: for row in rows:
book_id = lv.model().id(row) book_id = lv.model().id(row)
mi = db.new_api.get_metadata(book_id) mi = db.new_api.get_metadata(book_id)
mi.set('item_value', qquote('Item Value', True)) mi.set('item_value', _('Item Value'))
mi.set('item_value_no_plus', qquote('Item Value', False)) mi.set('item_value_quoted', qquote(_('Item Value'), True))
mi.set('item_value_no_plus', qquote(_('Item Value'), False))
vals.append(mi) vals.append(mi)
d = TemplateDialog(parent=self, text=self.web_search_template.text(), mi=vals) d = TemplateDialog(parent=self, text=self.web_search_template.text(), mi=vals)
if d.exec() == QDialog.DialogCode.Accepted: if d.exec() == QDialog.DialogCode.Accepted:
@ -682,8 +691,7 @@ class CreateCustomColumn(QDialog):
self.comments_type.setVisible(is_comments) self.comments_type.setVisible(is_comments)
self.comments_type_label.setVisible(is_comments) self.comments_type_label.setVisible(is_comments)
has_url_template = not is_comments and col_type in ('text', '*text', 'composite', '*composite', has_url_template = col_type in ('text', '*text', 'composite', '*composite', 'series', 'enumeration')
'series', 'enumeration')
self.web_search_label.setVisible(has_url_template) self.web_search_label.setVisible(has_url_template)
self.web_search_template.setVisible(has_url_template) self.web_search_template.setVisible(has_url_template)
self.web_search_toolbutton.setVisible(has_url_template) self.web_search_toolbutton.setVisible(has_url_template)
@ -962,21 +970,26 @@ class CreateNewCustomColumn:
'make_category': True or False -- whether the column is shown in the tag browser 'make_category': True or False -- whether the column is shown in the tag browser
'contains_html': True or False -- whether the column is interpreted as HTML 'contains_html': True or False -- whether the column is interpreted as HTML
'use_decorations': True or False -- should check marks be displayed 'use_decorations': True or False -- should check marks be displayed
'search_template': a template used to construct a search URL for book details
datetime columns: datetime columns:
'date_format': a string specifying the display format 'date_format': a string specifying the display format
enumerated columns enumerated columns
'enum_values': a string containing comma-separated valid values for an enumeration 'enum_values': a string containing comma-separated valid values for an enumeration
'enum_colors': a string containing comma-separated colors for an enumeration 'enum_colors': a string containing comma-separated colors for an enumeration
'use_decorations': True or False -- should check marks be displayed 'use_decorations': True or False -- should check marks be displayed
'search_template': a template used to construct a search URL for book details
float columns: float columns:
'decimals': the number of decimal digits to allow when editing (int). Range: 1 - 9 'decimals': the number of decimal digits to allow when editing (int). Range: 1 - 9
float and int columns: float and int columns:
'number_format': the format to apply when displaying the column 'number_format': the format to apply when displaying the column
rating columns: rating columns:
'allow_half_stars': True or False -- are half-stars allowed 'allow_half_stars': True or False -- are half-stars allowed
series columns:
'search_template': a template used to construct a search URL for book details
text columns: text columns:
'is_names': True or False -- whether the items are comma or ampersand separated 'is_names': True or False -- whether the items are comma or ampersand separated
'use_decorations': True or False -- should check marks be displayed 'use_decorations': True or False -- should check marks be displayed
'search_template': a template used to construct a search URL for book details
This method returns a tuple (Result.enum_value, message). If tuple[0] is This method returns a tuple (Result.enum_value, message). If tuple[0] is
Result.COLUMN_ADDED then the message is the lookup name including the '#'. Result.COLUMN_ADDED then the message is the lookup name including the '#'.

View File

@ -30,6 +30,7 @@ from calibre.db.constants import DATA_DIR_NAME, DATA_FILE_PATTERN
from calibre.db.notes.exim import expand_note_resources, parse_html from calibre.db.notes.exim import expand_note_resources, parse_html
from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata import title_sort
from calibre.ebooks.metadata.book.base import field_metadata from calibre.ebooks.metadata.book.base import field_metadata
from calibre.ebooks.metadata.search_internet import qquote
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre.utils.date import UNDEFINED_DATE, format_date, now, parse_date from calibre.utils.date import UNDEFINED_DATE, format_date, now, parse_date
from calibre.utils.icu import capitalize, sort_key, strcmp from calibre.utils.icu import capitalize, sort_key, strcmp
@ -55,6 +56,7 @@ FORMATTING_VALUES = _('Formatting values')
CASE_CHANGES = _('Case changes') CASE_CHANGES = _('Case changes')
DATE_FUNCTIONS = _('Date functions') DATE_FUNCTIONS = _('Date functions')
DB_FUNCS = _('Database functions') DB_FUNCS = _('Database functions')
URL_FUNCTIONS = _('URL functions')
# Class and method to save an untranslated copy of translated strings # Class and method to save an untranslated copy of translated strings
@ -2780,7 +2782,7 @@ of templates.
class BuiltinToHex(BuiltinFormatterFunction): class BuiltinToHex(BuiltinFormatterFunction):
name = 'to_hex' name = 'to_hex'
arg_count = 1 arg_count = 1
category = STRING_MANIPULATION category = URL_FUNCTIONS
__doc__ = doc = _( __doc__ = doc = _(
r''' r'''
``to_hex(val)`` -- returns the string ``val`` encoded into hex.[/] This is useful ``to_hex(val)`` -- returns the string ``val`` encoded into hex.[/] This is useful
@ -2794,7 +2796,7 @@ when constructing calibre URLs.
class BuiltinUrlsFromIdentifiers(BuiltinFormatterFunction): class BuiltinUrlsFromIdentifiers(BuiltinFormatterFunction):
name = 'urls_from_identifiers' name = 'urls_from_identifiers'
arg_count = 2 arg_count = 2
category = FORMATTING_VALUES category = URL_FUNCTIONS
__doc__ = doc = _( __doc__ = doc = _(
r''' r'''
``urls_from_identifiers(identifiers, sort_results)`` -- given a comma-separated ``urls_from_identifiers(identifiers, sort_results)`` -- given a comma-separated
@ -3213,6 +3215,203 @@ data without converting it to a string first. Example: ``list_count_field('tags'
raise NotImplementedError() raise NotImplementedError()
class BuiltinMakeUrl(BuiltinFormatterFunction):
name = 'make_url'
arg_count = -1
category = URL_FUNCTIONS
__doc__ = doc = _(
r'''
``make_url(path, [query_name, query_value]+)`` -- this function is the easiest way
to construct a query URL. It uses a ``path``, the web site and page you want to
query, and ``query_name``, ``query_value`` pairs from which the query is built.
In general, the ``query_value`` must be URL-encoded. With this function it is always
encoded and spaces are always replaced with ``'+'`` signs.
At least one ``query_name, query_value`` pair must be provided.
Example: constructing a Wikipedia search URL for the author `Niccolò Machiavelli`:
[CODE]
make_url('https://en.wikipedia.org/w/index.php', 'search', 'Niccolò Machiavelli')
[/CODE]
returns
[CODE]
https://en.wikipedia.org/w/index.php?search=Niccol%C3%B2+Machiavelli
[/CODE]
If you are writing a custom column book details URL template then use ``$item_name`` or
``field('item_name')`` to obtain the value of the field that was clicked on.
Example: if `Niccolò Machiavelli` was clicked then you can construct the URL using:
[CODE]
make_url('https://en.wikipedia.org/w/index.php', 'search', $item_name)
[/CODE]
See also the functions :ref:`make_url_extended`, :ref:`query_string` and :ref:`encode_for_url`.
''')
def evaluate(self, formatter, kwargs, mi, locals, path, *args):
if (len(args) % 2) != 0:
raise ValueError(_('{} requires an odd number of arguments').format('make_url'))
if len(args) < 2:
raise ValueError(_('{} requires at least 3 arguments').format('make_url'))
query_args = []
for i in range(0, len(args), 2):
query_args.append(f'{args[i]}={qquote(args[i+1].strip())}')
return f'{path}?{"&".join(query_args)}'
class BuiltinMakeUrlExtended(BuiltinFormatterFunction):
name = 'make_url_extended'
arg_count = -1
category = URL_FUNCTIONS
__doc__ = doc = _(
r'''
``make_url_extended(...)`` -- this function is similar to :ref:`make_url` but
gives you more control over the URL components. The components of a URL are
[B]scheme[/B]:://[B]authority[/B]/[B]path[/B]?[B]query string[/B].
See [URL href="https://en.wikipedia.org/wiki/URL"]Uniform Resource Locater[/URL] on Wikipedia for more detail.
The function has two variants:
[CODE]
make_url_extended(scheme, authority, path, [query_name, query_value]+)
[/CODE]
and
[CODE]
make_url_extended(scheme, authority, path, query_string)
[/CODE]
[/]
This function returns a URL constructed from the ``scheme``, ``authority``, ``path``,
and either the ``query_string`` or a query string constructed from the query argument pairs.
You must supply either a ``query_string`` or at least one ``query_name, query_value`` pair.
If you supply ``query_string`` and it is empty then the resulting URL will not have a query string section.
Example 1: constructing a Wikipedia search URL for the author `Niccolò Machiavelli`:
[CODE]
make_url_extended('https', 'en.wikipedia.org', '/w/index.php', 'search', 'Niccolò Machiavelli')
[/CODE]
returns
[CODE]
https://en.wikipedia.org/w/index.php?search=Niccol%C3%B2+Machiavelli
[/CODE]
See the :ref:`query_string`() function for an example using ``make_url_extended()`` with a ``query_string``.
If you are writing a custom column book details URL template then use ``$item_name`` or
``field('item_name')`` to obtain the value of the field that was clicked on.
Example: if `Niccolò Machiavelli` was clicked on then you can construct the URL using :
[CODE]
make_url_extended('https', 'en.wikipedia.org', '/w/index.php', 'search', $item_name')
[/CODE]
See also the functions :ref:`make_url`, :ref:`query_string` and :ref:`encode_for_url`.
''')
def evaluate(self, formatter, kwargs, mi, locals, scheme, host, path, *args):
if len(args) != 1:
if (len(args) % 2) != 0:
raise ValueError(_('{} requires an odd number of arguments').format('make_url_extended'))
if len(args) < 2:
raise ValueError(_('{} requires at least 5 arguments').format('make_url_extended'))
query_args = []
for i in range(0, len(args), 2):
query_args.append(f'{args[i]}={qquote(args[i+1].strip())}')
qs = '&'.join(query_args)
else:
qs = args[0]
if qs:
qs = '?' + qs
return f"{scheme}://{host}/{path[1:] if path.startswith('/') else path}{qs}"
class BuiltinQueryString(BuiltinFormatterFunction):
name = 'query_string'
arg_count = -1
category = URL_FUNCTIONS
__doc__ = doc = _(
r'''
``query_string([query_name, query_value, how_to_encode]+)``-- returns a URL query string
constructed from the ``query_name, query_value, how_to_encode`` triads.
A query string is a series of items where each item looks like ``query_name=query_value``
where ``query_value`` is URL-encoded as instructed. The query items are separated by
``'&'`` (ampersand) characters.
If ``how_to_encode`` is ``0`` then ``query_value`` is encoded and spaces are replaced
with ``'+'`` (plus) signs. If ``how_to_encode`` is ``1`` then ``query_value`` is
encoded with spaces replaced by ``%20``. If ``how_to_encode`` is ``2`` then ``query_value``
is returned unchanged; no encoding is done and spaces are not replaced. If you want
``query_value`` not to be encoded but spaces to be replaced then use the :ref:`re`
function, as in ``re($series, ' ', '%20')``
You use this function if you need specific control over how the parts of the
query string are constructed. You could then use the resultingquery string in
:ref:`make_url_extended`, as in
[CODE]
make_url_extended(
'https', 'your_host', 'your_path',
query_string('encoded', 'Hendrik Bäßler', 0, 'unencoded', 'Hendrik Bäßler', 2))
[/CODE]
giving you
[CODE]
https://your_host/your_path?encoded=Hendrik+B%C3%A4%C3%9Fler&unencoded=Hendrik Bäßler
[/CODE]
You must have at least one ``query_name, query_value, how_to_encode`` triad, but can
have as many as you wish.
The returned value is a URL query string with all the specified items, for example:
``name1=val1[&nameN=valN]*``. Note that the ``'?'`` `path` / `query string` separator
is not included in the returned result.
If you are writing a custom column book details URL template then use ``$item_name`` or
``field('item_name')`` to obtain the unencoded value of the field that was clicked.
You also have ``item_value_quoted`` where the value is already encoded with plus signs
replacing spaces, and ``item_value_no_plus`` where the value is already encoded
with ``%20`` replacing spaces.
See also the functions :ref:`make_url`, :ref:`make_url_extended` and :ref:`encode_for_url`.
''')
def evaluate(self, formatter, kwargs, mi, locals, *args):
if (len(args) % 3) != 0 or len(args) < 3:
raise ValueError(_('{} requires at least one group of 3 arguments').format('query_string'))
funcs = [
partial(qquote, use_plus=True),
partial(qquote, use_plus=False),
lambda x:x,
]
query_args = []
for i in range(0, len(args), 3):
if (f := args[i+2]) not in ('0', '1', '2'):
raise ValueError(
_('In {} the third argument of a group must be 0, 1, or 2, not {}').format('query_string', f))
query_args.append(f'{args[i]}={funcs[int(f)](args[i+1].strip())}')
return "&".join(query_args)
class BuiltinEncodeForURL(BuiltinFormatterFunction):
name = 'encode_for_url'
arg_count = 2
category = URL_FUNCTIONS
__doc__ = doc = _(
r'''
``encode_for_url(value, use_plus)`` -- returns the ``value`` encoded for use in a URL as
specified by ``use_plus``. The value is first URL-encoded. Next, if ``use_plus`` is ``0`` then
spaces are replaced by ``'+'`` (plus) signs. If it is ``1`` then spaces are replaced by ``%20``.
If you do not want the value to be encoding but to have spaces replaced then use the
:ref:`re` function, as in ``re($series, ' ', '%20')``
See also the functions :ref:`make_url`, :ref:`make_url_extended` and :ref:`query_string`.
''')
def evaluate(self, formatter, kwargs, mi, locals, value, use_plus):
if use_plus not in ('0', '1'):
raise ValueError(
_('In {} the second argument must be 0, or 1, not {}').format('quote_for_url', use_plus))
return qquote(value, use_plus=use_plus=='0')
_formatter_builtins = [ _formatter_builtins = [
BuiltinAdd(), BuiltinAnd(), BuiltinApproximateFormats(), BuiltinArguments(), BuiltinAdd(), BuiltinAnd(), BuiltinApproximateFormats(), BuiltinArguments(),
BuiltinAssign(), BuiltinAssign(),
@ -3222,7 +3421,7 @@ _formatter_builtins = [
BuiltinCmp(), BuiltinConnectedDeviceName(), BuiltinConnectedDeviceUUID(), BuiltinContains(), BuiltinCmp(), BuiltinConnectedDeviceName(), BuiltinConnectedDeviceUUID(), BuiltinContains(),
BuiltinCount(), BuiltinCurrentLibraryName(), BuiltinCurrentLibraryPath(), BuiltinCount(), BuiltinCurrentLibraryName(), BuiltinCurrentLibraryPath(),
BuiltinCurrentVirtualLibraryName(), BuiltinDateArithmetic(), BuiltinCurrentVirtualLibraryName(), BuiltinDateArithmetic(),
BuiltinDaysBetween(), BuiltinDivide(), BuiltinEval(), BuiltinDaysBetween(), BuiltinDivide(), BuiltinEncodeForURL(), BuiltinEval(),
BuiltinExtraFileNames(), BuiltinExtraFileSize(), BuiltinExtraFileModtime(), BuiltinExtraFileNames(), BuiltinExtraFileSize(), BuiltinExtraFileModtime(),
BuiltinFieldListCount(), BuiltinFirstNonEmpty(), BuiltinField(), BuiltinFieldExists(), BuiltinFieldListCount(), BuiltinFirstNonEmpty(), BuiltinField(), BuiltinFieldExists(),
BuiltinFinishFormatting(), BuiltinFirstMatchingCmp(), BuiltinFloor(), BuiltinFinishFormatting(), BuiltinFirstMatchingCmp(), BuiltinFloor(),
@ -3237,9 +3436,10 @@ _formatter_builtins = [
BuiltinListitem(), BuiltinListJoin(), BuiltinListRe(), BuiltinListitem(), BuiltinListJoin(), BuiltinListRe(),
BuiltinListReGroup(), BuiltinListRemoveDuplicates(), BuiltinListSort(), BuiltinListReGroup(), BuiltinListRemoveDuplicates(), BuiltinListSort(),
BuiltinListSplit(), BuiltinListUnion(),BuiltinLookup(), BuiltinListSplit(), BuiltinListUnion(),BuiltinLookup(),
BuiltinLowercase(), BuiltinMod(), BuiltinMultiply(), BuiltinNot(), BuiltinOndevice(), BuiltinLowercase(), BuiltinMakeUrl(), BuiltinMakeUrlExtended(), BuiltinMod(),
BuiltinOr(), BuiltinPrint(), BuiltinRatingToStars(), BuiltinRange(), BuiltinMultiply(), BuiltinNot(), BuiltinOndevice(),
BuiltinRawField(), BuiltinRawList(), BuiltinOr(), BuiltinPrint(), BuiltinQueryString(), BuiltinRatingToStars(),
BuiltinRange(), BuiltinRawField(), BuiltinRawList(),
BuiltinRe(), BuiltinReGroup(), BuiltinRound(), BuiltinSelect(), BuiltinSeriesSort(), BuiltinRe(), BuiltinReGroup(), BuiltinRound(), BuiltinSelect(), BuiltinSeriesSort(),
BuiltinSetGlobals(), BuiltinShorten(), BuiltinStrcat(), BuiltinStrcatMax(), BuiltinSetGlobals(), BuiltinShorten(), BuiltinStrcat(), BuiltinStrcatMax(),
BuiltinStrcmp(), BuiltinStrcmpcase(), BuiltinStrInList(), BuiltinStrlen(), BuiltinSubitems(), BuiltinStrcmp(), BuiltinStrcmpcase(), BuiltinStrInList(), BuiltinStrlen(), BuiltinSubitems(),