Search code examples
pythondjangodjango-tables2

Django tables2: Table reverts to previous value


I have a view where i display 2 tables and I have a form to create a new filter in my template. Using my code I would expect that the filter table to update and stay that way.

However, after creating a new filter, it displays in the table but if i refresh the page or create a new filter, it reverts to the "original" table i.e. if i have 1 filter and I create one but i refresh the page or create a new filter afterwards, the table displayed will show just one filter.

To illustrate in a more concrete way:

original: [<trend_monitoring.tables.ReportTable object at 0x7f01e4d3b700>, <trend_monitoring.tables.FilterTable object at 0x7f01e4d3bc70>]
[<trend_monitoring.tables.ReportTable object at 0x7f01e4d3b700>, <trend_monitoring.tables.FilterTable object at 0x7f01e4d3bc70>]
172.19.0.3 - - [10/Nov/2023:10:33:00 +0000] "POST /trendyqc/dashboard/ HTTP/1.0" 302 0 "http://localhost:8000/trendyqc/dashboard/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 OPR/106.0.0.0 (Edition developer)"
original: [<trend_monitoring.tables.ReportTable object at 0x7f01e4d45670>, <trend_monitoring.tables.FilterTable object at 0x7f01e4d45bb0>]
[<trend_monitoring.tables.ReportTable object at 0x7f01e4d45670>, <trend_monitoring.tables.FilterTable object at 0x7f01e4d45bb0>]
172.19.0.3 - - [10/Nov/2023:10:33:01 +0000] "GET /trendyqc/dashboard/ HTTP/1.0" 200 233735 "http://localhost:8000/trendyqc/dashboard/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 OPR/106.0.0.0 (Edition developer)"
original: [<trend_monitoring.tables.ReportTable object at 0x7f01e4d3b700>, <trend_monitoring.tables.FilterTable object at 0x7f01e4d3bc70>]
[<trend_monitoring.tables.ReportTable object at 0x7f01e4d3b700>, <trend_monitoring.tables.FilterTable object at 0x7f01e4d3bc70>]
172.19.0.3 - - [10/Nov/2023:10:33:26 +0000] "GET /trendyqc/dashboard/ HTTP/1.0" 200 232980 "http://localhost:8000/trendyqc/dashboard/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 OPR/106.0.0.0 (Edition developer)"

After refreshing, the RAM address goes back to the original value. I feel like there is some web dev voodoo going on but i don't know what is going on.

Thanks for your help.

Here is my view:

class Dashboard(MultiTableMixin, TemplateView):
    template_name = "dashboard.html"
    report_sample_data = Report_Sample.objects.all()
    tables = [
        ReportTable(Report.objects.all()),
        FilterTable(Filter.objects.all())
    ]
    model = Report

    table_pagination = {
        "per_page": 10
    }

    def _get_context_data_dashboard(self):
        """ Get the basic data that needs to be displayed in the dashboard page

        Returns:
            dict: Dict of report and assay data to be passed to the dashboard
        """

        # object list needs to be defined for SingleTableViews but i have no
        # need for it
        # get the default context data (the one i need is one called table)
        context = self.get_context_data(object_list="")
        print(f"original: {context['tables']}")

        # get all the assays and sort them
        assays = sorted({
            assay
            for assay in self.report_sample_data.values_list(
                "assay", flat=True)
        })
        sequencer_ids = sorted({
            sequencer_id
            for sequencer_id in self.model.objects.all().values_list(
                "sequencer_id", flat=True)
        })
        project_names = sorted(
            project_name
            for project_name in self.model.objects.all().values_list(
                "project_name", flat=True)
        )

        context["project_names"] = project_names
        context["assays"] = assays
        context["sequencer_ids"] = sequencer_ids
        plotable_metrics = {
            **self._get_plotable_metrics(bam_qc),
            **self._get_plotable_metrics(fastq_qc),
            **self._get_plotable_metrics(vcf_qc)
        }
        context["metrics"] = dict(sorted(plotable_metrics.items()))
        return context

    def _get_plotable_metrics(self, module) -> dict:
        """ Gather all the plotable metrics by model in a dict

        Args:
            module (module): Module containing the definition of models

        Returns:
            dict: Dict with the name of the model as key and the name of the
            field as value
        """

        plotable_metrics = {}

        # loop through the modules' classes and get their name and object into
        # a dict
        module_dict = dict(
            [
                (name, cls)
                for name, cls in module.__dict__.items()
                if isinstance(cls, type)
            ]
        )

        for model_name, model in module_dict.items():
            plotable_metrics.setdefault(model_name, [])

            for field in model._meta.fields:
                # get the type of the field
                field_type = field.get_internal_type()

                # only get fields with those type for plotability
                if field_type in ["FloatField", "IntegerField"]:
                    plotable_metrics[model_name].append(field.name)

            plotable_metrics[model_name].sort()

        return plotable_metrics

    def get(self, request):
        """ Handle GET request

        Args:
            request (?): HTML request coming in

        Returns:
            ?: Render Django thingy?
        """

        context = self._get_context_data_dashboard()
        print(context["tables"])
        request.session.pop("form", None)
        return render(request, self.template_name, context)

    def post(self, request):
        """ Handle POST request

        Args:
            request (?): HTML request coming in

        Returns:
            ?: Render Django thingy? | Redirect thingy towards the Plot view
        """

        context = self._get_context_data_dashboard()
        print(context["tables"])
        form = FilterForm(request.POST)
        request.session.pop("form", None)

        # button in the filter table has been clicked
        if "filter_use" in request.POST:
            # get the filter id from the button value
            filter_id = request.POST["filter_use"]
            # get the filter obj in the database
            filter_obj = Filter.objects.get(id=filter_id)
            # deserialize the filter content for use in the Plot page
            request.session["form"] = json.loads(filter_obj.content)
            return redirect("Plot")

        # call the clean function and see if the form data is valid
        if form.is_valid():
            if "plot" in request.POST:
                # save the cleaned data in the session so that it gets passed to
                # the Plot view
                request.session["form"] = form.cleaned_data
                return redirect("Plot")

            elif "save_filter" in request.POST:
                filter_name = request.POST["save_filter"]

                # the default value that the prompt return is Save filter i.e.
                # if nothing was inputted the value is Save filter
                if filter_name != "Save filter":
                    msg, msg_status = import_filter(
                        filter_name, form.cleaned_data
                    )
                    messages.add_message(request, msg_status, f"{msg}")

                return redirect("Dashboard")

        else:
            # add the errors for displaying in the dashboard template
            for error_field in form.errors:
                if isinstance(form.errors[error_field], list):
                    for error in form.errors[error_field]:
                        messages.add_message(
                            request, messages.ERROR,
                            f"{error_field}: {error}"
                        )
                else:
                    messages.add_message(
                            request, messages.ERROR,
                            f"{error_field}: {''.join(form.errors[error])}"
                        )

        return render(request, self.template_name, context)

Here is my template:

{% extends "base.html" %}

{% load static %}
{% load render_table from django_tables2 %}

{% block dashboard %}

<div style="width:90%; margin: auto; padding: 10px;">
<p>There are <b>{{ project_names|length }}</b> projects currently in TrendyQC.</p>
</div>


<div style="width:90%; margin: auto; padding: 10px;">
    {% render_table tables.0 %}
</div>

{% if user.is_authenticated %}

<br>

<form action="{% url 'Dashboard' %}" method="post" id="filter_use"> {% csrf_token %}
    <div style="width:90%; margin: auto; padding: 10px;">
        {% render_table tables.1 %}
    </div>
</form>

{% endif %}

<br>

<form action="{% url 'Dashboard' %}" method="post" id="filter_form"> {% csrf_token %}
    <!-- Div for 3 inner divs that will occupy the page horizontally -->
    <div class="filter" style="width:90%; margin: auto; padding: 10px; ">
        <!-- 1st inner div: filter for obtaining a subset of runs -->
        <div class="filter-column" style="display: inline-block; *display: inline; zoom: 1; vertical-align: top; width:40%">
            <h5>Subset selection (select at least one)</h5>
            <div>
                <ul>
                    <select class="multiselect" name="assay_select" multiple title="Choose an assay">
                        {% for assay in assays %}
                            <option value="{{ assay }}">{{ assay }}</option>
                        {% endfor %}
                    </select>
                </ul>
                <ul>
                    <select class="multiselect" name="run_select" multiple title="Choose a run">
                        {% for project_name in project_names %}
                            <option value="{{ project_name }}">{{ project_name }}</option>
                        {% endfor %}
                    </select>
                </ul>
                <ul>
                    <select class="multiselect" name="sequencer_select" multiple title="Choose a sequencer id">
                        {% for sequencer_id in sequencer_ids %}
                            <option value="{{ sequencer_id }}">{{ sequencer_id }}</option>
                        {% endfor %}
                    </select>
                </ul>
                <ul>
                    <input type="date" name="date_start"> - <input type="date" name="date_end">
                </ul>
            </div>
        </div>

        <!-- 2nd inner div: Metrics for x-axis -->
        <div class="filter-column" style="display: inline-block; *display: inline; zoom: 1; vertical-align: top; width:40%">
            <h5>X axis - Metrics</h5>
            <select class="multiselect" name="metrics_x" disabled title="Choose a metric">
                {% for model, fields in metrics.items %}
                    <optgroup label={{ model }}>
                        {% for field in fields %}
                            <option data-subtext="{{ model }}" data-tokens="{{ model }} {{ field }}" value="{{ model }}|{{ field }}">{{ field }}</option>
                        {% endfor %}
                    </optgroup>
                {% endfor %}
            </select>
        </div>

        <!-- 3rd inner div: Metrics for y-axis -->
        <div class="filter-column" style="display: inline-block; *display: inline; zoom: 1; vertical-align: top; width:18%">
            <h5>Y axis - Metrics</h5>
            <select class="multiselect" name="metrics_y" title="Choose a metric">
                {% for model, fields in metrics.items %}
                    <optgroup label={{ model }}>
                        {% for field in fields %}
                            <option data-subtext="{{ model }}" data-tokens="{{ model }} {{ field }}" value="{{ model }}|{{ field }}">{{ field }}</option>
                        {% endfor %}
                    </optgroup>
                {% endfor %}
            </select>
        </div>
    </div>

    <br>

    <div style="display: inline-block; *display: inline; zoom: 1; vertical-align: top; padding-left:5%">
        <input class="btn btn-info" type="submit" name="plot" value="Plot">
        <input class="btn btn-info" type="submit" value="Save filter" name="save_filter" id="save_filter" onclick="saveFilter();"/>
    </div>
</form>

<script>
$(function () {
    $(".multiselect").selectpicker({
        liveSearch: true
    });
});

function saveFilter() {
    var filter_name = prompt("Name your filter:", "");
    if (!filter_name) return;
    $("#save_filter").val(filter_name);
    $("#filter_form").submit();
}

// if a button is clicked i.e. the buttons in the filter table submit the filter use form
$("button").click(function() {
    $("#filter_use").submit();
});
</script>

{% endblock %}

Solution

  • Looks like having the Django query in the __init__ of the class did not refresh the value of the tables when getting a new GET request.

    Moving this query in my custom _get_context_data function seems to make the refresh work:

    class Dashboard(MultiTableMixin, TemplateView):
        template_name = "dashboard.html"
        report_sample_data = Report_Sample.objects.all()
        tables = [
            ReportTable(Report.objects.all())
        ]
        model = Report
    
        table_pagination = {
            "per_page": 10
        }
    
        def _get_context_data(self):
            """ Get the basic data that needs to be displayed in the dashboard page
    
            Returns:
                dict: Dict of report and assay data to be passed to the dashboard
            """
    
            # get the default context data (the one key i need is one called tables)
            context = super().get_context_data()
    
            context["tables"].append(FilterTable(Filter.objects.all()))