diff --git a/django_tables2/columns/base.py b/django_tables2/columns/base.py index 2c2a26e9..8a2796ab 100644 --- a/django_tables2/columns/base.py +++ b/django_tables2/columns/base.py @@ -1,5 +1,6 @@ from collections import OrderedDict from itertools import islice +from urllib.parse import urlencode from django.core.exceptions import ImproperlyConfigured from django.urls import reverse @@ -65,7 +66,9 @@ class LinkTransform: accessor = None attrs = None - def __init__(self, url=None, accessor=None, attrs=None, reverse_args=None): + def __init__( + self, url=None, accessor=None, attrs=None, reverse_args=None, query=None, fragment=None + ): """ arguments: url (callable): If supplied, the result of this callable will be used as ``href`` attribute. @@ -75,10 +78,14 @@ def __init__(self, url=None, accessor=None, attrs=None, reverse_args=None): reverse_args (dict, tuple): Arguments to ``django.urls.reverse()``. If dict, the arguments are assumed to be keyword arguments to ``reverse()``, if tuple, a ``(viewname, args)`` or ``(viewname, kwargs)`` + query (dict): If supplied, field-value pairs to be formatted into a query string. + fragment (str): If supplied, value of URL fragment identifier (hash). """ self.url = url self.attrs = attrs self.accessor = accessor + self.query = query + self.fragment = fragment if isinstance(reverse_args, (list, tuple)): viewname, args = reverse_args @@ -95,23 +102,37 @@ def compose_url(self, **kwargs): record = kwargs["record"] if self.reverse_args.get("viewname", None) is not None: - return self.call_reverse(record=record) - - if bound_column is None and self.accessor is None: - accessor = Accessor("") + url = self.call_reverse(record=record) else: - accessor = Accessor(self.accessor if self.accessor is not None else bound_column.name) - context = accessor.resolve(record) - if not hasattr(context, "get_absolute_url"): - if hasattr(record, "get_absolute_url"): - context = record + if bound_column is None and self.accessor is None: + accessor = Accessor("") else: - raise TypeError( - "for linkify=True, '{}' must have a method get_absolute_url".format( - str(context) - ) + accessor = Accessor( + self.accessor if self.accessor is not None else bound_column.name ) - return context.get_absolute_url() + context = accessor.resolve(record) + if not hasattr(context, "get_absolute_url"): + if hasattr(record, "get_absolute_url"): + context = record + else: + raise TypeError( + "for linkify=True, '{}' must have a method get_absolute_url".format( + str(context) + ) + ) + url = context.get_absolute_url() + + if self.query: + url += "?" + urlencode( + { + a: v.resolve(record) if isinstance(v, Accessor) else v + for a, v in self.query.items() + } + ) + if self.fragment: + url += "#" + self.fragment + + return url def call_reverse(self, record): """ @@ -150,6 +171,20 @@ def __call__(self, content, **kwargs): return format_html("{}", attrs.as_html(), content) + @classmethod + def get_callback(cls, *args, **kwargs): + """ + This method constructs a LinkTransform and returns a callback function suitable as the linkify + parameter of ``Column.__init__()`` This may be used if you wish to subclass LinkTransform or to access + (future) features of LinkTransform which may not be exposed via ``linkify``. + """ + link_transform = cls(*args, **kwargs) + + def callback(record, value, **kwargs): + return link_transform.compose_url(record=record, value=value, **kwargs) + + return callback + @library.register class Column: @@ -207,9 +242,11 @@ class Column: ``a`` tag. The different ways to define the ``href`` attribute: - If `True`, the ``record.get_absolute_url()`` or the related model's - `get_absolute_url()` is used. - - If a callable is passed, the returned value is used, if it's not ``None``. - - If a `dict` is passed, it's passed on to ``~django.urls.reverse``. + ``get_absolute_url()`` is used. + - If a callable is passed, the returned value is used, if it's not `None`. + - If a `dict` is passed, optional items named `query` (dict), and `fragment` (str), + if present, specify the query string and fragment (hash) elements of the URL. + The remaining items are passed on to ``~django.urls.reverse`` as kwargs. - If a `tuple` is passed, it must be either a (viewname, args) or (viewname, kwargs) tuple, which is also passed to ``~django.urls.reverse``. @@ -295,8 +332,14 @@ def __init__( link_kwargs = None if callable(linkify) or hasattr(self, "get_url"): link_kwargs = dict(url=linkify if callable(linkify) else self.get_url) - elif isinstance(linkify, (dict, tuple)): + elif isinstance(linkify, (list, tuple)): link_kwargs = dict(reverse_args=linkify) + elif isinstance(linkify, dict): + # specific keys in linkify are understood to be link_kwargs, and the rest must be reverse_args + link_kwargs = { + name: linkify.pop(name) for name in ("query", "fragment") if name in linkify + } + link_kwargs["reverse_args"] = linkify elif linkify is True: link_kwargs = dict(accessor=self.accessor) diff --git a/django_tables2/columns/linkcolumn.py b/django_tables2/columns/linkcolumn.py index adf58762..c508cbd8 100644 --- a/django_tables2/columns/linkcolumn.py +++ b/django_tables2/columns/linkcolumn.py @@ -67,6 +67,8 @@ class LinkColumn(BaseLinkColumn): text (str or callable): Either static text, or a callable. If set, this will be used to render the text inside link instead of value (default). The callable gets the record being rendered as argument. + query (dict): Optional name/value pair for query string. See `~urllib.urlencode`. + fragment (str): Optional URL Fragment Identifier (hash). .. [2] In order to create a link to a URL that relies on information in the current row, `.Accessor` objects can be used in the *args* or *kwargs* @@ -130,6 +132,8 @@ def __init__( kwargs=None, current_app=None, attrs=None, + query=None, + fragment=None, **extra ): super().__init__( @@ -140,6 +144,8 @@ def __init__( args=args, kwargs=kwargs, current_app=current_app, + query=query, + fragment=fragment, ), **extra ) diff --git a/tests/columns/test_linkcolumn.py b/tests/columns/test_linkcolumn.py index cb7d010e..f463d58a 100644 --- a/tests/columns/test_linkcolumn.py +++ b/tests/columns/test_linkcolumn.py @@ -1,3 +1,6 @@ +import re +from urllib.parse import parse_qsl, urlparse + from django.template import Context, Template from django.test import TestCase from django.urls import reverse @@ -228,3 +231,25 @@ class Table(tables.Table): table = Table([{"col": "link-text", "id": 1}]) self.assertEqual(table.rows[0].get_cell_value("col"), "link-text") + + def test_query(self): + query = {"a": A("a"), "b": "baker", "c": "12"} + + class PersonTable(tables.Table): + a = tables.LinkColumn("occupation", kwargs={"pk": A("a")}, query=query) + + table = PersonTable([{"a": 0}, {"a": 1}]) + + for a in range(1): + href = re.search('href="(.*)"', table.rows[a].get_cell("a")).group(1) + query["a"] = str(a) + self.assertEqual(query, dict(parse_qsl(urlparse(href).query))) + + def test_fragment(self): + class PersonTable(tables.Table): + a = tables.LinkColumn("occupation", kwargs={"pk": A("a")}, fragment="fragval") + + table = PersonTable([{"a": 0}, {"a": 1}]) + + href = re.search('href="(.*)"', table.rows[0].get_cell("a")).group(1) + self.assertEqual("fragval", urlparse(href).fragment)