From 44f05ad63b157f5d27874d4c268715b71d175079 Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Tue, 9 Aug 2022 07:20:30 +0100 Subject: [PATCH] Implement paginated sortable table for results --- app/urls.py | 8 +- core/lib/opensearch.py | 2 +- core/templates/ui/drilldown/drilldown.html | 11 +- .../templates/ui/drilldown/table_results.html | 8 +- .../drilldown/table_results_partial copy.html | 80 ++++++ .../ui/drilldown/table_results_partial.html | 234 ++++++++++++------ core/views/ui/drilldown.py | 194 +++++++++++---- core/views/ui/tables.py | 2 + 8 files changed, 405 insertions(+), 134 deletions(-) create mode 100644 core/templates/ui/drilldown/table_results_partial copy.html diff --git a/app/urls.py b/app/urls.py index dfd1346..a413e37 100644 --- a/app/urls.py +++ b/app/urls.py @@ -51,7 +51,10 @@ from core.views.manage.threshold.threshold import ( ) # 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 ( Insights, InsightsChannels, @@ -62,7 +65,8 @@ from core.views.ui.insights import ( ) 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("callback", Callback.as_view(), name="callback"), path("billing/", Billing.as_view(), name="billing"), diff --git a/core/lib/opensearch.py b/core/lib/opensearch.py index 8541d81..1c4dbf0 100644 --- a/core/lib/opensearch.py +++ b/core/lib/opensearch.py @@ -299,7 +299,7 @@ def query_results(request, query_params, size=None): context = { "query": query, - "results": results_parsed, + "object_list": results_parsed, "card": results["hits"]["total"]["value"], "took": results["took"], "redacted": results["redacted"], diff --git a/core/templates/ui/drilldown/drilldown.html b/core/templates/ui/drilldown/drilldown.html index 3b2e3bb..86270b9 100644 --- a/core/templates/ui/drilldown/drilldown.html +++ b/core/templates/ui/drilldown/drilldown.html @@ -88,7 +88,7 @@ }
-
@@ -113,7 +113,7 @@
- {% if results %} + + {% if table %} + {% include 'ui/drilldown/table_results.html' %} {% endif %}
diff --git a/core/templates/ui/drilldown/table_results.html b/core/templates/ui/drilldown/table_results.html index d842efa..c691678 100644 --- a/core/templates/ui/drilldown/table_results.html +++ b/core/templates/ui/drilldown/table_results.html @@ -10,7 +10,7 @@
-

fetched {{ results|length }} of {{ card }} hits in {{ took }}ms

+

fetched {{ table.data|length }} of {{ card }} hits in {{ took }}ms

{% if exemption is not None %}
@@ -24,6 +24,12 @@ {% endif %} {% endif %}
+
+
+ +
+ +
{% include 'ui/drilldown/table_results_partial.html' %} diff --git a/core/templates/ui/drilldown/table_results_partial copy.html b/core/templates/ui/drilldown/table_results_partial copy.html new file mode 100644 index 0000000..00433f9 --- /dev/null +++ b/core/templates/ui/drilldown/table_results_partial copy.html @@ -0,0 +1,80 @@ +{% extends 'django-tables2/bulma.html' %} + +{% load django_tables2 %} + +{% load i18n %} + +{% block table.thead %} + {% if table.show_header %} + + + {% for column in table.columns %} + + {{ column.header }} + + {% endfor %} + + + {% endif %} +{% endblock table.thead %} + +{# Pagination block overrides #} +{% block pagination.previous %} + {% if table.page.has_previous %} +
  • + +
  • + {% endif %} +{% endblock pagination.previous %} + +{% block pagination.range %} + {% for p in table.page|table_page_range:table.paginator %} +
  • + +
  • + {% endfor %} +{% endblock pagination.range %} +{% if table.page.has_next %} + {% block pagination.next %} +
  • + +
  • + {% endblock pagination.next %} +{% endif %} diff --git a/core/templates/ui/drilldown/table_results_partial.html b/core/templates/ui/drilldown/table_results_partial.html index 401abf7..78f9dbc 100644 --- a/core/templates/ui/drilldown/table_results_partial.html +++ b/core/templates/ui/drilldown/table_results_partial.html @@ -1,75 +1,163 @@ -{% extends 'django-tables2/bulma.html' %} - {% load django_tables2 %} - {% load i18n %} - -{% block table.thead %} - {% if table.show_header %} - - - {% for column in table.columns %} - - {{ column.header }} - - {% endfor %} - - - {% endif %} -{% endblock table.thead %} - -{# Pagination block overrides #} -{% block pagination.previous %} - -{% endblock pagination.previous %} -{% block pagination.range %} - {% for p in table.page|table_page_range:table.paginator %} -
  • - -
  • - {% endfor %} -{% endblock pagination.range %} -{% block pagination.next %} - -{% endblock pagination.next %} \ No newline at end of file +{% load django_tables2_bulma_template %} +{% block table-wrapper %} +
    + {% block table %} + + {% block table.thead %} + {% if table.show_header %} + + {% block table.thead.row %} + + {% for column in table.columns %} + {% block table.thead.th %} + + {% endblock table.thead.th %} + {% endfor %} + + {% endblock table.thead.row %} + + {% endif %} + {% endblock table.thead %} + {% block table.tbody %} + + {% for row in table.paginated_rows %} + {% block table.tbody.row %} + + {% for column, cell in row.items %} + {% block table.tbody.td %} + + {% endblock table.tbody.td %} + {% endfor %} + + {% endblock table.tbody.row %} + {% empty %} + {% if table.empty_text %} + {% block table.tbody.empty_text %} + + {% endblock table.tbody.empty_text %} + {% endif %} + {% endfor %} + + {% endblock table.tbody %} + {% block table.tfoot %} + {% if table.has_footer %} + + {% block table.tfoot.row %} + + {% for column in table.columns %} + {% block table.tfoot.td %} + + {% endblock table.tfoot.td %} + {% endfor %} + + {% endblock table.tfoot.row %} + + {% endif %} + {% endblock table.tfoot %} +
    + {% if column.orderable %} + {% if column.is_ordered %} + {% is_descending column.order_by as descending %} + {% if descending %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + + {{ column.header }} + + {% else %} + {{ column.header }} + {% endif %} +
    {% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}
    {{ table.empty_text }}
    {{ column.footer }}
    + {% endblock table %} + {% block pagination %} + {% if table.page and table.paginator.num_pages > 1 %} + + {% endif %} + {% endblock pagination %} +
    +{% endblock table-wrapper %} \ No newline at end of file diff --git a/core/views/ui/drilldown.py b/core/views/ui/drilldown.py index 25906d7..4b9b431 100644 --- a/core/views/ui/drilldown.py +++ b/core/views/ui/drilldown.py @@ -6,7 +6,7 @@ from django.http import HttpResponse, JsonResponse from django.shortcuts import render from django.urls import reverse from django.views import View -from django_tables2 import SingleTableMixin +from django_tables2 import SingleTableView from rest_framework.parsers import FormParser from rest_framework.views import APIView @@ -20,30 +20,6 @@ from core.lib.threshold import ( 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): spl = dates.split(" - ") if all(spl): @@ -76,12 +52,52 @@ def create_tags(query): return tags -def drilldown_search(request): - template_name = "ui/drilldown/results.html" +def make_table(context): + 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: + print("GET") + if not request.htmx: + print("NOT REQUEST HTMX") + template_name = "ui/drilldown/drilldown.html" query_params = request.GET.dict() elif request.POST: + print("POST") 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 if "dates" in query_params: @@ -99,54 +115,126 @@ def drilldown_search(request): context = query_results(request, query_params) elif request.POST: context = query_results(request, query_params) + print("QUERY PARAMS", query_params) # Turn the query into tags for populating the taglist - tags = create_tags(query_params["query"]) - context["tags"] = tags + if "query" in query_params: + tags = create_tags(query_params["query"]) + context["tags"] = tags context["params"] = query_params if "message" in context: + print("MESSAGE IN CONTEXT") return render(request, template_name, context) - context["data"] = 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 context["results"] - ] - ) + # Create data for chart.js sentiment graph + graph = make_graph(context["object_list"]) + context["data"] = graph + print("MADE GRAPH") + if context: + context["sizes"] = sizes + context = make_table(context) + + # URI we're passing to the template for linking + if "csrfmiddlewaretoken" in query_params: + del query_params["csrfmiddlewaretoken"] + 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: - return context + print("IS GET") + if request.htmx: + print("IS HTMX PUSHING", url_params) + response["HX-Push"] = reverse("home") + "?" + url_params elif request.POST: - del query_params["csrfmiddlewaretoken"] - url_params = urllib.parse.urlencode(query_params) + print("IS POST") + print("TEMPLATE", template_name) response["HX-Push"] = reverse("home") + "?" + url_params + if return_context: + return context return response else: + print("NO RESULTS", context) 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): template_name = "ui/drilldown/drilldown.html" plan_name = "drilldown" def get(self, request): - if request.user.is_anonymous: - 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) + return drilldown_search(request) def post(self, request): return drilldown_search(request) diff --git a/core/views/ui/tables.py b/core/views/ui/tables.py index e03df85..ed64836 100644 --- a/core/views/ui/tables.py +++ b/core/views/ui/tables.py @@ -32,3 +32,5 @@ class DrilldownTable(Table): status = Column() user = Column() version_sentiment = Column() + template_name = "ui/drilldown/table_results.html" + paginate_by = 5