From b2a1c42f3df56f41fd1e9687c5163a49109ad6d3 Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Thu, 21 Jul 2022 13:51:12 +0100 Subject: [PATCH] Implement more UI elements --- app/urls.py | 10 +++ core/api/views/__init__.py | 0 core/api/views/threshold.py | 106 +++++++++++++++++++++++++++++ core/lib/threshold.py | 76 +++++++++++++++++++++ core/templates/base.html | 1 + core/templates/modals/info.html | 110 +++++++++++++++++++++++++++++++ core/templates/ui/drilldown.html | 34 +++++++++- core/templates/ui/results.html | 44 ++++++++++++- core/views/dynamic/search.py | 26 +++++++- 9 files changed, 403 insertions(+), 4 deletions(-) create mode 100644 core/api/views/__init__.py create mode 100644 core/api/views/threshold.py create mode 100644 core/lib/threshold.py create mode 100644 core/templates/modals/info.html diff --git a/app/urls.py b/app/urls.py index f1b38b2..b1fc77f 100644 --- a/app/urls.py +++ b/app/urls.py @@ -19,6 +19,12 @@ from django.contrib import admin from django.urls import include, path from django.views.generic import TemplateView +from core.api.views.threshold import ( + ThresholdChans, + ThresholdInfoModal, + ThresholdOnline, + ThresholdUsers, +) from core.ui.views.drilldown import Drilldown from core.views import Billing, Cancel, Home, Order, Portal, Signup from core.views.callbacks import Callback @@ -44,5 +50,9 @@ urlpatterns = [ path("accounts/signup/", Signup.as_view(), name="signup"), path("ui/drilldown/", Drilldown.as_view(), name="drilldown"), path("parts/search/", Search.as_view(), name="search"), + path("modal/info/", ThresholdInfoModal.as_view(), name="modal_info"), + path("api/chans/", ThresholdChans.as_view(), name="chans"), + path("api/users/", ThresholdUsers.as_view(), name="users"), + path("api/online/", ThresholdOnline.as_view(), name="online"), path("api/search/", APISearch.as_view(), name="api_search"), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/core/api/views/__init__.py b/core/api/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/api/views/threshold.py b/core/api/views/threshold.py new file mode 100644 index 0000000..03a8612 --- /dev/null +++ b/core/api/views/threshold.py @@ -0,0 +1,106 @@ +import logging + +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpResponse, JsonResponse +from django.shortcuts import render +from rest_framework.parsers import FormParser +from rest_framework.views import APIView + +from core.lib.threshold import annotate_online, get_chans, get_users + +logger = logging.getLogger(__name__) + + +class ThresholdChans(LoginRequiredMixin, APIView): + parser_classes = [FormParser] + plan_name = "drilldown" + + def post(self, request): + if not request.user.has_plan(self.plan_name): + return JsonResponse({"success": False}) + if "net" not in request.data: + return JsonResponse({"success": False}) + if "query" not in request.data: + return JsonResponse({"success": False}) + net = request.data["net"] + query = request.data["query"] + channels = get_chans(net, [query]) + if not channels: + return HttpResponse("") + channels_human = ", ".join(channels) + return HttpResponse(channels_human) + + +class ThresholdUsers(LoginRequiredMixin, APIView): + parser_classes = [FormParser] + plan_name = "drilldown" + + def post(self, request): + if not request.user.has_plan(self.plan_name): + return JsonResponse({"success": False}) + if "net" not in request.data: + return JsonResponse({"success": False}) + if "query" not in request.data: + return JsonResponse({"success": False}) + net = request.data["net"] + query = request.data["query"] + users = get_users(net, [query]) + if not users: + return HttpResponse("") + users_human = ", ".join(users) + return HttpResponse(users_human) + + +class ThresholdOnline(LoginRequiredMixin, APIView): + parser_classes = [FormParser] + plan_name = "drilldown" + + def post(self, request): + if not request.user.has_plan(self.plan_name): + return JsonResponse({"success": False}) + if "net" not in request.data: + return JsonResponse({"success": False}) + if "query" not in request.data: + return JsonResponse({"success": False}) + net = request.data["net"] + query = request.data["query"] + online_info = annotate_online(net, query) + return JsonResponse(online_info) + + +class ThresholdInfoModal(LoginRequiredMixin, APIView): + parser_classes = [FormParser] + plan_name = "drilldown" + template_name = "modals/info.html" + + def post(self, request): + if not request.user.has_plan(self.plan_name): + return JsonResponse({"success": False}) + if "net" not in request.data: + return JsonResponse({"success": False}) + if "nick" not in request.data: + return JsonResponse({"success": False}) + if "channel" not in request.data: + return JsonResponse({"success": False}) + net = request.data["net"] + nick = request.data["nick"] + channel = request.data["channel"] + channels = get_chans(net, [nick]) + users = get_users(net, [channel]) + if channels: + inter_users = get_users(net, channels) + else: + inter_users = [] + if users: + inter_chans = get_chans(net, users) + else: + inter_chans = [] + context = { + "nick": nick, + "channel": channel, + "chans": channels, + "users": users, + "inter_chans": inter_chans, + "inter_users": inter_users, + } + return render(request, self.template_name, context) diff --git a/core/lib/threshold.py b/core/lib/threshold.py new file mode 100644 index 0000000..51ac704 --- /dev/null +++ b/core/lib/threshold.py @@ -0,0 +1,76 @@ +import logging +from json import dumps + +import requests +from django.conf import settings +from requests.exceptions import JSONDecodeError + +logger = logging.getLogger(__name__) + + +def escape(obj): + chars = ["[", "]", "^", "-", "*", "?"] + if isinstance(obj, str): + obj = obj.replace("\\", "\\\\") + for i in chars: + obj = obj.replace(i, "\\" + i) + elif isinstance(obj, list): + for i in obj: + i = escape(i) + elif isinstance(obj, dict): + for key in obj: + obj[key] = escape(obj[key]) + return obj + + +def threshold_request(url, data): + headers = { + "ApiKey": settings.THRESHOLD_API_KEY, + "Token": settings.THRESHOLD_API_TOKEN, + } + for key in data: + data[key] = escape(data[key]) + r = requests.post( + f"{settings.THRESHOLD_ENDPOINT}/{url}/", data=dumps(data), headers=headers + ) + if not r.headers.get("Counter") == settings.THRESHOLD_API_COUNTER: + logger.error( + ( + f"Threshold API counter mismatch: " + f"{r.headers.get('Counter')} != " + f"{settings.THRESHOLD_API_COUNTER}" + ) + ) + return False + try: + response = r.json() + except JSONDecodeError: + logging.error(f"Invalid JSON response: {r.text}") + return response + + +def get_chans(net, query): + url = "chans" + payload = {"net": net, "query": query} + channels = threshold_request(url, payload) + if not channels: + return [] + return channels["chans"] + + +def get_users(net, query): + url = "users" + payload = {"net": net, "query": query} + users = threshold_request(url, payload) + if not users: + return [] + return users["users"] + + +def annotate_online(net, query): + url = "online" + payload = {"net": net, "query": query} + online_info = threshold_request(url, payload) + if not online_info: + return {} + return online_info diff --git a/core/templates/base.html b/core/templates/base.html index 61086a0..9c21169 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -9,6 +9,7 @@ Pathogen - {{ request.path_info }} + + + diff --git a/core/templates/ui/drilldown.html b/core/templates/ui/drilldown.html index 81a40ec..7149b9e 100644 --- a/core/templates/ui/drilldown.html +++ b/core/templates/ui/drilldown.html @@ -2,6 +2,38 @@ {% load static %} {% block content %} +
{% csrf_token %} @@ -10,7 +42,7 @@
- + diff --git a/core/templates/ui/results.html b/core/templates/ui/results.html index d3b158c..703aa79 100644 --- a/core/templates/ui/results.html +++ b/core/templates/ui/results.html @@ -9,14 +9,21 @@
+ Online + Offline + Unknown + IRC: + Discord:
+ + @@ -25,10 +32,43 @@ {% for item in results %} - + + - + + diff --git a/core/views/dynamic/search.py b/core/views/dynamic/search.py index 8750827..cd09cae 100644 --- a/core/views/dynamic/search.py +++ b/core/views/dynamic/search.py @@ -7,6 +7,7 @@ from django.shortcuts import render from django.views import View from core.lib.opensearch import initialise_opensearch, run_main_query +from core.lib.threshold import annotate_online client = initialise_opensearch() @@ -26,7 +27,30 @@ def query_results(request, post_params, api=False): if "hits" in results.keys(): if "hits" in results["hits"]: for item in results["hits"]["hits"]: - results_parsed.append(item["_source"]) + element = item["_source"] + element["id"] = item["_id"] + ts = element["ts"] + ts_spl = ts.split("T") + date = ts_spl[0] + time = ts_spl[1] + element["date"] = date + element["time"] = time + results_parsed.append(element) + # Figure out items with net (not discord) + nets = set() + for x in results_parsed: + if "net" in x: + nets.add(x["net"]) + + # Annotate the online attribute from Threshold + for net in nets: + online_info = annotate_online( + net, [x["nick"] for x in results_parsed if x["src"] == "irc"] + ) + for item in results_parsed: + if item["nick"] in online_info: + item["online"] = online_info[item["nick"]] + context = { "query": query, "results": results_parsed,
src TS msg host nickactions channel net
{{ item.ts }} + {% if item.src == 'irc' %} + + {% elif item.src == 'dis' %} + + {% endif %} + +

{{ item.date }}

+

{{ item.time }}

+
{{ item.msg }} {{ item.host }}{{ item.nick }} + {% if item.online is True %} + {{ item.nick }} + {% elif item.online is False %} + {{ item.nick }} + {% else %} + {{ item.nick }} + {% endif %} + + {% if item.src == 'irc' %} + +
+ {% endif %} +
{{ item.channel }} {{ item.net }}