Begin implementing smarter WM system for multi-type objects

This commit is contained in:
Mark Veidemanis 2022-11-29 07:20:39 +00:00
parent fd4cecee05
commit ac3a57a2e8
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
22 changed files with 377 additions and 608 deletions

View File

@ -320,8 +320,18 @@
{% endblock %} {% endblock %}
<section class="section"> <section class="section">
<div class="container"> <div class="container">
{% block content %} {% block content_wrapper %}
{% block content %}
{% endblock %}
{% endblock %} {% endblock %}
<div id="modals-here">
</div>
<div id="windows-here">
</div>
<div id="widgets-here" style="display: none;">
{% block widgets %}
{% endblock %}
</div>
</div> </div>
</section> </section>
</body> </body>

View File

@ -1,48 +1,152 @@
{% extends "base.html" %} {% extends 'base.html' %}
{% load static %} {% load static %}
{% load joinsep %}
{% block outer_content %}
{% if params.modal == 'context' %}
<div
style="display: none;"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url 'modal_context' %}"
hx-vals='{"net": "{{ params.net|escapejs }}",
"num": "{{ params.num|escapejs }}",
"source": "{{ params.source|escapejs }}",
"channel": "{{ params.channel|escapejs }}",
"time": "{{ params.time|escapejs }}",
"date": "{{ params.date|escapejs }}",
"index": "{{ params.index }}",
"type": "{{ params.type|escapejs }}",
"mtype": "{{ params.mtype|escapejs }}",
"nick": "{{ params.nick|escapejs }}"}'
hx-target="#modals-here"
hx-trigger="load">
</div>
{% endif %}
<script src="{% static 'js/chart.js' %}"></script>
<script src="{% static 'tabs.js' %}"></script>
<script>
function setupTags() {
var inputTags = document.getElementById('tags');
new BulmaTagsInput(inputTags);
{% block content %} inputTags.BulmaTagsInput().on('before.add', function(item) {
<div class="block"> if (item.includes(": ")) {
{% for block in blocks %} var spl = item.split(": ");
{% if block.title is not None %} } else {
<h1 class="title">{{ block.title }}</h1> var spl = item.split(":");
{% endif %} }
<div class="box"> var field = spl[0];
<div class="columns"> try {
{% if block.column1 is not None %} var value = JSON.parse(spl[1]);
<div class="column"> } catch {
{{ block.column1 }} var value = spl[1];
</div> }
{% endif %} return `${field}: ${value}`;
{% if block.column2 is not None %} });
<div class="column"> inputTags.BulmaTagsInput().on('after.remove', function(item) {
{{ block.column2 }} var spl = item.split(": ");
</div> var field = spl[0];
{% endif %} var value = spl[1].trim();
{% if block.column3 is not None %} });
<div class="column"> }
{{ block.column3 }} function populateSearch(field, value) {
</div> var inputTags = document.getElementById('tags');
{% endif %} inputTags.BulmaTagsInput().add(field+": "+value);
</div> //htmx.trigger("#search", "click");
<div class="columns"> }
{% if block.image1 is not None %} </script>
<div class="column">
<img src="{% static block.image1 %}"> <div class="grid-stack" id="grid-stack-main">
</div> <div class="grid-stack-item" gs-w="7" gs-h="10" gs-y="0" gs-x="1">
{% endif %} <div class="grid-stack-item-content">
{% if block.image2 is not None %} <nav class="panel">
<div class="column"> <p class="panel-heading" style="padding: .2em; line-height: .5em;">
<img src="{% static block.image2 %}"> <i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i>
</div> Search
{% endif %} </p>
{% if block.image3 is not None %} <article class="panel-block is-active">
<div class="column"> {% include 'window-content/search.html' %}
<img src="{% static block.image3 %}"> </article>
</div> </nav>
{% endif %}
</div>
</div> </div>
{% endfor %} </div>
</div> </div>
<script>
var grid = GridStack.init({
cellHeight: 20,
cellWidth: 50,
cellHeightUnit: 'px',
auto: true,
float: true,
draggable: {handle: '.panel-heading', scroll: false, appendTo: 'body'},
removable: false,
animate: true,
});
// GridStack.init();
setupTags();
// a widget is ready to be loaded
document.addEventListener('load-widget', function(event) {
let container = htmx.find('#widget');
// get the scripts, they won't be run on the new element so we need to eval them
var scripts = htmx.findAll(container, "script");
let widgetelement = container.firstElementChild.cloneNode(true);
var new_id = widgetelement.id;
// check if there's an existing element like the one we want to swap
let grid_element = htmx.find('#grid-stack-main');
let existing_widget = htmx.find(grid_element, "#"+new_id);
// get the size and position attributes
if (existing_widget) {
let attrs = existing_widget.getAttributeNames();
for (let i = 0, len = attrs.length; i < len; i++) {
if (attrs[i].startsWith('gs-')) { // only target gridstack attributes
widgetelement.setAttribute(attrs[i], existing_widget.getAttribute(attrs[i]));
}
}
}
// clear the queue element
container.outerHTML = "";
// temporary workaround, other widgets can be duplicated, but not results
if (widgetelement.id == 'widget-results') {
grid.removeWidget("widget-results");
}
grid.addWidget(widgetelement);
// re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid
htmx.process(widgetelement);
// update size when the widget is loaded
document.addEventListener('load-widget-results', function(evt) {
var added_widget = htmx.find(grid_element, '#widget-results');
var itemContent = htmx.find(added_widget, ".control");
var scrollheight = itemContent.scrollHeight+80;
var verticalmargin = 0;
var cellheight = grid.opts.cellHeight;
var height = Math.ceil((scrollheight + verticalmargin) / (cellheight + verticalmargin));
var opts = {
h: height,
}
grid.update(
added_widget,
opts
);
});
// run the JS scripts inside the added element again
// for instance, this will fix the dropdown
for (var i = 0; i < scripts.length; i++) {
eval(scripts[i].innerHTML);
}
});
</script>
{% endblock %} {% endblock %}
{% block widgets %}
{% if table %}
{% include 'partials/results_load.html' %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1 @@
<button class="modal-close is-large" aria-label="close"></button>

View File

@ -0,0 +1,3 @@
<i
class="fa-solid fa-xmark has-text-grey-light float-right"
onclick='grid.removeWidget("widget-{{ unique }}");'></i>

View File

@ -0,0 +1,3 @@
<i
class="fa-solid fa-xmark has-text-grey-light float-right"
data-script="on click remove the closest <nav/>"></i>

View File

@ -1,20 +1,10 @@
{% extends 'wm/widget.html' %} {% extends 'wm/widget.html' %}
{% load static %} {% load static %}
{% block widget_options %}
gs-w="10" gs-h="1" gs-y="10" gs-x="1"
{% endblock %}
{% block heading %} {% block heading %}
Results Results
{% endblock %} {% endblock %}
{% block close_button %}
<i
class="fa-solid fa-xmark has-text-grey-light float-right"
onclick='grid.removeWidget("drilldown-widget-{{ unique }}"); //grid.compact();'></i>
{% endblock %}
{% block panel_content %} {% block panel_content %}
{% include 'partials/notify.html' %} {% include 'partials/notify.html' %}
<script src="{% static 'js/column-shifter.js' %}"></script> <script src="{% static 'js/column-shifter.js' %}"></script>
@ -38,6 +28,6 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% include 'ui/drilldown/table_results_partial.html' %} {% include 'partials/results_table.html' %}
{% include 'ui/drilldown/sentiment_partial.html' %} {% include 'partials/sentiment_chart.html' %}
{% endblock %} {% endblock %}

View File

@ -141,10 +141,6 @@
<i class="fa-solid fa-file-slash"></i> <i class="fa-solid fa-file-slash"></i>
</span> </span>
</td> </td>
{% elif column.name == 'tokens' %}
<td class="{{ column.name }} wrap" style="max-width: 10em">
{{ cell|joinsep:',' }}
</td>
{% elif column.name == 'src' %} {% elif column.name == 'src' %}
<td class="{{ column.name }}"> <td class="{{ column.name }}">
<a <a
@ -301,7 +297,7 @@
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url 'modal_drilldown' type='window' %}" hx-post="{% url 'modal_drilldown' type='window' %}"
hx-vals='{"net": "{{ row.cells.net }}", "nick": "{{ row.cells.nick }}", "channel": "{{ row.cells.channel }}"}' hx-vals='{"net": "{{ row.cells.net }}", "nick": "{{ row.cells.nick }}", "channel": "{{ row.cells.channel }}"}'
hx-target="#items-here" hx-target="#windows-here"
hx-swap="afterend" hx-swap="afterend"
hx-trigger="click" hx-trigger="click"
class="has-text-black"> class="has-text-black">
@ -364,25 +360,17 @@
</span> </span>
{% endif %} {% endif %}
</td> </td>
{% elif column.name|slice:":6" == "words_" %} {% elif column.name == "tokens" %}
<td class="{{ column.name }}"> <td class="{{ column.name }}">
{% if cell.0.1|length == 0 %} <div class="tags">
<a {% for word in cell %}
class="tag is-info" <a
onclick="populateSearch('{{ column.name }}', '{{ cell }}')"> class="tag is-info"
{{ cell }} onclick="populateSearch('{{ column.name }}', '{{ word }}')">
</a> {{ word }}
{% else %} </a>
<div class="tags"> {% endfor %}
{% for word in cell %} </div>
<a
class="tag is-info"
onclick="populateSearch('{{ column.name }}', '{{ word }}')">
{{ word }}
</a>
{% endfor %}
</div>
{% endif %}
</td> </td>
{% else %} {% else %}
<td class="{{ column.name }}"> <td class="{{ column.name }}">

View File

@ -1,163 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% load joinsep %}
{% block outer_content %}
{% if params.modal == 'context' %}
<div
style="display: none;"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url 'modal_context' %}"
hx-vals='{"net": "{{ params.net|escapejs }}",
"num": "{{ params.num|escapejs }}",
"source": "{{ params.source|escapejs }}",
"channel": "{{ params.channel|escapejs }}",
"time": "{{ params.time|escapejs }}",
"date": "{{ params.date|escapejs }}",
"index": "{{ params.index }}",
"type": "{{ params.type|escapejs }}",
"mtype": "{{ params.mtype|escapejs }}",
"nick": "{{ params.nick|escapejs }}"}'
hx-target="#modals-here"
hx-trigger="load">
</div>
{% endif %}
<script src="{% static 'js/chart.js' %}"></script>
<script src="{% static 'tabs.js' %}"></script>
<script>
function setupTags() {
var inputTags = document.getElementById('tags');
new BulmaTagsInput(inputTags);
inputTags.BulmaTagsInput().on('before.add', function(item) {
if (item.includes(": ")) {
var spl = item.split(": ");
} else {
var spl = item.split(":");
}
var field = spl[0];
try {
var value = JSON.parse(spl[1]);
} catch {
var value = spl[1];
}
return `${field}: ${value}`;
});
inputTags.BulmaTagsInput().on('after.remove', function(item) {
var spl = item.split(": ");
var field = spl[0];
var value = spl[1].trim();
});
}
function populateSearch(field, value) {
var inputTags = document.getElementById('tags');
inputTags.BulmaTagsInput().add(field+": "+value);
//htmx.trigger("#search", "click");
}
</script>
<div class="grid-stack" id="grid-stack-main">
<div class="grid-stack-item" gs-w="7" gs-h="10" gs-y="0" gs-x="1">
<div class="grid-stack-item-content">
<nav class="panel">
<p class="panel-heading" style="padding: .2em; line-height: .5em;">
<i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i>
Search
</p>
<article class="panel-block is-active">
{% include 'ui/drilldown/search_partial.html' %}
</article>
</nav>
</div>
</div>
</div>
<script>
var grid = GridStack.init({
cellHeight: 20,
cellWidth: 50,
cellHeightUnit: 'px',
auto: true,
float: true,
draggable: {handle: '.panel-heading', scroll: false, appendTo: 'body'},
removable: false,
animate: true,
});
// GridStack.init();
setupTags();
// a widget is ready to be loaded
document.addEventListener('load-widget', function(event) {
let container = htmx.find('#drilldown-widget');
// get the scripts, they won't be run on the new element so we need to eval them
var scripts = htmx.findAll(container, "script");
let widgetelement = container.firstElementChild.cloneNode(true);
// check if there's an existing element like the one we want to swap
let grid_element = htmx.find('#grid-stack-main');
let existing_widget = htmx.find(grid_element, '#drilldown-widget-results');
// get the size and position attributes
if (existing_widget) {
let attrs = existing_widget.getAttributeNames();
for (let i = 0, len = attrs.length; i < len; i++) {
if (attrs[i].startsWith('gs-')) { // only target gridstack attributes
widgetelement.setAttribute(attrs[i], existing_widget.getAttribute(attrs[i]));
}
}
}
// clear the queue element
container.outerHTML = "";
// temporary workaround, other widgets can be duplicated, but not results
if (widgetelement.id == 'drilldown-widget-results') {
grid.removeWidget("drilldown-widget-{{ unique }}");
}
grid.addWidget(widgetelement);
// re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid
htmx.process(widgetelement);
// update size when the widget is loaded
document.addEventListener('load-widget-results', function(evt) {
var added_widget = htmx.find(grid_element, '#drilldown-widget-results');
console.log(added_widget);
var itemContent = htmx.find(added_widget, ".control");
console.log(itemContent);
var scrollheight = itemContent.scrollHeight+80;
var verticalmargin = 0;
var cellheight = grid.opts.cellHeight;
var height = Math.ceil((scrollheight + verticalmargin) / (cellheight + verticalmargin));
var opts = {
h: height,
}
grid.update(
added_widget,
opts
);
});
// run the JS scripts inside the added element again
// for instance, this will fix the dropdown
for (var i = 0; i < scripts.length; i++) {
eval(scripts[i].innerHTML);
}
});
</script>
<div id="modals-here">
</div>
<div id="items-here">
</div>
<div id="widgets-here" style="display: none;">
</div>
<div id="results" style="display: none;">
{% if table %}
{% include 'widgets/table_results.html' %}
{% endif %}
</div>
<script>
</script>
{% endblock %}

View File

@ -1,122 +0,0 @@
{% load index %}
{% load static %}
<script src="{% static 'modal.js' %}"></script>
<script>
document.addEventListener("restore-modal-scroll", function(event) {
var modalContent = document.getElementsByClassName("modal-content")[0];
var maxScroll = modalContent.scrollHeight - modalContent.offsetHeight;
var scrollpos = localStorage.getItem('scrollpos_modal_content');
if (scrollpos == 'BOTTOM') {
modalContent.scrollTop = maxScroll;
} else if (scrollpos) {
modalContent.scrollTop = scrollpos;
};
});
document.addEventListener("htmx:beforeSwap", function(event) {
var modalContent = document.getElementsByClassName("modal-content")[0];
var scrollpos = modalContent.scrollTop;
if(modalContent.scrollTop === (modalContent.scrollHeight - modalContent.offsetHeight)) {
localStorage.setItem('scrollpos_modal_content', 'BOTTOM');
} else {
localStorage.setItem('scrollpos_modal_content', scrollpos);
}
});
</script>
<style>
#tab-content-{{ unique }} div {
display: none;
}
#tab-content-{{ unique }} div.is-active {
display: block;
}
</style>
<div id="modal" class="modal is-active is-clipped">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box">
{% include 'partials/notify.html' %}
<div class="tabs is-toggle is-fullwidth is-info" id="tabs-{{ unique }}">
<ul>
<li class="is-active" data-tab="1">
<a>
<span class="icon is-small"><i class="fa-solid fa-message-arrow-down"></i></span>
<span>Scrollback</span>
</a>
</li>
<li data-tab="2">
<a>
<span class="icon is-small"><i class="fa-solid fa-messages"></i></span>
<span>Context</span>
</a>
</li>
<li data-tab="3">
<a>
<span class="icon is-small"><i class="fa-solid fa-message"></i></span>
<span>Message</span>
</a>
</li>
<li data-tab="4">
<a>
<span class="icon is-small"><i class="fa-solid fa-asterisk"></i></span>
<span>Info</span>
</a>
</li>
</ul>
</div>
<div id="tab-content-{{ unique }}">
<div class="is-active" data-content="1">
<h4 class="subtitle is-4">Scrollback of {{ channel }} on {{ net }}{{ num }}</h4>
{% include 'modals/context_table.html' %}
{% if user.is_superuser and src == 'irc' %}
<form method="PUT">
<article class="field has-addons">
<article class="control is-expanded has-icons-left">
<input id="context-input" name="msg" class="input" type="text" placeholder="Type your message here">
<span class="icon is-small is-left">
<i class="fas fa-magnifying-glass"></i>
</span>
</article>
<article class="control">
<article class="field">
<button
id="search"
class="button is-info is-fullwidth"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-put="{% url 'threshold_irc_msg' net num %}"
hx-vals='{"channel": "{{ channel }}", "nick": "{{ nick }}"}'
hx-trigger="click"
hx-target="#context-input"
hx-swap="outerHTML">
Send
</button>
</article>
</article>
</article>
</form>
{% endif %}
</div>
<div data-content="2">
<h4 class="subtitle is-4">Scrollback of {{ channel }} on {{ net }}{{ num }} around {{ ts }}</h4>
Context
</div>
<div data-content="3">
<h4 class="subtitle is-4">Message details</h4>
Message deetails
</div>
<div data-content="4">
<h4 class="subtitle is-4">Information about {{ channel }} on {{ net }}{{ num }}</h4>
info
</div>
</div>
</div>
<script>initTabs("{{ unique }}");</script>
<button class="modal-close is-large" aria-label="close"></button>
</div>
</div>

View File

@ -1,177 +0,0 @@
<article class="table-container" id="modal-context-table">
<table class="table is-fullwidth">
<thead>
<th></th>
<th></th>
<th></th>
</thead>
<tbody>
{% for item in object_list %}
{% if item.type == 'control' %}
<tr>
<td></td>
<td>
<span class="icon has-text-grey" data-tooltip="Hidden">
<i class="fa-solid fa-file-slash"></i>
</span>
</td>
<td>
<p class="has-text-grey">Hidden {{ item.hidden }} similar result{% if item.hidden > 1%}s{% endif %}</p>
</td>
</tr>
{% else %}
<tr>
<td>{{ item.time }}</td>
<td>
{% if item.type != 'znc' and item.type != 'self' and query is not True %}
<article class="nowrap-parent">
<article class="nowrap-child">
{% if item.type == 'msg' %}
<span class="icon" data-tooltip="Message">
<i class="fa-solid fa-message"></i>
</span>
{% elif item.type == 'join' %}
<span class="icon" data-tooltip="Join">
<i class="fa-solid fa-person-to-portal"></i>
</span>
{% elif item.type == 'part' %}
<span class="icon" data-tooltip="Part">
<i class="fa-solid fa-person-from-portal"></i>
</span>
{% elif item.type == 'quit' %}
<span class="icon" data-tooltip="Quit">
<i class="fa-solid fa-circle-xmark"></i>
</span>
{% elif item.type == 'kick' %}
<span class="icon" data-tooltip="Kick">
<i class="fa-solid fa-user-slash"></i>
</span>
{% elif item.type == 'nick' %}
<span class="icon" data-tooltip="Nick">
<i class="fa-solid fa-signature"></i>
</span>
{% elif item.type == 'mode' %}
<span class="icon" data-tooltip="Mode">
<i class="fa-solid fa-gear"></i>
</span>
{% elif item.type == 'action' %}
<span class="icon" data-tooltip="Action">
<i class="fa-solid fa-exclamation"></i>
</span>
{% elif item.type == 'notice' %}
<span class="icon" data-tooltip="Notice">
<i class="fa-solid fa-message-code"></i>
</span>
{% elif item.type == 'conn' %}
<span class="icon" data-tooltip="Connection">
<i class="fa-solid fa-cloud-exclamation"></i>
</span>
{% elif item.type == 'znc' %}
<span class="icon" data-tooltip="ZNC">
<i class="fa-brands fa-unity"></i>
</span>
{% elif item.type == 'query' %}
<span class="icon" data-tooltip="Query">
<i class="fa-solid fa-message"></i>
</span>
{% elif item.type == 'highlight' %}
<span class="icon" data-tooltip="Highlight">
<i class="fa-solid fa-exclamation"></i>
</span>
{% elif item.type == 'who' %}
<span class="icon" data-tooltip="Who">
<i class="fa-solid fa-passport"></i>
</span>
{% elif item.type == 'topic' %}
<span class="icon" data-tooltip="Topic">
<i class="fa-solid fa-sign"></i>
</span>
{% else %}
{{ item.type }}
{% endif %}
{% if item.online is True %}
<span class="icon has-text-success has-tooltip-success" data-tooltip="Online">
<i class="fa-solid fa-circle"></i>
</span>
{% elif item.online is False %}
<span class="icon has-text-danger has-tooltip-danger" data-tooltip="Offline">
<i class="fa-solid fa-circle"></i>
</span>
{% else %}
<span class="icon has-text-warning has-tooltip-warning" data-tooltip="Unknown">
<i class="fa-solid fa-circle"></i>
</span>
{% endif %}
{% if item.src == 'irc' %}
<a
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url 'modal_drilldown' %}"
hx-vals='{"net": "{{ item.net|escapejs }}", "nick": "{{ item.nick|escapejs }}", "channel": "{{ item.channel|escapejs }}"}'
hx-target="#modals-here"
hx-trigger="click"
class="has-text-black">
<span class="icon" data-tooltip="Open drilldown modal">
<i class="fa-solid fa-album"></i>
</span>
</a>
{% endif %}
</article>
<a class="nowrap-child has-text-link is-underlined" onclick="populateSearch('nick', '{{ item.nick|escapejs }}')">
{{ item.nick }}
</a>
{% if item.num_chans != '—' %}
<article class="nowrap-child">
<span class="tag">
{{ item.num_chans }}
</span>
</article>
{% endif %}
</article>
{% endif %}
{% if item.type == 'self' %}
<span class="icon has-text-primary" data-tooltip="You">
<i class="fa-solid fa-message-check"></i>
</span>
{% elif item.type == 'znc' %}
<span class="icon has-text-info" data-tooltip="ZNC">
<i class="fa-brands fa-unity"></i>
</span>
{% elif query %}
<span class="icon has-text-info" data-tooltip="Auth">
<i class="fa-solid fa-passport"></i>
</span>
{% endif %}
</td>
<td class="wrap">{{ item.msg }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% if object_list %}
<div
class="modal-refresh"
style="display: none;"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url 'modal_context_table' %}"
hx-vals='{"net": "{{ net }}",
"num": "{{ num }}",
"src": "{{ src }}",
"channel": "{{ channel }}",
"time": "{{ time }}",
"date": "{{ date }}",
"index": "{{ index }}",
"type": "{{ type }}",
"mtype": "{{ mtype }}",
"nick": "{{ nick }}",
"dedup": "{{ params.dedup }}"}'
hx-target="#modal-context-table"
hx-trigger="every 5s">
</div>
{% endif %}
</article>
<script>
var modal_event = new Event('restore-modal-scroll');
document.dispatchEvent(modal_event);
</script>

View File

@ -0,0 +1,26 @@
{% load static %}
{% include 'partials/notify.html' %}
<script src="{% static 'js/column-shifter.js' %}"></script>
{% if cache is not None %}
<span class="icon has-tooltip-bottom" data-tooltip="Cached">
<i class="fa-solid fa-database"></i>
</span>
{% endif %}
fetched {{ table.data|length }} hits in {{ took }}ms
{% if exemption is not None %}
<span class="icon has-tooltip-bottom" data-tooltip="God mode">
<i class="fa-solid fa-book-bible"></i>
</span>
{% else %}
{% if redacted is not None %}
<span class="icon has-tooltip-bottom" data-tooltip="{{ redacted }} redacted">
<i class="fa-solid fa-mask"></i>
</span>
{% endif %}
{% endif %}
{% include 'partials/results_table.html' %}
{% include 'partials/sentiment_chart.html' %}

View File

@ -1,6 +1,6 @@
<form class="skipEmptyFields" method="POST" hx-post="{% url 'search' %}" <form class="skipEmptyFields" method="POST" hx-post="{% url 'search' %}"
hx-trigger="change" hx-trigger="change"
hx-target="#results" hx-target="#widgets-here"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-indicator="#spinner"> hx-indicator="#spinner">
{% csrf_token %} {% csrf_token %}
@ -11,7 +11,7 @@
<input <input
hx-post="{% url 'search' %}" hx-post="{% url 'search' %}"
hx-trigger="keyup changed delay:200ms" hx-trigger="keyup changed delay:200ms"
hx-target="#results" hx-target="#widgets-here"
hx-swap="innerHTML" hx-swap="innerHTML"
name="query" name="query"
value="{{ params.query }}" value="{{ params.query }}"
@ -29,7 +29,7 @@
class="button is-info is-fullwidth" class="button is-info is-fullwidth"
hx-post="{% url 'search' %}" hx-post="{% url 'search' %}"
hx-trigger="click" hx-trigger="click"
hx-target="#results" hx-target="#widgets-here"
hx-swap="innerHTML"> hx-swap="innerHTML">
Search Search
</button> </button>
@ -394,7 +394,7 @@
<input <input
hx-trigger="change" hx-trigger="change"
hx-post="{% url 'search' %}" hx-post="{% url 'search' %}"
hx-target="#results" hx-target="#widgets-here"
hx-swap="innerHTML" hx-swap="innerHTML"
id="tags" id="tags"
class="input" class="input"

View File

@ -1,4 +1,4 @@
{% extends 'wm/magnet.html' %} {% extends 'wm/window.html' %}
{% block heading %} {% block heading %}
Drilldown Drilldown

View File

@ -12,8 +12,9 @@
<div class="modal-content"> <div class="modal-content">
<div class="box"> <div class="box">
{% block modal_content %} {% block modal_content %}
{% include window_content %}
{% endblock %} {% endblock %}
<button class="modal-close is-large" aria-label="close"></button> {% include 'partials/close-modal.html' %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block content %}
{% include window_content %}
{% endblock %}

View File

@ -3,9 +3,7 @@
<p class="panel-heading" style="padding: .2em; line-height: .5em;"> <p class="panel-heading" style="padding: .2em; line-height: .5em;">
<i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i> <i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i>
{% block close_button %} {% block close_button %}
<i {% include 'partials/close-window.html' %}
class="fa-solid fa-xmark has-text-grey-light float-right"
data-script="on click remove the closest <nav/>"></i>
{% endblock %} {% endblock %}
{% block heading %} {% block heading %}
{% endblock %} {% endblock %}

View File

@ -1,24 +1,24 @@
<div id="drilldown-widget"> <div id="widget">
<div id="drilldown-widget-{{ unique }}" class="grid-stack-item" {% block widget_options %}{% endblock %}> <div id="widget-{{ unique }}" class="grid-stack-item" {% block widget_options %}gs-w="10" gs-h="1" gs-y="10" gs-x="1"{% endblock %}>
<div class="grid-stack-item-content"> <div class="grid-stack-item-content">
<nav class="panel"> <nav class="panel">
<p class="panel-heading" style="padding: .2em; line-height: .5em;"> <p class="panel-heading" style="padding: .2em; line-height: .5em;">
<i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i> <i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i>
{% block close_button %} {% block close_button %}
<i {% include 'partials/close-widget.html' %}
class="fa-solid fa-xmark has-text-grey-light float-right"
onclick='grid.removeWidget("drilldown-widget-{{ unique }}");'></i>
{% endblock %} {% endblock %}
<i <i
class="fa-solid fa-arrows-minimize has-text-grey-light float-right" class="fa-solid fa-arrows-minimize has-text-grey-light float-right"
onclick='grid.compact();'></i> onclick='grid.compact();'></i>
{% block heading %} {% block heading %}
{{ title }}
{% endblock %} {% endblock %}
</p> </p>
<article class="panel-block is-active"> <article class="panel-block is-active">
<div class="control"> <div class="control">
{% block panel_content %} {% block panel_content %}
{% include window_content %}
{% endblock %} {% endblock %}
</div> </div>
</article> </article>

View File

@ -1,8 +1,10 @@
<magnet-block attract-distance="10" align-to="outer|center" class="floating-window"> <magnet-block attract-distance="10" align-to="outer|center" class="floating-window">
{% extends 'wm/panel.html' %} {% extends 'wm/panel.html' %}
{% block heading %} {% block heading %}
{{ title }}
{% endblock %} {% endblock %}
{% block panel_content %} {% block panel_content %}
{% include window_content %}
{% endblock %} {% endblock %}
</magnet-block> </magnet-block>

View File

@ -6,7 +6,6 @@ from django.conf import settings
from django.http import HttpResponse, JsonResponse 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_tables2 import SingleTableView 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
@ -215,21 +214,132 @@ def drilldown_search(request, return_context=False, template=None):
class DrilldownTableView(SingleTableView): class DrilldownTableView(SingleTableView):
table_class = DrilldownTable table_class = DrilldownTable
template_name = "widgets/table_results.html" template_name = "wm/widget.html"
window_content = "window-content/results.html"
# htmx_partial = "partials/"
paginate_by = settings.DRILLDOWN_RESULTS_PER_PAGE paginate_by = settings.DRILLDOWN_RESULTS_PER_PAGE
def get_queryset(self, request, **kwargs): def common_request(self, request, **kwargs):
context = drilldown_search(request, return_context=True) extra_params = {}
# Save the context as we will need to merge other attributes later
self.context = context
if "object_list" in context: if request.user.is_anonymous:
return context["object_list"] sizes = settings.MAIN_SIZES_ANON
else: else:
return [] sizes = settings.MAIN_SIZES
if request.GET:
self.template_name = "index.html"
# GET arguments in URL like ?query=xyz
query_params = request.GET.dict()
elif request.POST:
query_params = request.POST.dict()
else:
self.template_name = "index.html"
# No query, this is a fresh page load
# Don't try to search, since there's clearly nothing to do
params_with_defaults = {}
helpers.add_defaults(params_with_defaults)
context = {
"sizes": sizes,
"params": params_with_defaults,
"unique": "results",
"window_content": self.window_content,
"title": "Results",
}
return render(request, self.template_name, context)
# Merge everything together just in case
tmp_post = request.POST.dict()
tmp_get = request.GET.dict()
tmp_post = {k: v for k, v in tmp_post.items() if v and not v == "None"}
tmp_get = {k: v for k, v in tmp_get.items() if v and not v == "None"}
query_params.update(tmp_post)
query_params.update(tmp_get)
# URI we're passing to the template for linking
if "csrfmiddlewaretoken" in query_params:
del query_params["csrfmiddlewaretoken"]
# Parse the dates
if "dates" in query_params:
dates = parse_dates(query_params["dates"])
del query_params["dates"]
if dates:
if "message" in dates:
return render(request, self.template_name, dates)
query_params["from_date"] = dates["from_date"]
query_params["to_date"] = dates["to_date"]
query_params["from_time"] = dates["from_time"]
query_params["to_time"] = dates["to_time"]
# Remove null values
if "query" in query_params:
if query_params["query"] == "":
del query_params["query"]
# Remove null tags values
if "tags" in query_params:
if query_params["tags"] == "":
del query_params["tags"]
else:
# Parse the tags and populate cast to pass to search function
tags = parse_tags(query_params["tags"])
extra_params["tags"] = tags
context = db.query_results(request, query_params, **extra_params)
# Unique is for identifying the widgets.
# We don't want a random one since we only want one results pane.
context["unique"] = "results"
context["window_content"] = self.window_content
context["title"] = "Results"
# Valid sizes
context["sizes"] = sizes
# Add any default parameters to the context
params_with_defaults = dict(query_params)
helpers.add_defaults(params_with_defaults)
context["params"] = params_with_defaults
# Remove anything that we or the user set to a default for
# pretty URLs
helpers.remove_defaults(query_params)
url_params = urllib.parse.urlencode(query_params)
context["client_uri"] = url_params
# There's an error
if "message" in context:
response = render(request, self.template_name, context)
# Still push the URL so they can share it to get assistance
if request.GET:
if request.htmx:
response["HX-Push"] = reverse("home") + "?" + url_params
elif request.POST:
response["HX-Push"] = reverse("home") + "?" + url_params
return response
# Create data for chart.js sentiment graph
graph = make_graph(context["object_list"])
context["data"] = graph
# Create the table
context = make_table(context)
# 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
# unique = str(uuid.uuid4())[:8]
# self.context = context
return context
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset(request) self.context = self.common_request(request)
if isinstance(self.context, HttpResponse):
return self.context
self.object_list = self.context["object_list"]
show = [] show = []
show = set().union(*(d.keys() for d in self.object_list)) show = set().union(*(d.keys() for d in self.object_list))
allow_empty = self.get_allow_empty() allow_empty = self.get_allow_empty()
@ -245,17 +355,17 @@ class DrilldownTableView(SingleTableView):
else: else:
is_empty = not self.object_list # noqa is_empty = not self.object_list # noqa
context = self.get_context_data() context = self.get_context_data()
if isinstance(self.context, HttpResponse):
return self.context
for k, v in self.context.items(): for k, v in self.context.items():
if k not in context: if k not in context:
context[k] = v context[k] = v
context["show"] = show context["show"] = show
if request.method == "GET": # if request.htmx:
if not request.htmx: # self.template_name = self.window_content
self.template_name = "ui/drilldown/drilldown.html" # if request.method == "GET":
# if not request.htmx:
# self.template_name = "ui/drilldown/drilldown.html"
response = self.render_to_response(context) response = self.render_to_response(context)
# if not request.method == "GET": # if not request.method == "GET":
if "client_uri" in context: if "client_uri" in context:
@ -266,15 +376,15 @@ class DrilldownTableView(SingleTableView):
return self.get(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):
return drilldown_search(request) # return drilldown_search(request)
def post(self, request): # def post(self, request):
return drilldown_search(request) # return drilldown_search(request)
class DrilldownContextModal(APIView): class DrilldownContextModal(APIView):
@ -377,7 +487,6 @@ class DrilldownContextModal(APIView):
type=type, type=type,
nicks=nicks_sensitive, nicks=nicks_sensitive,
) )
print("QUERY", search_query)
results = db.query_results( results = db.query_results(
request, request,
query_params, query_params,
@ -437,45 +546,18 @@ class ThresholdInfoModal(APIView):
nick = request.data["nick"] nick = request.data["nick"]
channel = request.data["channel"] channel = request.data["channel"]
# SAFE BLOCK #
# Lookup the hash values but don't disclose them to the user
# if settings.HASHING:
# SAFE_PARAMS = request.data.dict()
# hash_lookup(request.user, SAFE_PARAMS)
channels = get_chans(net, [nick]) channels = get_chans(net, [nick])
print("CHANNELS", channels)
users = get_users(net, [nick]) users = get_users(net, [nick])
print("USERS", users)
num_users = annotate_num_users(net, channels) num_users = annotate_num_users(net, channels)
print("NUM_USERS", num_users)
num_chans = annotate_num_chans(net, users) num_chans = annotate_num_chans(net, users)
print("NUM_CHANS", num_chans)
if channels: if channels:
inter_users = get_users(net, channels) inter_users = get_users(net, channels)
else: else:
inter_users = [] inter_users = []
print("INTER_USERS", inter_users)
if users: if users:
inter_chans = get_chans(net, users) inter_chans = get_chans(net, users)
else: else:
inter_chans = [] inter_chans = []
print("INTER_CHANS", inter_chans)
# if settings.HASHING:
# hash_list(request.user, inter_chans)
# hash_list(request.user, inter_users)
# hash_list(request.user, num_chans, hash_keys=True)
# hash_list(request.user, num_users, hash_keys=True)
# hash_list(request.user, channels)
# hash_list(request.user, users)
# if settings.RANDOMISATION:
# randomise_list(request.user, num_chans)
# randomise_list(request.user, num_users)
# SAFE BLOCK END #
unique = str(uuid.uuid4())[:8] unique = str(uuid.uuid4())[:8]
context = { context = {
@ -490,5 +572,4 @@ class ThresholdInfoModal(APIView):
"num_chans": num_chans, "num_chans": num_chans,
"unique": unique, "unique": unique,
} }
print("CON", context)
return render(request, self.template_name, context) return render(request, self.template_name, context)

18
docker/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
# syntax=docker/dockerfile:1
FROM python:3
RUN useradd -d /code pathogen
RUN mkdir /code
RUN chown pathogen:pathogen /code
RUN mkdir /venv
RUN chown pathogen:pathogen /venv
USER pathogen
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /code
COPY requirements.dev.txt /code/
RUN python -m venv /venv
RUN . /venv/bin/activate && pip install -r requirements.dev.txt
CMD . /venv/bin/activate && exec python manage.py runserver 0.0.0.0:8000

View File

@ -3,4 +3,4 @@ PORTAINER_GIT_DIR=.
APP_LOCAL_SETTINGS=./app/local_settings.py APP_LOCAL_SETTINGS=./app/local_settings.py
APP_DATABASE_FILE=./db.sqlite3 APP_DATABASE_FILE=./db.sqlite3
STATIC_ROOT=/conf/static STATIC_ROOT=/conf/static
OPERATION=uwsgi OPERATION=dev