Implement paginated sortable table for results

This commit is contained in:
Mark Veidemanis 2022-08-09 07:20:30 +01:00
parent 62133a8cbb
commit 44f05ad63b
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
8 changed files with 405 additions and 134 deletions

View File

@ -51,7 +51,10 @@ from core.views.manage.threshold.threshold import (
) )
# Main tool pages # Main tool pages
from core.views.ui.drilldown import Drilldown, ThresholdInfoModal # DrilldownTableView, from core.views.ui.drilldown import ( # DrilldownTableView,; Drilldown,
DrilldownTableView,
ThresholdInfoModal,
)
from core.views.ui.insights import ( from core.views.ui.insights import (
Insights, Insights,
InsightsChannels, InsightsChannels,
@ -62,7 +65,8 @@ from core.views.ui.insights import (
) )
urlpatterns = [ urlpatterns = [
path("", Drilldown.as_view(), name="home"), path("", DrilldownTableView.as_view(), name="home"),
path("search/", DrilldownTableView.as_view(), name="search"),
path("about/", About.as_view(), name="about"), path("about/", About.as_view(), name="about"),
path("callback", Callback.as_view(), name="callback"), path("callback", Callback.as_view(), name="callback"),
path("billing/", Billing.as_view(), name="billing"), path("billing/", Billing.as_view(), name="billing"),

View File

@ -299,7 +299,7 @@ def query_results(request, query_params, size=None):
context = { context = {
"query": query, "query": query,
"results": results_parsed, "object_list": results_parsed,
"card": results["hits"]["total"]["value"], "card": results["hits"]["total"]["value"],
"took": results["took"], "took": results["took"],
"redacted": results["redacted"], "redacted": results["redacted"],

View File

@ -88,7 +88,7 @@
} }
</script> </script>
<div> <div>
<form method="POST" hx-post="{% url 'home' %}" <form method="POST" hx-post="{% url 'search' %}"
hx-trigger="change" hx-trigger="change"
hx-target="#results" hx-target="#results"
hx-swap="innerHTML" hx-swap="innerHTML"
@ -100,7 +100,7 @@
<div class="field has-addons"> <div class="field has-addons">
<div class="control is-expanded has-icons-left"> <div class="control is-expanded has-icons-left">
<input <input
hx-post="{% url 'home' %}" hx-post="{% url 'search' %}"
hx-trigger="keyup changed delay:200ms" hx-trigger="keyup changed delay:200ms"
hx-target="#results" hx-target="#results"
hx-swap="innerHTML" id="query" name="query" value="{{ params.query }}" class="input" type="text" placeholder="msg: science AND nick: BillNye AND channel: #science"> hx-swap="innerHTML" id="query" name="query" value="{{ params.query }}" class="input" type="text" placeholder="msg: science AND nick: BillNye AND channel: #science">
@ -113,7 +113,7 @@
<button <button
id="search" id="search"
class="button is-info is-fullwidth" class="button is-info is-fullwidth"
hx-post="{% url 'home' %}" hx-post="{% url 'search' %}"
hx-trigger="click" hx-trigger="click"
hx-target="#results" hx-target="#results"
hx-swap="innerHTML"> hx-swap="innerHTML">
@ -365,8 +365,11 @@
</div> </div>
<div class="block"> <div class="block">
<div id="results"> <div id="results">
{% if results %} <!-- {% if results %}
{% include 'ui/drilldown/results.html' %} {% include 'ui/drilldown/results.html' %}
{% endif %} -->
{% if table %}
{% include 'ui/drilldown/table_results.html' %}
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@
<i class="fa-solid fa-chart-mixed"></i> <i class="fa-solid fa-chart-mixed"></i>
</div> </div>
<div class="nowrap-child"> <div class="nowrap-child">
<p>fetched {{ results|length }} of {{ card }} hits in {{ took }}ms</p> <p>fetched {{ table.data|length }} of {{ card }} hits in {{ took }}ms</p>
</div> </div>
{% if exemption is not None %} {% if exemption is not None %}
<div class="nowrap-child"> <div class="nowrap-child">
@ -24,6 +24,12 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
<div class="box">
<div style="height: 30rem">
<canvas id="volume"></canvas>
</div>
<script src="{% static 'chart.js' %}"></script>
</div>
<div class="box"> <div class="box">
<div class="table-container"> <div class="table-container">
{% include 'ui/drilldown/table_results_partial.html' %} {% include 'ui/drilldown/table_results_partial.html' %}

View File

@ -0,0 +1,80 @@
{% extends 'django-tables2/bulma.html' %}
{% load django_tables2 %}
{% load i18n %}
{% block table.thead %}
{% if table.show_header %}
<thead {{ table.attrs.thead.as_html }}>
<tr>
{% for column in table.columns %}
<th
{{ column.attrs.th.as_html }}
hx-get="search/{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}&{{ uri }}"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-trigger="click"
hx-target="#results"
hx-swap="innerHTML"
hx-indicator=".progress"
style="cursor: pointer;">
{{ column.header }}
</th>
{% endfor %}
</tr>
</thead>
{% endif %}
{% endblock table.thead %}
{# Pagination block overrides #}
{% block pagination.previous %}
{% if table.page.has_previous %}
<li class="pagination-previous">
<div
hx-get="search/{% querystring table.prefixed_page_field=table.page.previous_page_number %}&{{ uri }}"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-trigger="click"
hx-target="#results"
hx-swap="innerHTML"
hx-indicator=".progress"
class="pagination-link">
<span aria-hidden="true">&laquo;</span>
{% trans 'previous' %}
</div>
</li>
{% endif %}
{% endblock pagination.previous %}
{% block pagination.range %}
{% for p in table.page|table_page_range:table.paginator %}
<li class="page-item{% if table.page.number == p %} active{% endif %}">
<div
class="pagination-link"
{% if p != '...' %}hx-get="search/{% querystring table.prefixed_page_field=p %}&{{ uri }}"{% endif %}
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-trigger="click"
hx-target="#results"
hx-swap="innerHTML"
hx-indicator=".progress">
{{ p }}
</div>
</li>
{% endfor %}
{% endblock pagination.range %}
{% if table.page.has_next %}
{% block pagination.next %}
<li class="pagination-next">
<div
hx-get="search/{% querystring table.prefixed_page_field=table.page.next_page_number %}&{{ uri }}"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-trigger="click"
hx-target="#results"
hx-swap="innerHTML"
hx-indicator=".progress"
class="pagination-link">
{% trans 'next' %}
<span aria-hidden="true">&raquo;</span>
</div>
</li>
{% endblock pagination.next %}
{% endif %}

View File

@ -1,75 +1,163 @@
{% extends 'django-tables2/bulma.html' %}
{% load django_tables2 %} {% load django_tables2 %}
{% load i18n %} {% load i18n %}
{% load django_tables2_bulma_template %}
{% block table-wrapper %}
<div class="container">
{% block table %}
<table {% render_attrs table.attrs class="table is-striped is-hoverable is-fullwidth" %}>
{% block table.thead %} {% block table.thead %}
{% if table.show_header %} {% if table.show_header %}
<thead {{ table.attrs.thead.as_html }}> <thead {% render_attrs table.attrs.thead class="" %}>
{% block table.thead.row %}
<tr> <tr>
{% for column in table.columns %} {% for column in table.columns %}
<th {% block table.thead.th %}
{{ column.attrs.th.as_html }} <th {% render_attrs column.attrs.th class="" %}>
hx-post="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}" {% if column.orderable %}
{% if column.is_ordered %}
{% is_descending column.order_by as descending %}
{% if descending %}
<span aria-hidden="true">{% block table.desc_icon %}&darr;{% endblock table.desc_icon %}</span>
{% else %}
<span aria-hidden="true">{% block table.asc_icon %}&uarr;{% endblock table.asc_icon %}</span>
{% endif %}
{% else %}
<span aria-hidden="true">{% block table.orderable_icon %}&ReverseUpEquilibrium;{% endblock table.orderable_icon %}</span>
{% endif %}
<a
hx-get="search/{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}&{{ uri }}"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-trigger="click" hx-trigger="click"
hx-target="div.table-container" hx-target="#results"
hx-swap="outerHTML" hx-swap="innerHTML"
hx-indicator=".progress" hx-indicator=".progress"
style="cursor: pointer;"> style="cursor: pointer;">
{{ column.header }} {{ column.header }}
</a>
{% else %}
{{ column.header }}
{% endif %}
</th> </th>
{% endblock table.thead.th %}
{% endfor %} {% endfor %}
</tr> </tr>
{% endblock table.thead.row %}
</thead> </thead>
{% endif %} {% endif %}
{% endblock table.thead %} {% endblock table.thead %}
{% block table.tbody %}
{# Pagination block overrides #} <tbody {{ table.attrs.tbody.as_html }}>
{% for row in table.paginated_rows %}
{% block table.tbody.row %}
<tr {{ row.attrs.as_html }}>
{% for column, cell in row.items %}
{% block table.tbody.td %}
<td {{ column.attrs.td.as_html }}>{% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}</td>
{% endblock table.tbody.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 %}
{% if table.has_footer %}
<tfoot {{ table.attrs.tfoot.as_html }}>
{% block table.tfoot.row %}
<tr>
{% for column in table.columns %}
{% block table.tfoot.td %}
<td {{ column.attrs.tf.as_html }}>{{ column.footer }}</td>
{% endblock table.tfoot.td %}
{% endfor %}
</tr>
{% endblock table.tfoot.row %}
</tfoot>
{% endif %}
{% endblock table.tfoot %}
</table>
{% endblock table %}
{% block pagination %}
{% if table.page and table.paginator.num_pages > 1 %}
<nav class="pagination is-justify-content-flex-end" role="navigation" aria-label="pagination">
{% block pagination.previous %} {% block pagination.previous %}
<li class="previous page-item"> <a
<div class="pagination-previous is-flex-grow-0 {% if not table.page.has_previous %}is-hidden-mobile{% endif %}"
hx-post="{% querystring table.prefixed_page_field=table.page.previous_page_number %}" {% if table.page.has_previous %}
hx-get="search/{% querystring table.prefixed_page_field=table.page.previous_page_number %}&{{ uri }}"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-trigger="click" hx-trigger="click"
hx-target="div.table-container" hx-target="#results"
hx-swap="outerHTML" hx-swap="innerHTML"
hx-indicator=".progress" hx-indicator=".progress"
class="page-link"> {% else %}
href="#"
disabled
{% endif %}
style="order:1;">
{% block pagination.previous.text %}
<span aria-hidden="true">&laquo;</span> <span aria-hidden="true">&laquo;</span>
{% trans 'previous' %} {% endblock pagination.previous.text %}
</div> </a>
</li>
{% endblock pagination.previous %} {% endblock pagination.previous %}
{% block pagination.range %} {% block pagination.next %}
{% for p in table.page|table_page_range:table.paginator %} <a class="pagination-next is-flex-grow-0 {% if not table.page.has_next %}is-hidden-mobile{% endif %}"
<li class="page-item{% if table.page.number == p %} active{% endif %}"> {% if table.page.has_next %}
<div hx-get="search/{% querystring table.prefixed_page_field=table.page.next_page_number %}&{{ uri }}"
class="page-link"
{% if p != '...' %}hx-post="{% querystring table.prefixed_page_field=p %}"{% endif %}
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-trigger="click" hx-trigger="click"
hx-target="div.table-container" hx-target="#results"
hx-swap="outerHTML" hx-swap="innerHTML"
hx-indicator=".progress"> hx-indicator=".progress"
{% else %}
href="#"
disabled
{% endif %}
style="order:3;"
>
{% block pagination.next.text %}
<span aria-hidden="true">&raquo;</span>
{% endblock pagination.next.text %}
</a>
{% endblock pagination.next %}
{% if table.page.has_previous or table.page.has_next %}
{% block pagination.range %}
<ul class="pagination-list is-flex-grow-0" style="order:2;">
{% for p in table.page|table_page_range:table.paginator %}
<li>
<a class="pagination-link {% if p == table.page.number %}is-current{% endif %}"
aria-label="Page {{ p }}" block
{% if p == table.page.number %}aria-current="page"{% endif %}
{% if p == table.page.number %}
href="#"
{% else %}
hx-get="search/{% querystring table.prefixed_page_field=p %}&{{ uri }}"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-trigger="click"
hx-target="#results"
hx-swap="innerHTML"
hx-indicator=".progress"
{% endif %}
>
{% if p == '...' %}
<span class="pagination-ellipsis">&hellip;</span>
{% else %}
{{ p }} {{ p }}
</div> {% endif %}
</a>
</li> </li>
{% endfor %} {% endfor %}
</ul>
{% endblock pagination.range %} {% endblock pagination.range %}
{% block pagination.next %} {% endif %}
<li class="next page-item"> </nav>
<div {% endif %}
hx-post="{% querystring table.prefixed_page_field=table.page.next_page_number %}" {% endblock pagination %}
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-trigger="click"
hx-target="div.table-container"
hx-swap="outerHTML"
hx-indicator=".progress"
class="page-link">
{% trans 'next' %}
<span aria-hidden="true">&raquo;</span>
</div> </div>
</li> {% endblock table-wrapper %}
{% endblock pagination.next %}

View File

@ -6,7 +6,7 @@ from django.http import HttpResponse, JsonResponse
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
from django.views import View from django.views import View
from django_tables2 import SingleTableMixin from django_tables2 import SingleTableView
from rest_framework.parsers import FormParser from rest_framework.parsers import FormParser
from rest_framework.views import APIView from rest_framework.views import APIView
@ -20,30 +20,6 @@ from core.lib.threshold import (
from core.views.ui.tables import DrilldownTable from core.views.ui.tables import DrilldownTable
class DrilldownTableView(View, SingleTableMixin):
table_class = DrilldownTable
template_name = "ui/drilldown/table_results.html"
paginate_by = 5
def post(self, request):
context = query_results(request)
table = DrilldownTable(context["results"])
context["table"] = table
del context["results"]
if "message" in context:
return render(request, self.template_name, context)
if self.request.htmx:
template_name = "ui/drilldown/table_results.html"
else:
template_name = "ui/drilldown/table_results_partial.html"
if context:
return render(request, template_name, context)
else:
return HttpResponse("No results")
def parse_dates(dates): def parse_dates(dates):
spl = dates.split(" - ") spl = dates.split(" - ")
if all(spl): if all(spl):
@ -76,12 +52,52 @@ def create_tags(query):
return tags return tags
def drilldown_search(request): def make_table(context):
template_name = "ui/drilldown/results.html" table = DrilldownTable(context["object_list"])
context["table"] = table
# del context["results"]
return context
def make_graph(results):
graph = json.dumps(
[
{
"text": item.get("msg", None) or item.get("id"),
"nick": item.get("nick", None),
"value": item.get("sentiment", None) or None,
"date": item.get("ts"),
}
for item in results
]
)
return graph
def drilldown_search(request, return_context=False, template=None):
if not template:
template_name = "ui/drilldown/table_results.html"
else:
template_name = template
if request.user.is_anonymous:
sizes = settings.OPENSEARCH_MAIN_SIZES_ANON
else:
sizes = settings.OPENSEARCH_MAIN_SIZES
if request.GET: if request.GET:
print("GET")
if not request.htmx:
print("NOT REQUEST HTMX")
template_name = "ui/drilldown/drilldown.html"
query_params = request.GET.dict() query_params = request.GET.dict()
elif request.POST: elif request.POST:
print("POST")
query_params = request.POST.dict() query_params = request.POST.dict()
else:
print("ELSE")
template_name = "ui/drilldown/drilldown.html"
context = {"sizes": sizes}
return render(request, template_name, context)
# Parse the dates # Parse the dates
if "dates" in query_params: if "dates" in query_params:
@ -99,54 +115,126 @@ def drilldown_search(request):
context = query_results(request, query_params) context = query_results(request, query_params)
elif request.POST: elif request.POST:
context = query_results(request, query_params) context = query_results(request, query_params)
print("QUERY PARAMS", query_params)
# Turn the query into tags for populating the taglist # Turn the query into tags for populating the taglist
if "query" in query_params:
tags = create_tags(query_params["query"]) tags = create_tags(query_params["query"])
context["tags"] = tags context["tags"] = tags
context["params"] = query_params context["params"] = query_params
if "message" in context: if "message" in context:
print("MESSAGE IN CONTEXT")
return render(request, template_name, context) return render(request, template_name, context)
context["data"] = json.dumps( # Create data for chart.js sentiment graph
[ graph = make_graph(context["object_list"])
{ context["data"] = graph
"text": item.get("msg", None) or item.get("id"), print("MADE GRAPH")
"nick": item.get("nick", None),
"value": item.get("sentiment", None) or None,
"date": item.get("ts"),
}
for item in context["results"]
]
)
if context: if context:
response = render(request, template_name, context) context["sizes"] = sizes
if request.GET: context = make_table(context)
return context
elif request.POST: # URI we're passing to the template for linking
if "csrfmiddlewaretoken" in query_params:
del query_params["csrfmiddlewaretoken"] del query_params["csrfmiddlewaretoken"]
url_params = urllib.parse.urlencode(query_params) url_params = urllib.parse.urlencode(query_params)
context["client_uri"] = url_params
# URI we're passing to the template for linking, table fields removed
table_fields = ["page", "sort"]
clean_params = {k: v for k, v in query_params.items() if k not in table_fields}
clean_url_params = urllib.parse.urlencode(clean_params)
context["uri"] = clean_url_params
response = render(request, template_name, context)
if request.GET:
print("IS GET")
if request.htmx:
print("IS HTMX PUSHING", url_params)
response["HX-Push"] = reverse("home") + "?" + url_params response["HX-Push"] = reverse("home") + "?" + url_params
elif request.POST:
print("IS POST")
print("TEMPLATE", template_name)
response["HX-Push"] = reverse("home") + "?" + url_params
if return_context:
return context
return response return response
else: else:
print("NO RESULTS", context)
return HttpResponse("No results") return HttpResponse("No results")
class DrilldownTableView(SingleTableView):
table_class = DrilldownTable
template_name = "ui/drilldown/table_results.html"
paginate_by = 5
def get_queryset(self, request, **kwargs):
print("QUERYSET KWARGS", kwargs)
context = drilldown_search(request, return_context=True)
# Save the context as we will need to merge other attributes later
self.context = context
print(
"VIEW CONTEXT",
{k: v for k, v in context.items() if k not in ["object_list", "data"]},
)
if "object_list" in context:
return context["object_list"]
else:
print("NO OBJ LIST IN CONTEXT", context)
return []
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset(request)
allow_empty = self.get_allow_empty()
if not allow_empty:
# When pagination is enabled and object_list is a queryset,
# it's better to do a cheap query than to load the unpaginated
# queryset in memory.
if self.get_paginate_by(self.object_list) is not None and hasattr(
self.object_list, "exists"
):
is_empty = not self.object_list.exists() # noqa
else:
is_empty = not self.object_list # noqa
context = self.get_context_data()
if isinstance(self.context, HttpResponse):
return self.context
print(
"TABLE CONTEXT",
{k: v for k, v in context.items() if k not in ["object_list", "data"]},
)
for k, v in self.context.items():
if k not in context:
context[k] = v
print(
"FINAL CONTEXT",
{k: v for k, v in context.items() if k not in ["object_list", "data"]},
)
if request.method == "GET":
if not request.htmx:
print("NOT REQUEST HTMX")
self.template_name = "ui/drilldown/drilldown.html"
response = self.render_to_response(context)
# if not request.method == "GET":
if "client_uri" in context:
print("PUSHING CLIENT URI", context["client_uri"])
response["HX-Push"] = reverse("home") + "?" + context["client_uri"]
return response
def post(self, request, *args, **kwargs):
return self.get(request, *args, **kwargs)
class Drilldown(View): class Drilldown(View):
template_name = "ui/drilldown/drilldown.html" template_name = "ui/drilldown/drilldown.html"
plan_name = "drilldown" plan_name = "drilldown"
def get(self, request): def get(self, request):
if request.user.is_anonymous: return drilldown_search(request)
sizes = settings.OPENSEARCH_MAIN_SIZES_ANON
else:
sizes = settings.OPENSEARCH_MAIN_SIZES
context = {}
if request.GET:
context = drilldown_search(request)
context["sizes"] = sizes
return render(request, self.template_name, context)
def post(self, request): def post(self, request):
return drilldown_search(request) return drilldown_search(request)

View File

@ -32,3 +32,5 @@ class DrilldownTable(Table):
status = Column() status = Column()
user = Column() user = Column()
version_sentiment = Column() version_sentiment = Column()
template_name = "ui/drilldown/table_results.html"
paginate_by = 5