Compact interfaces and edit more things inline
This commit is contained in:
193
core/templates/mixins/window-content/person-form.html
Normal file
193
core/templates/mixins/window-content/person-form.html
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
{% include "mixins/partials/notify.html" %}
|
||||||
|
{% if page_title is not None %}
|
||||||
|
<h1 class="title is-4">{{ page_title }}</h1>
|
||||||
|
{% endif %}
|
||||||
|
{% if page_subtitle is not None %}
|
||||||
|
<h1 class="subtitle">{{ page_subtitle }}</h1>
|
||||||
|
{% endif %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.person-tab-pane {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-tab-pane.is-active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-tab-pane-body {
|
||||||
|
max-height: min(52vh, 30rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-content-count {
|
||||||
|
margin-left: 0.3rem;
|
||||||
|
min-width: 2.35rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<form
|
||||||
|
id="person-form-tabs-{{ object.id }}"
|
||||||
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||||
|
hx-post="{{ submit_url }}"
|
||||||
|
hx-target="#modals-here"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for hidden in form.hidden_fields %}
|
||||||
|
{{ hidden }}
|
||||||
|
{% endfor %}
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<article class="message is-danger">
|
||||||
|
<div class="message-body">{{ form.non_field_errors }}</div>
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{ form.name|as_crispy_field }}
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
<div class="column is-12-mobile is-4-tablet">{{ form.sentiment|as_crispy_field }}</div>
|
||||||
|
<div class="column is-12-mobile is-4-tablet">{{ form.timezone|as_crispy_field }}</div>
|
||||||
|
<div class="column is-12-mobile is-4-tablet">{{ form.last_interaction|as_crispy_field }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs is-small is-toggle is-toggle-rounded" style="margin-bottom: 0.75rem;">
|
||||||
|
<ul>
|
||||||
|
<li class="is-active">
|
||||||
|
<a href="#" data-person-tab-target="summary">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-file-lines"></i></span>
|
||||||
|
<span>Summary</span>
|
||||||
|
<span id="person-count-summary-{{ object.id }}" class="tag is-light is-small person-content-count">0</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" data-person-tab-target="profile">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-id-card"></i></span>
|
||||||
|
<span>Profile</span>
|
||||||
|
<span id="person-count-profile-{{ object.id }}" class="tag is-light is-small person-content-count">0</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" data-person-tab-target="revealed">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-eye"></i></span>
|
||||||
|
<span>Revealed</span>
|
||||||
|
<span id="person-count-revealed-{{ object.id }}" class="tag is-light is-small person-content-count">0</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" data-person-tab-target="likes">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-thumbs-up"></i></span>
|
||||||
|
<span>Likes</span>
|
||||||
|
<span id="person-count-likes-{{ object.id }}" class="tag is-light is-small person-content-count">0</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" data-person-tab-target="dislikes">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-thumbs-down"></i></span>
|
||||||
|
<span>Dislikes</span>
|
||||||
|
<span id="person-count-dislikes-{{ object.id }}" class="tag is-light is-small person-content-count">0</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="person-tab-pane is-active" data-person-tab-pane="summary">
|
||||||
|
<div class="person-tab-pane-body">
|
||||||
|
{{ form.summary|as_crispy_field }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="person-tab-pane" data-person-tab-pane="profile">
|
||||||
|
<div class="person-tab-pane-body">
|
||||||
|
{{ form.profile|as_crispy_field }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="person-tab-pane" data-person-tab-pane="revealed">
|
||||||
|
<div class="person-tab-pane-body">
|
||||||
|
{{ form.revealed|as_crispy_field }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="person-tab-pane" data-person-tab-pane="likes">
|
||||||
|
<div class="person-tab-pane-body">
|
||||||
|
{{ form.likes|as_crispy_field }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="person-tab-pane" data-person-tab-pane="dislikes">
|
||||||
|
<div class="person-tab-pane-body">
|
||||||
|
{{ form.dislikes|as_crispy_field }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons" style="margin-top: 1rem; margin-bottom: 0;">
|
||||||
|
{% if hide_cancel is not True %}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button is-light modal-close-button">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button type="submit" class="button modal-close-button">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const root = document.getElementById("person-form-tabs-{{ object.id }}");
|
||||||
|
if (!root || root.dataset.tabsBound === "1") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.dataset.tabsBound = "1";
|
||||||
|
|
||||||
|
const tabs = Array.from(root.querySelectorAll("[data-person-tab-target]"));
|
||||||
|
const panes = Array.from(root.querySelectorAll("[data-person-tab-pane]"));
|
||||||
|
const contentFields = ["summary", "profile", "revealed", "likes", "dislikes"];
|
||||||
|
|
||||||
|
function setActive(key) {
|
||||||
|
tabs.forEach((tab) => {
|
||||||
|
const li = tab.closest("li");
|
||||||
|
if (!li) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
li.classList.toggle("is-active", tab.dataset.personTabTarget === key);
|
||||||
|
});
|
||||||
|
panes.forEach((pane) => {
|
||||||
|
pane.classList.toggle("is-active", pane.dataset.personTabPane === key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCounts() {
|
||||||
|
contentFields.forEach((name) => {
|
||||||
|
const field = root.querySelector("[name='" + name + "']");
|
||||||
|
const counter = document.getElementById("person-count-" + name + "-{{ object.id }}");
|
||||||
|
if (!field || !counter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const length = (field.value || "").trim().length;
|
||||||
|
counter.textContent = String(length);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tabs.forEach((tab) => {
|
||||||
|
tab.addEventListener("click", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
setActive(tab.dataset.personTabTarget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
contentFields.forEach((name) => {
|
||||||
|
const field = root.querySelector("[name='" + name + "']");
|
||||||
|
if (field) {
|
||||||
|
field.addEventListener("input", updateCounts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCounts();
|
||||||
|
if (tabs[0]) {
|
||||||
|
setActive(tabs[0].dataset.personTabTarget);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
44
core/templates/mixins/window-content/queue-form-inline.html
Normal file
44
core/templates/mixins/window-content/queue-form-inline.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{% include "mixins/partials/notify.html" %}
|
||||||
|
{% if page_title is not None %}
|
||||||
|
<h1 class="title is-4">{{ page_title }}</h1>
|
||||||
|
{% endif %}
|
||||||
|
{% if page_subtitle is not None %}
|
||||||
|
<h1 class="subtitle">{{ page_subtitle }}</h1>
|
||||||
|
{% endif %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
<form
|
||||||
|
id="queue-inline-form-{{ object.id }}"
|
||||||
|
data-inline-target="{{ submit_target|default:'#modals-here' }}"
|
||||||
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||||
|
hx-post="{{ submit_url }}"
|
||||||
|
hx-target="{{ submit_target|default:'#modals-here' }}"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for hidden in form.hidden_fields %}
|
||||||
|
{{ hidden }}
|
||||||
|
{% endfor %}
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<article class="message is-danger">
|
||||||
|
<div class="message-body">{{ form.non_field_errors }}</div>
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
<div class="buttons are-small" style="margin-bottom: 0;">
|
||||||
|
{% if is_inline_edit %}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button is-light"
|
||||||
|
onclick="(function(){const f=document.getElementById('queue-inline-form-{{ object.id }}'); if(!f){return;} const target=f.dataset.inlineTarget; const host=target ? document.querySelector(target) : null; if(host){host.innerHTML=''; host.style.display='none';}})(); return false;">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
{% elif hide_cancel is not True %}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button is-light modal-close-button">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button type="submit" class="button modal-close-button">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
77
core/templates/mixins/wm/widget.html
Normal file
77
core/templates/mixins/wm/widget.html
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<div id="widget">
|
||||||
|
<div id="widget-{{ unique }}" class="grid-stack-item" {% block widget_options %}{% if widget_options is None %}gs-w="6" gs-h="1" gs-y="20" gs-x="0"{% else %}{% autoescape off %}{{ widget_options }}{% endautoescape %}{% endif %}{% endblock %}>
|
||||||
|
<div class="grid-stack-item-content">
|
||||||
|
|
||||||
|
<nav class="panel">
|
||||||
|
<p class="panel-heading" style="padding: .2em; line-height: .5em;">
|
||||||
|
<i class="{{ widget_icon|default:'fa-solid fa-arrows-up-down-left-right' }} has-text-grey-light"></i>
|
||||||
|
{% block close_button %}
|
||||||
|
{% include "mixins/partials/close-widget.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
<i
|
||||||
|
class="fa-solid fa-arrows-minimize has-text-grey-light float-right"
|
||||||
|
onclick="grid.compact();"></i>
|
||||||
|
{% block heading %}
|
||||||
|
{{ title }}
|
||||||
|
{% endblock %}
|
||||||
|
</p>
|
||||||
|
<article class="panel-block is-active">
|
||||||
|
<div class="control">
|
||||||
|
{% block panel_content %}
|
||||||
|
{% include window_content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
{% block custom_script %}
|
||||||
|
{% endblock %}
|
||||||
|
var widget_event = new Event("load-widget");
|
||||||
|
document.dispatchEvent(widget_event);
|
||||||
|
(function () {
|
||||||
|
var widgetRoot = document.getElementById("widget-{{ unique }}");
|
||||||
|
var iconClass = "{{ widget_icon|default:'fa-solid fa-arrows-minimize'|escapejs }}";
|
||||||
|
function decorateHandle() {
|
||||||
|
if (!widgetRoot) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var handles = widgetRoot.querySelectorAll(".ui-resizable-se");
|
||||||
|
if (!handles.length) {
|
||||||
|
handles = widgetRoot.querySelectorAll(".ui-resizable-handle");
|
||||||
|
}
|
||||||
|
if (!handles.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
handles.forEach(function (handle) {
|
||||||
|
if (handle.dataset.iconApplied === "1") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handle.dataset.iconApplied = "1";
|
||||||
|
handle.style.display = "flex";
|
||||||
|
handle.style.alignItems = "center";
|
||||||
|
handle.style.justifyContent = "center";
|
||||||
|
handle.style.overflow = "hidden";
|
||||||
|
var icon = document.createElement("i");
|
||||||
|
icon.className = iconClass + " has-text-grey-light";
|
||||||
|
icon.style.fontSize = "0.65rem";
|
||||||
|
icon.style.opacity = "0.65";
|
||||||
|
icon.style.pointerEvents = "none";
|
||||||
|
handle.appendChild(icon);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var attempts = 0;
|
||||||
|
var timer = window.setInterval(function () {
|
||||||
|
attempts += 1;
|
||||||
|
if (decorateHandle() || attempts > 10) {
|
||||||
|
window.clearInterval(timer);
|
||||||
|
}
|
||||||
|
}, 80);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% block custom_end %}
|
||||||
|
{% endblock %}
|
||||||
@@ -199,10 +199,15 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="is-flex is-align-items-center" style="gap: 0.35rem;">
|
<div class="is-flex is-align-items-center ai-capsule-controls">
|
||||||
<span id="ai-cache-indicator-{{ person.id }}" class="tag is-warning is-light is-small" style="display: none;">
|
<span class="ai-cache-indicator-slot">
|
||||||
|
<span
|
||||||
|
id="ai-cache-indicator-{{ person.id }}"
|
||||||
|
class="tag is-warning is-light is-small"
|
||||||
|
aria-hidden="true">
|
||||||
Cached
|
Cached
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="button is-small is-ghost"
|
class="button is-small is-ghost"
|
||||||
@@ -267,12 +272,38 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.ai-person-widget .ai-capsule-controls {
|
||||||
|
gap: 0.35rem;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.ai-person-widget .ai-cache-indicator-slot {
|
||||||
|
width: 9.5rem;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.ai-person-widget .ai-cache-indicator-slot .tag {
|
||||||
|
width: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.ai-person-widget .ai-cache-indicator-slot .tag.is-visible {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.ai-person-widget .ai-top-tabs li a {
|
.ai-person-widget .ai-top-tabs li a {
|
||||||
min-height: 2.1rem;
|
min-height: 2.1rem;
|
||||||
min-width: 2.1rem;
|
min-width: 2.1rem;
|
||||||
padding: 0 0.45rem;
|
padding: 0 0.45rem;
|
||||||
}
|
}
|
||||||
|
.ai-person-widget .ai-cache-indicator-slot {
|
||||||
|
width: 8.4rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -407,8 +438,13 @@
|
|||||||
}
|
}
|
||||||
if (show) {
|
if (show) {
|
||||||
indicator.textContent = formatCacheAge(ts);
|
indicator.textContent = formatCacheAge(ts);
|
||||||
|
indicator.classList.add("is-visible");
|
||||||
|
indicator.setAttribute("aria-hidden", "false");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
indicator.style.display = show ? "inline-flex" : "none";
|
indicator.textContent = "Cached";
|
||||||
|
indicator.classList.remove("is-visible");
|
||||||
|
indicator.setAttribute("aria-hidden", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
const OPERATION_TABS = ["summarise", "draft_reply", "extract_patterns"];
|
const OPERATION_TABS = ["summarise", "draft_reply", "extract_patterns"];
|
||||||
|
|||||||
@@ -61,10 +61,11 @@
|
|||||||
<div class="buttons are-small" style="margin: 0;">
|
<div class="buttons are-small" style="margin: 0;">
|
||||||
<button
|
<button
|
||||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||||
hx-get="{% url 'queue_update' type=type pk=item.id %}"
|
hx-get="{% url 'queue_update' type='window' pk=item.id %}?hx_target=%23queue-inline-editor-{{ item.id }}"
|
||||||
hx-trigger="click"
|
hx-trigger="click"
|
||||||
hx-target="#{{ type }}s-here"
|
hx-target="#queue-inline-editor-{{ item.id }}"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
|
_="on htmx:afterRequest if event.detail.successful set #queue-inline-editor-{{ item.id }}.style.display to 'block' end"
|
||||||
class="button is-light">
|
class="button is-light">
|
||||||
<span class="icon is-small"><i class="fa-solid fa-pen"></i></span>
|
<span class="icon is-small"><i class="fa-solid fa-pen"></i></span>
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
@@ -82,6 +83,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
id="queue-inline-editor-{{ item.id }}"
|
||||||
|
style="display: none; margin-top: 0.55rem; padding: 0.55rem; border-radius: 8px; border: 1px solid rgba(50, 115, 220, 0.25); background: rgba(255, 255, 255, 0.78);"></div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -53,6 +53,21 @@ def _column_field_name(column: "OsintColumn") -> str:
|
|||||||
return str(column.key)
|
return str(column.key)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_icon_class(raw: str | None, default: str) -> str:
|
||||||
|
icon_class = str(raw or "").strip()
|
||||||
|
if not icon_class:
|
||||||
|
return default
|
||||||
|
cleaned_parts = []
|
||||||
|
for part in icon_class.split():
|
||||||
|
if not part:
|
||||||
|
continue
|
||||||
|
if all(ch.isalnum() or ch in {"-", "_"} for ch in part):
|
||||||
|
cleaned_parts.append(part)
|
||||||
|
if not cleaned_parts:
|
||||||
|
return default
|
||||||
|
return " ".join(cleaned_parts)
|
||||||
|
|
||||||
|
|
||||||
def _url_with_query(base_url: str, query: dict[str, Any]) -> str:
|
def _url_with_query(base_url: str, query: dict[str, Any]) -> str:
|
||||||
params = {}
|
params = {}
|
||||||
for key, value in query.items():
|
for key, value in query.items():
|
||||||
@@ -378,6 +393,13 @@ OSINT_SCOPES: dict[str, OsintScopeConfig] = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OSINT_SCOPE_ICONS: dict[str, str] = {
|
||||||
|
"people": "fa-solid fa-user-group",
|
||||||
|
"groups": "fa-solid fa-users",
|
||||||
|
"personas": "fa-solid fa-masks-theater",
|
||||||
|
"manipulations": "fa-solid fa-sliders",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class OSINTListBase(ObjectList):
|
class OSINTListBase(ObjectList):
|
||||||
list_template = "partials/osint/list-table.html"
|
list_template = "partials/osint/list-table.html"
|
||||||
@@ -511,6 +533,10 @@ class OSINTListBase(ObjectList):
|
|||||||
request_type: str,
|
request_type: str,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
context_type = _context_type(request_type)
|
context_type = _context_type(request_type)
|
||||||
|
update_type = "window" if request_type == "widget" else context_type
|
||||||
|
update_target = (
|
||||||
|
"#windows-here" if update_type == "window" else f"#{update_type}s-here"
|
||||||
|
)
|
||||||
rows = []
|
rows = []
|
||||||
for item in object_list:
|
for item in object_list:
|
||||||
row = {"id": str(item.pk), "cells": [], "actions": []}
|
row = {"id": str(item.pk), "cells": [], "actions": []}
|
||||||
@@ -524,7 +550,7 @@ class OSINTListBase(ObjectList):
|
|||||||
|
|
||||||
update_url = reverse(
|
update_url = reverse(
|
||||||
scope.update_url_name,
|
scope.update_url_name,
|
||||||
kwargs={"type": context_type, "pk": item.pk},
|
kwargs={"type": update_type, "pk": item.pk},
|
||||||
)
|
)
|
||||||
delete_url = reverse(
|
delete_url = reverse(
|
||||||
scope.delete_url_name,
|
scope.delete_url_name,
|
||||||
@@ -535,7 +561,7 @@ class OSINTListBase(ObjectList):
|
|||||||
{
|
{
|
||||||
"mode": "hx-get",
|
"mode": "hx-get",
|
||||||
"url": update_url,
|
"url": update_url,
|
||||||
"target": f"#{context_type}s-here",
|
"target": update_target,
|
||||||
"icon": "fa-solid fa-pencil",
|
"icon": "fa-solid fa-pencil",
|
||||||
"title": "Edit",
|
"title": "Edit",
|
||||||
}
|
}
|
||||||
@@ -653,6 +679,10 @@ class OSINTListBase(ObjectList):
|
|||||||
context["osint_show_actions"] = True
|
context["osint_show_actions"] = True
|
||||||
context["osint_search_url"] = list_url
|
context["osint_search_url"] = list_url
|
||||||
context["osint_result_count"] = context["osint_pagination"].get("count", 0)
|
context["osint_result_count"] = context["osint_pagination"].get("count", 0)
|
||||||
|
context["widget_icon"] = _safe_icon_class(
|
||||||
|
self.request.GET.get("widget_icon"),
|
||||||
|
OSINT_SCOPE_ICONS.get(scope.key, "fa-solid fa-arrows-minimize"),
|
||||||
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@@ -1030,6 +1060,10 @@ class OSINTSearch(LoginRequiredMixin, View):
|
|||||||
"unique": "osint-search-widget",
|
"unique": "osint-search-widget",
|
||||||
"window_content": self.panel_template,
|
"window_content": self.panel_template,
|
||||||
"widget_options": 'gs-w="8" gs-h="14" gs-x="0" gs-y="0" gs-min-w="5"',
|
"widget_options": 'gs-w="8" gs-h="14" gs-x="0" gs-y="0" gs-min-w="5"',
|
||||||
|
"widget_icon": _safe_icon_class(
|
||||||
|
request.GET.get("widget_icon"),
|
||||||
|
"fa-solid fa-magnifying-glass",
|
||||||
|
),
|
||||||
**context,
|
**context,
|
||||||
}
|
}
|
||||||
return render(request, self.widget_template, widget_context)
|
return render(request, self.widget_template, widget_context)
|
||||||
@@ -1054,25 +1088,37 @@ class OSINTWorkspaceTabsWidget(LoginRequiredMixin, View):
|
|||||||
"key": "people",
|
"key": "people",
|
||||||
"label": "People",
|
"label": "People",
|
||||||
"icon": "fa-solid fa-user-group",
|
"icon": "fa-solid fa-user-group",
|
||||||
"widget_url": reverse("people", kwargs={"type": "widget"}),
|
"widget_url": _url_with_query(
|
||||||
|
reverse("people", kwargs={"type": "widget"}),
|
||||||
|
{"widget_icon": "fa-solid fa-user-group"},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "groups",
|
"key": "groups",
|
||||||
"label": "Groups",
|
"label": "Groups",
|
||||||
"icon": "fa-solid fa-users",
|
"icon": "fa-solid fa-users",
|
||||||
"widget_url": reverse("groups", kwargs={"type": "widget"}),
|
"widget_url": _url_with_query(
|
||||||
|
reverse("groups", kwargs={"type": "widget"}),
|
||||||
|
{"widget_icon": "fa-solid fa-users"},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "personas",
|
"key": "personas",
|
||||||
"label": "Personas",
|
"label": "Personas",
|
||||||
"icon": "fa-solid fa-masks-theater",
|
"icon": "fa-solid fa-masks-theater",
|
||||||
"widget_url": reverse("personas", kwargs={"type": "widget"}),
|
"widget_url": _url_with_query(
|
||||||
|
reverse("personas", kwargs={"type": "widget"}),
|
||||||
|
{"widget_icon": "fa-solid fa-masks-theater"},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "manipulations",
|
"key": "manipulations",
|
||||||
"label": "Manipulations",
|
"label": "Manipulations",
|
||||||
"icon": "fa-solid fa-sliders",
|
"icon": "fa-solid fa-sliders",
|
||||||
"widget_url": reverse("manipulations", kwargs={"type": "widget"}),
|
"widget_url": _url_with_query(
|
||||||
|
reverse("manipulations", kwargs={"type": "widget"}),
|
||||||
|
{"widget_icon": "fa-solid fa-sliders"},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
context = {
|
context = {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class PersonCreate(LoginRequiredMixin, ObjectCreate):
|
|||||||
class PersonUpdate(LoginRequiredMixin, ObjectUpdate):
|
class PersonUpdate(LoginRequiredMixin, ObjectUpdate):
|
||||||
model = Person
|
model = Person
|
||||||
form_class = PersonForm
|
form_class = PersonForm
|
||||||
|
window_content = "mixins/window-content/person-form.html"
|
||||||
|
|
||||||
submit_url_name = "person_update"
|
submit_url_name = "person_update"
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
@@ -12,6 +14,7 @@ from core.models import Message, QueuedMessage
|
|||||||
from core.util import logs
|
from core.util import logs
|
||||||
|
|
||||||
log = logs.get_logger("queue")
|
log = logs.get_logger("queue")
|
||||||
|
_INLINE_TARGET_RE = re.compile(r"^#queue-inline-editor-[A-Za-z0-9_-]+$")
|
||||||
|
|
||||||
|
|
||||||
class AcceptMessageAPI(LoginRequiredMixin, APIView):
|
class AcceptMessageAPI(LoginRequiredMixin, APIView):
|
||||||
@@ -92,9 +95,22 @@ class QueueCreate(LoginRequiredMixin, ObjectCreate):
|
|||||||
class QueueUpdate(LoginRequiredMixin, ObjectUpdate):
|
class QueueUpdate(LoginRequiredMixin, ObjectUpdate):
|
||||||
model = QueuedMessage
|
model = QueuedMessage
|
||||||
form_class = QueueForm
|
form_class = QueueForm
|
||||||
|
window_content = "mixins/window-content/queue-form-inline.html"
|
||||||
|
|
||||||
submit_url_name = "queue_update"
|
submit_url_name = "queue_update"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
raw_target = str(self.request.GET.get("hx_target") or "").strip()
|
||||||
|
if _INLINE_TARGET_RE.fullmatch(raw_target):
|
||||||
|
context["submit_target"] = raw_target
|
||||||
|
else:
|
||||||
|
context["submit_target"] = "#modals-here"
|
||||||
|
context["is_inline_edit"] = context["submit_target"].startswith(
|
||||||
|
"#queue-inline-editor-"
|
||||||
|
)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class QueueDelete(LoginRequiredMixin, ObjectDelete):
|
class QueueDelete(LoginRequiredMixin, ObjectDelete):
|
||||||
model = QueuedMessage
|
model = QueuedMessage
|
||||||
|
|||||||
Reference in New Issue
Block a user