Search code examples
pythondjangokeyerror

Django-tables2 KeyError when field has {curly brackets}


I have a model with Artist and Track class and when the title field of a Track has curly brackets, I get a KeyError Exception. (E.g. Track Title "Beat It {Freestyle}"). Looks like django_tables2 is passing my Track title field value to string.format(). How do I prevent that in django_tables2?

models.py

class Artist (models.Model):
    name = models.CharField(max_length=100)
    slug = AutoSlugField(populate_from='name', unique=True, editable=True, slugify=custom_slugify)


class Track (models.Model):    
    artist = models.ForeignKey(Artist, blank=True, null=True, on_delete=models.SET_NULL, verbose_name="Artist")
    user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, verbose_name="Submitted by", default=1)
    title = models.CharField(max_length=100, verbose_name="Title")
    timestamp = models.DateTimeField(default=timezone.now)

tables.py

class TrackTable(tables.Table):
    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user', None)
        self.artistlink = kwargs.pop('artistlink', None)
        super(TrackTable, self).__init__(*args, **kwargs)

def render_artist(self, value):
    if self.artistlink == True:
        return format_html('<a href="/music/by/{}">{}</a>', value.slug, value)
    else:
        return value

def render_user(self, value):
    return format_html('<a href="/users/{}">{}</a>', value, value)

download = DownloadColumn(empty_values=(), orderable=False) 

track_id = tables.CheckBoxColumn(accessor="pk",
                            attrs = {"th__input": {"id": "checkAll"}},
                            orderable=False)

class Meta:
    model = Track
    attrs = {"class": "table table-bordered table-condensed table-striped", "id": "track-table",}
    fields = ('track_id', 'artist', 'title', 'user', 'download')

views.py

def profile_detail(request, slug=None):
    alpha_list = ['0-9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']          

    playlist_list = Playlist.objects.filter(user=request.user.id)

    track_list = Track.objects.filter(artist__slug=slug)
    ltr = slug[0].upper()
    artist = get_object_or_404(Artist, slug=slug)

    table = TrackTable(track_list, user=request.user, artistlink=False)

    RequestConfig(request, paginate={"per_page": 15}).configure(table)  

    context = {
        "playlist_list": playlist_list,
        "track_list": track_list,
        "table": table,
        "alpha_list": alpha_list,
        "artist": artist,
    }
    return render(request, "profile_detail.html", context)

profile_detail.html

<form method='POST' id="add-track-form" action='{% url "playlists:playlist_addtrack" %}'>{% csrf_token %}
...
        {% render_table table "django_tables2/bootstrap3.html" %}
...
</form>

bootstrap3.html

% load querystring from django_tables2 %}
{% load trans blocktrans from i18n %}
{% load bootstrap3 %}

{% if table.page %}
    <div class="table-responsive">
{% endif %}

{% block table %}
    <table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
        {% block table.thead %}
            <thead>
            <tr>
                {% for column in table.columns %}
                    {% if column.orderable %}
                        <th {{ column.attrs.th.as_html }}><a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a></th>
                    {% else %}
                        <th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
                    {% endif %}
                {% endfor %}
            </tr>
            </thead>
        {% endblock table.thead %}
        {% block table.tbody %}
            <tbody>
            {% for row in table.page.object_list|default:table.rows %} {# support pagination #}
                {% block table.tbody.row %}
                    <tr class="{% cycle "odd" "even" %}">
                        {% for column, cell in row.items %}
                            <td {{ column.attrs.td.as_html }}>{{ cell }}</td>
                        {% endfor %}
                    </tr>
                {% endblock table.tbody.row %}
            {% empty %}
                {% if table.empty_text %}
                    {% block table.tbody.empty_text %}
                        <tr><td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr>
                    {% endblock table.tbody.empty_text %}
                {% endif %}
            {% endfor %}
            </tbody>
        {% endblock table.tbody %}
        {% block table.tfoot %}
            <tfoot></tfoot>
        {% endblock table.tfoot %}
    </table>
    </div> <!-- table-responsive -->
{% endblock table %}

traceback:

Template error:
In template /Users/will/bpmdata/bpmdb/templates/django_tables2/bootstrap3.html, error at line 29
   Freestyle   19 :                     {% endif %}
   20 :                 {% endfor %}
   21 :             </tr>
   22 :             </thead>
   23 :         {% endblock table.thead %}
   24 :         {% block table.tbody %}
   25 :             <tbody>
   26 :             {% for row in table.page.object_list|default:table.rows %} {# support pagination #}
   27 :                 {% block table.tbody.row %}
   28 :                     <tr class="{% cycle "odd" "even" %}">
   29 :                          {% for column, cell in row.items %} 
   30 :                             <td {{ column.attrs.td.as_html }}>{{ cell }}</td>
   31 :                         {% endfor %}
   32 :                     </tr>
   33 :                 {% endblock table.tbody.row %}
   34 :             {% empty %}
   35 :                 {% if table.empty_text %}
   36 :                     {% block table.tbody.empty_text %}
   37 :                         <tr><td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr>
   38 :                     {% endblock table.tbody.empty_text %}
   39 :                 {% endif %}

Traceback:

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/core/handlers/exception.py" in inner
  41.             response = get_response(request)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/core/handlers/base.py" in _legacy_get_response
  249.             response = self._get_response(request)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/core/handlers/base.py" in _get_response
  187.                 response = self.process_exception_by_middleware(e, request)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/core/handlers/base.py" in _get_response
  185.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/Users/will/bpmdata/bpmdb/profiles/views.py" in profile_detail
  136.  return render(request, "profile_detail.html", context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/shortcuts.py" in render
  30.     content = loader.render_to_string(template_name, context, request, using=using)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/loader.py" in render_to_string
  68.     return template.render(context, request)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/backends/django.py" in render
  66.             return self.template.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render
  207.                     return self._render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in _render
  199.         return self.nodelist.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render
  990.                 bit = node.render_annotated(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render_annotated
  957.             return self.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/loader_tags.py" in render
  177.             return compiled_parent._render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in _render
  199.         return self.nodelist.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render
  990.                 bit = node.render_annotated(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render_annotated
  957.             return self.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/loader_tags.py" in render
  72.                 result = block.nodelist.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render
  990.                 bit = node.render_annotated(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render_annotated
  957.             return self.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/defaulttags.py" in render
  322.                 return nodelist.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render
  990.                 bit = node.render_annotated(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render_annotated
  957.             return self.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django_tables2/templatetags/django_tables2.py" in render
  154.             return template.render(context.flatten())

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/backends/django.py" in render
  66.             return self.template.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render
  207.                     return self._render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in _render
  199.         return self.nodelist.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render
  990.                 bit = node.render_annotated(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render_annotated
  957.             return self.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/loader_tags.py" in render
  63.                 result = self.nodelist.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render
  990.                 bit = node.render_annotated(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render_annotated
  957.             return self.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/loader_tags.py" in render
  63.                 result = self.nodelist.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render
  990.                 bit = node.render_annotated(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render_annotated
  957.             return self.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/defaulttags.py" in render
  216.                     nodelist.append(node.render_annotated(context))

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render_annotated
  957.             return self.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/loader_tags.py" in render
  63.                 result = self.nodelist.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render
  990.                 bit = node.render_annotated(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/base.py" in render_annotated
  957.             return self.render(context)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/template/defaulttags.py" in render
  172.                 values = list(values)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django_tables2/rows.py" in items
  229.             yield (column, self.get_cell(column.name))

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django_tables2/rows.py" in get_cell
  180.             default=self.table.columns[name].default

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django_tables2/rows.py" in _get_and_render_with
  155.         return render_func(bound_column, value)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django_tables2/rows.py" in _call_render
  189.             self._optional_cell_arguments(bound_column, value)

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django_tables2/utils.py" in call_with_appropriate
  511.     return fn(**kwargs)

File "/Users/will/bpmdata/bpmdb/profiles/tables.py" in render
  42.           return format_html(referral_url, artist=artist1, title=record.title) 

File "/Users/will/.virtualenvs/bpmdb/lib/python3.5/site-packages/django/utils/html.py" in format_html
  100.     return mark_safe(format_string.format(*args_safe, **kwargs_safe))

Exception Type: KeyError at /music/by/michael-jackson/
Exception Value: 'Freestyle'

Solution

  • Considering that curly braces in strings cannot pass silently when str.format is called, except you control how the formatting is done (which you can't here), you can either replace curly braces with another character like a double parens ((..)) or double up the braces in anticipation of str.format; the latter is however more fragile since the field value is dependent on a later call to str.format:

    >>> '{{Freestyle}}'.format()
    '{Freestyle}'