Search code examples
djangopaginationserver-sidebootstrap-table

How to convert a django template to use bootstrap-table server-side pagination


I have a number of pages rendered by django templates to which I have applied bootstrap-table to implement column switching, client-side pagination, and multi-column sorting. This was after having created a fully functioning django template.

My tables are very large and each column has multiple manipulations, such as:

  • links to other pages on the site
  • number formatting
  • horizontal alignment (e.g. right-justify numbers)
  • concatenating values from related tables, delimited by various strings (e.g. comma-delimiting)
  • tooltips
  • filling in empty values with "None"
  • converting timedeltas to days or weeks ...

A number of the manipulations utilize simple_tags and filters written in python. There's even one template that uses javascript to do some custom stuff with some colspans using bootstrap table events (e.g. $("#advsrchres").bootstrapTable({onAll: ...).

And every example I look at that uses bootstrap-table's server-side pagination, there is no template and all the data is obtained using a "data-url" that returns JSON.

I'm hoping I'm wrong about this, but my assessment is that I would have to rewrite all those cell decorations in the template in javascript or something. I haven't started looking into how to do it yet, so after much fruitless googling, I'm here to see if anyone knows a way to not have to entirely rewrite those huge django templates in order to implement server-side pagination? Is there a way to tell bootstrap-table to insert the data from the JSON into the django template?

Here's a sample of one of the templates...

    <table class="table table-hover table-striped table-bordered"
        id="advsrchres"
        data-toggle="table"
        data-buttons-toolbar=".buttons-toolbar"
        data-buttons-class="primary"
        data-buttons-align="right"
        data-filter-control="false"
        data-search="false"
        data-show-search-clear-button="false"
        data-show-multi-sort="true"
        data-show-columns="true"
        data-show-columns-toggle-all="true"
        data-show-fullscreen="false"
        data-show-export="false"
        data-pagination="true">

        <colgroup span="8" class="identdata"></colgroup>
        <colgroup span="4" class="datadata"></colgroup>
        <colgroup span="12" class="metadata"></colgroup>

        <thead>
            <tr>
                <th data-valign="top" data-sortable="true" data-visible="false" data-sorter="alphanum" data-field="Animal" class="idgrp">Animal</th>
                <th data-valign="top" data-sortable="true" data-visible="true" data-sorter="alphanum" data-field="Sample" class="idgrp" data-switchable="false">Sample</th>
                <th data-valign="top" data-sortable="true" data-visible="true" data-sorter="alphanum" data-field="Tissue" class="idgrp">Tissue</th>
                <th data-valign="top" data-sortable="true" data-visible="false" data-sorter="alphanum" data-field="Peak_Group" class="idgrp">Peak Group</th>
                <th data-valign="top" data-sortable="true" data-visible="true" data-sorter="alphanum" data-field="Compound_Name" class="idgrp">Measured<br>Compound</th>
                <th data-valign="top" data-sortable="true" data-visible="false" data-sorter="alphanum" data-field="Compound_Synonym" class="idgrp">Measured<br>Compound<br>Synonym(s)</th>
                <th data-valign="top" data-sortable="true" data-visible="true" data-sorter="alphanum" data-field="Labeled_Element" class="idgrp">Labeled<br>Element</th>
                <th data-valign="top" data-sortable="true" data-visible="false" data-sorter="alphanum" data-field="Peak_Group_Set_Filename" class="idgrp">Peak Group Set Filename</th>

                <th data-valign="top" data-sortable="true" data-visible="true" data-sorter="numericOnly" data-field="Total_Abundance" class="datagrp" data-switchable="false">Total<br>Abundance</th>
                <th data-valign="top" data-sortable="true" data-visible="true" data-sorter="numericOnly" data-field="Enrichment_Fraction" class="datagrp">Enrichment<br>Fraction</th>
                <th data-valign="top" data-sortable="true" data-visible="true" data-sorter="numericOnly" data-field="Enrichment_Abundance" class="datagrp">Enrichment<br>Abundance</th>
                <th data-valign="top" data-sortable="true" data-visible="true" data-sorter="numericOnly" data-field="Normalized_Labeling" class="datagrp">Normalized<br>Labeling</th>

                <th data-valign="top" data-sortable="true" data-visible="false" data-sorter="alphanum" data-field="Formula" class="metagrp">Formula</th>
                <th data-valign="top" data-sortable="true" data-visible="true" data-sorter="alphanum" data-field="Genotype" class="metagrp">Genotype</th>
                <th data-valign="top" data-sortable="true" data-visible="false" data-sorter="alphanum" data-field="Sex" class="metagrp">Sex</th>
                <th data-valign="top" data-sortable="true" data-visible="true" data-sorter="alphanum" data-field="Feeding_Status" class="metagrp">Feeding<br>Status</th>
                <th data-valign="top" data-sortable="true" data-visible="false" data-sorter="alphanum" data-field="Diet" class="metagrp">Diet</th>
                <th data-valign="top" data-sortable="true" data-visible="true" data-sorter="alphanum" data-field="Treatment" class="metagrp">Treatment</th>
                <th data-valign="top" data-sortable="true" data-visible="false" data-sorter="numericOnly" data-field="Body_Weight" class="metagrp">Body<br>Weight<br>(g)</th>
                <th data-valign="top" data-sortable="true" data-visible="false" data-sorter="alphanum" data-field="Age" class="metagrp">Age<br>(weeks)</th>
                <th data-valign="top" data-sortable="true" data-visible="true" data-sorter="alphanum" data-field="Tracer_Compound" class="metagrp" data-switchable="false">Tracer<br>Compound</th>
                <th data-valign="top" data-sortable="true" data-visible="false" data-sorter="numericOnly" data-field="Tracer_Infusion_Rate" class="metagrp">Tracer<br>Infusion<br>Rate<br>(ul/min/g)</th>
                <th data-valign="top" data-sortable="true" data-visible="false" data-sorter="numericOnly" data-field="Tracer_Infusion_Concentration" class="metagrp">Tracer<br>Infusion<br>Concentration<br>(mM)</th>
                <th data-valign="top" data-sortable="true" data-visible="true" data-sorter="alphanum" data-field="Study" class="metagrp">Studies</th>
            </tr>
        </thead>

        <tbody>

            {% for pg in res.all %}



... SNIP ... below shows a sample of 6 of the 24 columns in this particular template 



                    <!-- Body Weight (g) -->
                    <td class="text-end">
                        {{ pg.msrun.sample.animal.body_weight }}
                    </td>

                    <!-- Age (weeks) -->
                    <td class="text-end">
                        <p title="{{ pg.msrun.sample.animal.age }} (d-hh:mm:ss)">{{ pg.msrun.sample.animal.age|durationToWeeks|decimalPlaces:2 }}</p>
                    </td>

                    <!-- Tracer Compound -->
                    <td>
                        {% if pg.msrun.sample.animal.tracer_compound is None %}
                            <!-- Put displayed link text first for sorting -->
                            <div style="display:none;">None</div>
                            <p title="Animal has no tracer.">None</p>
                        {% else %}
                            <!-- Put displayed link text first for sorting -->
                            <div style="display:none;">{{ pg.msrun.sample.animal.tracer_compound.name }}</div>
                            <a href="{% url 'compound_detail' pg.msrun.sample.animal.tracer_compound.id %}">
                                {{ pg.msrun.sample.animal.tracer_compound.name }}
                            </a>
                        {% endif %}
                    </td>

                    <!-- Tracer Infusion Rate (ul/min/g) -->
                    <td class="text-end">
                        {{ pg.msrun.sample.animal.tracer_infusion_rate }}
                    </td>

                    <!-- Tracer Infusion Concentration (mM) -->
                    <td class="text-end">
                        {{ pg.msrun.sample.animal.tracer_infusion_concentration }}
                    </td>

                    <!-- Studies -->
                    <td>
                        <!-- Put displayed link text first for sorting -->
                        <div style="display:none;">
                            {% define True as first %}
                            {% for study in pg.msrun.sample.animal.studies.all %}{% if not first %},<br>{% endif%}{{ study.name }}{% define False as first %}{% endfor %}
                        </div>

                        {% define True as first %}
                        {% for study in pg.msrun.sample.animal.studies.all %}{% if not first %},<br>{% endif%}<a href="{% url 'study_detail' study.id %}">{{ study.name }}</a>{% define False as first %}{% endfor %}
                    </td>
            {% endfor %}
        </tbody>

Solution

  • There are a few things to note here.

    1. I did not point out (because it didn't occur to me that it would be relevant) that the data I'm displaying is the result of a search. Bootstrap-table's server-side pagination only supports static data, so figuring out how to utilize a template is moot.
    2. While you cannot use any bootstrap-table builtin server-side pagination feature, it does provide various decorations of paging controls, so that when you implement your own server-side pagination, it would at least take advantage of BST's CSS.
    3. Django's builtin paginator tools are only for individual models and cannot be applied to searches whose results do not equate to 1 record = 1 row.

    So I ended up implementing my own paginator class and took advantage of BST's paginator control decorations. I won't go into the details of the implementation, since there are a number of options, but in order to roll your own paginator when you have:

    1. Tables resulting from a search
    2. Rows ≠ records (i.e. they're not 1:1)
    3. Joined records

    These are essentially what you would need:

    • For the search
      • A hidden form field that defines the search parameters (I used a JSONField)
      • Any other field for optional controls
    • A form field for rows per page
    • A form field for the page number to navigate to
    • And optionally:
      • A field that indicates a field by which to sort
      • A field for sort direction

    And when you perform the search using the Django ORM, you just have the slice the results based on the page number and rows/records per page.

    There are a few quirks to note about Django in this regard:

    While you can use .distinct(fields) (with .order_by(field)) to mimmic a true SQL left join (where you can get duplicate "root table" records), you have to deal with a few limitations:

    1. If you want to compute counts of unique values in a column to provide some stats, you cannot use .annotate(Count(...)) because you'll end up with a NotImplemented exception.
    2. In the view code, you have access to the "true left join" through M:M relationships, but in the template, you do not, and you have to loop through all possible related record for each duplicate root table record (e.g. {% for rootTable.MMrecs.all }). But you can work around this limitation using .annotate() in the view, like: .annotate("myMMtablerec"=F("MMrecs__pk")). Then in the template, I just used a template tag to retrieve the specific record from the rootTable.MMrecs.all that belongs to that row.