Add authors to tasks
This commit is contained in:
@@ -3,6 +3,12 @@
|
|||||||
<section class="section"><div class="container">
|
<section class="section"><div class="container">
|
||||||
<h1 class="title is-4">Task #{{ task.reference_code }}: {{ task.title }}</h1>
|
<h1 class="title is-4">Task #{{ task.reference_code }}: {{ task.title }}</h1>
|
||||||
<p class="subtitle is-6">{{ task.project.name }}{% if task.epic %} / {{ task.epic.name }}{% endif %} · {{ task.status_snapshot }}</p>
|
<p class="subtitle is-6">{{ task.project.name }}{% if task.epic %} / {{ task.epic.name }}{% endif %} · {{ task.status_snapshot }}</p>
|
||||||
|
<p class="is-size-7 has-text-grey" style="margin-top:-0.65rem; margin-bottom: 0.65rem;">
|
||||||
|
Created by {{ task.creator_label|default:"Unknown" }}
|
||||||
|
{% if task.origin_message_id %}
|
||||||
|
· Source message <code>{{ task.origin_message_id }}</code>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
<div class="buttons"><a class="button is-small is-light" href="{% url 'tasks_hub' %}">Back</a></div>
|
<div class="buttons"><a class="button is-small is-light" href="{% url 'tasks_hub' %}">Back</a></div>
|
||||||
<article class="box">
|
<article class="box">
|
||||||
<h2 class="title is-6">Events</h2>
|
<h2 class="title is-6">Events</h2>
|
||||||
@@ -10,7 +16,17 @@
|
|||||||
<thead><tr><th>When</th><th>Type</th><th>Actor</th><th>Payload</th></tr></thead>
|
<thead><tr><th>When</th><th>Type</th><th>Actor</th><th>Payload</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in events %}
|
{% for row in events %}
|
||||||
<tr><td>{{ row.created_at }}</td><td>{{ row.event_type }}</td><td>{{ row.actor_identifier }}</td><td><code>{{ row.payload }}</code></td></tr>
|
<tr>
|
||||||
|
<td>{{ row.created_at }}</td>
|
||||||
|
<td>{{ row.event_type }}</td>
|
||||||
|
<td>
|
||||||
|
{{ row.actor_display|default:"Unknown" }}
|
||||||
|
{% if row.actor_identifier and row.actor_identifier != row.actor_display %}
|
||||||
|
<div class="has-text-grey"><code>{{ row.actor_identifier }}</code></div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td><code>{{ row.payload }}</code></td>
|
||||||
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr><td colspan="4">No events.</td></tr>
|
<tr><td colspan="4">No events.</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -4,9 +4,15 @@
|
|||||||
<h1 class="title is-4">Epic: {{ epic.name }}</h1>
|
<h1 class="title is-4">Epic: {{ epic.name }}</h1>
|
||||||
<div class="buttons"><a class="button is-small is-light" href="{% url 'tasks_project' project_id=epic.project_id %}">Back to project</a></div>
|
<div class="buttons"><a class="button is-small is-light" href="{% url 'tasks_project' project_id=epic.project_id %}">Back to project</a></div>
|
||||||
<article class="box">
|
<article class="box">
|
||||||
<ul>
|
<ul class="is-size-7">
|
||||||
{% for row in tasks %}
|
{% for row in tasks %}
|
||||||
<li><a href="{% url 'tasks_task' task_id=row.id %}">#{{ row.reference_code }} {{ row.title }}</a></li>
|
<li>
|
||||||
|
<a href="{% url 'tasks_task' task_id=row.id %}">#{{ row.reference_code }} {{ row.title }}</a>
|
||||||
|
<span class="has-text-grey">· by {{ row.creator_label|default:"Unknown" }}</span>
|
||||||
|
{% if row.creator_identifier %}
|
||||||
|
<code>{{ row.creator_identifier }}</code>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<li>No tasks.</li>
|
<li>No tasks.</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -85,18 +85,24 @@
|
|||||||
<article class="box">
|
<article class="box">
|
||||||
<h2 class="title is-6">Derived Tasks</h2>
|
<h2 class="title is-6">Derived Tasks</h2>
|
||||||
<table class="table is-fullwidth is-striped is-size-7">
|
<table class="table is-fullwidth is-striped is-size-7">
|
||||||
<thead><tr><th>Ref</th><th>Title</th><th>Project</th><th>Status</th><th></th></tr></thead>
|
<thead><tr><th>Ref</th><th>Title</th><th>Created By</th><th>Project</th><th>Status</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in tasks %}
|
{% for row in tasks %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>#{{ row.reference_code }}</td>
|
<td>#{{ row.reference_code }}</td>
|
||||||
<td>{{ row.title }}</td>
|
<td>{{ row.title }}</td>
|
||||||
|
<td>
|
||||||
|
{{ row.creator_label|default:"Unknown" }}
|
||||||
|
{% if row.creator_identifier %}
|
||||||
|
<div class="has-text-grey"><code>{{ row.creator_identifier }}</code></div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>{{ row.project.name }}{% if row.epic %} / {{ row.epic.name }}{% endif %}</td>
|
<td>{{ row.project.name }}{% if row.epic %} / {{ row.epic.name }}{% endif %}</td>
|
||||||
<td>{{ row.status_snapshot }}</td>
|
<td>{{ row.status_snapshot }}</td>
|
||||||
<td><a class="button is-small is-light" href="{% url 'tasks_task' task_id=row.id %}">Open</a></td>
|
<td><a class="button is-small is-light" href="{% url 'tasks_task' task_id=row.id %}">Open</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr><td colspan="5">No tasks yet.</td></tr>
|
<tr><td colspan="6">No tasks yet.</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -134,18 +134,24 @@
|
|||||||
<article class="box">
|
<article class="box">
|
||||||
<h2 class="title is-6">Recent Derived Tasks</h2>
|
<h2 class="title is-6">Recent Derived Tasks</h2>
|
||||||
<table class="table is-fullwidth is-striped is-size-7">
|
<table class="table is-fullwidth is-striped is-size-7">
|
||||||
<thead><tr><th>Ref</th><th>Title</th><th>Project</th><th>Status</th><th></th></tr></thead>
|
<thead><tr><th>Ref</th><th>Title</th><th>Created By</th><th>Project</th><th>Status</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in tasks %}
|
{% for row in tasks %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>#{{ row.reference_code }}</td>
|
<td>#{{ row.reference_code }}</td>
|
||||||
<td>{{ row.title }}</td>
|
<td>{{ row.title }}</td>
|
||||||
|
<td>
|
||||||
|
{{ row.creator_label|default:"Unknown" }}
|
||||||
|
{% if row.creator_identifier %}
|
||||||
|
<div class="has-text-grey"><code>{{ row.creator_identifier }}</code></div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>{{ row.project.name }}{% if row.epic %} / {{ row.epic.name }}{% endif %}</td>
|
<td>{{ row.project.name }}{% if row.epic %} / {{ row.epic.name }}{% endif %}</td>
|
||||||
<td>{{ row.status_snapshot }}</td>
|
<td>{{ row.status_snapshot }}</td>
|
||||||
<td><a class="button is-small is-light" href="{% url 'tasks_task' task_id=row.id %}">Open</a></td>
|
<td><a class="button is-small is-light" href="{% url 'tasks_task' task_id=row.id %}">Open</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr><td colspan="5">No derived tasks yet.</td></tr>
|
<tr><td colspan="6">No derived tasks yet.</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -55,17 +55,23 @@
|
|||||||
<article class="box">
|
<article class="box">
|
||||||
<h2 class="title is-6">Tasks</h2>
|
<h2 class="title is-6">Tasks</h2>
|
||||||
<table class="table is-fullwidth is-striped is-size-7">
|
<table class="table is-fullwidth is-striped is-size-7">
|
||||||
<thead><tr><th>Ref</th><th>Title</th><th>Epic</th><th></th></tr></thead>
|
<thead><tr><th>Ref</th><th>Title</th><th>Created By</th><th>Epic</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in tasks %}
|
{% for row in tasks %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>#{{ row.reference_code }}</td>
|
<td>#{{ row.reference_code }}</td>
|
||||||
<td>{{ row.title }}</td>
|
<td>{{ row.title }}</td>
|
||||||
|
<td>
|
||||||
|
{{ row.creator_label|default:"Unknown" }}
|
||||||
|
{% if row.creator_identifier %}
|
||||||
|
<div class="has-text-grey"><code>{{ row.creator_identifier }}</code></div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>{% if row.epic %}{{ row.epic.name }}{% else %}-{% endif %}</td>
|
<td>{% if row.epic %}{{ row.epic.name }}{% else %}-{% endif %}</td>
|
||||||
<td><a class="button is-small is-light" href="{% url 'tasks_task' task_id=row.id %}">Open</a></td>
|
<td><a class="button is-small is-light" href="{% url 'tasks_task' task_id=row.id %}">Open</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr><td colspan="4">No tasks.</td></tr>
|
<tr><td colspan="5">No tasks.</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -362,7 +362,13 @@
|
|||||||
<div class="column is-12">
|
<div class="column is-12">
|
||||||
<section class="tasks-panel">
|
<section class="tasks-panel">
|
||||||
<h3 class="title is-7">External Chat Links</h3>
|
<h3 class="title is-7">External Chat Links</h3>
|
||||||
<p class="help">Map a GIA contact identifier to one Codex conversation/session so task-sync updates are routed to the correct Codex thread.</p>
|
<p class="help">Map one GIA contact to one Codex thread for task-sync routing.</p>
|
||||||
|
<details class="tasks-external-help">
|
||||||
|
<summary class="is-size-7">More info</summary>
|
||||||
|
<p class="help">
|
||||||
|
This is task-sync only. It does not mirror full chat history. The link tells the Codex worker which Codex conversation/session should receive updates for tasks from that contact/group.
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
{% if external_link_scoped %}
|
{% if external_link_scoped %}
|
||||||
<article class="message is-info is-light tasks-link-scope-note">
|
<article class="message is-info is-light tasks-link-scope-note">
|
||||||
<div class="message-body">
|
<div class="message-body">
|
||||||
@@ -390,7 +396,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="column is-12-mobile is-8-tablet is-5-desktop">
|
<div class="column is-12-mobile is-8-tablet is-5-desktop">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-size-7">Contact Identifier</label>
|
<label class="label is-size-7">Contact</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<div class="select is-small is-fullwidth">
|
<div class="select is-small is-fullwidth">
|
||||||
<select name="person_identifier_id">
|
<select name="person_identifier_id">
|
||||||
@@ -401,16 +407,16 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help">Choose which contact/group in GIA this Codex chat mapping belongs to.</p>
|
<p class="help">Which GIA contact/group this link belongs to.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-12-mobile is-8-tablet is-3-desktop">
|
<div class="column is-12-mobile is-8-tablet is-3-desktop">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-size-7">External Chat ID</label>
|
<label class="label is-size-7">Codex Chat ID</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input is-small" name="external_chat_id" placeholder="codex-chat-...">
|
<input class="input is-small" name="external_chat_id" placeholder="codex-chat-...">
|
||||||
</div>
|
</div>
|
||||||
<p class="help">Use the Codex conversation/session ID (the stable ID Codex worker should target for task updates).</p>
|
<p class="help">Stable Codex conversation/session ID.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-6-mobile is-4-tablet is-2-desktop">
|
<div class="column is-6-mobile is-4-tablet is-2-desktop">
|
||||||
@@ -485,6 +491,14 @@
|
|||||||
.tasks-settings-page .tasks-link-scope-note {
|
.tasks-settings-page .tasks-link-scope-note {
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
.tasks-settings-page .tasks-external-help {
|
||||||
|
margin-bottom: 0.55rem;
|
||||||
|
}
|
||||||
|
.tasks-settings-page .tasks-external-help > summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #4a4a4a;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
.tasks-settings-page .tasks-external-link-columns .field {
|
.tasks-settings-page .tasks-external-link-columns .field {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -343,6 +343,8 @@
|
|||||||
<label class="checkbox is-size-7"><input type="checkbox" class="compose-export-field" value="text" checked> Text</label>
|
<label class="checkbox is-size-7"><input type="checkbox" class="compose-export-field" value="text" checked> Text</label>
|
||||||
<label class="checkbox is-size-7"><input type="checkbox" class="compose-export-field" value="time" checked> Time</label>
|
<label class="checkbox is-size-7"><input type="checkbox" class="compose-export-field" value="time" checked> Time</label>
|
||||||
<label class="checkbox is-size-7"><input type="checkbox" class="compose-export-field" value="sender" checked> Sender</label>
|
<label class="checkbox is-size-7"><input type="checkbox" class="compose-export-field" value="sender" checked> Sender</label>
|
||||||
|
<label class="checkbox is-size-7"><input type="checkbox" class="compose-export-field" value="author" checked> Author</label>
|
||||||
|
<label class="checkbox is-size-7"><input type="checkbox" class="compose-export-field" value="author_identifier"> Author Identifier</label>
|
||||||
<label class="checkbox is-size-7"><input type="checkbox" class="compose-export-field" value="source_service"> Source</label>
|
<label class="checkbox is-size-7"><input type="checkbox" class="compose-export-field" value="source_service"> Source</label>
|
||||||
<label class="checkbox is-size-7"><input type="checkbox" class="compose-export-field" value="source_label"> Source Label</label>
|
<label class="checkbox is-size-7"><input type="checkbox" class="compose-export-field" value="source_label"> Source Label</label>
|
||||||
<label class="checkbox is-size-7"><input type="checkbox" class="compose-export-field" value="direction"> Direction</label>
|
<label class="checkbox is-size-7"><input type="checkbox" class="compose-export-field" value="direction"> Direction</label>
|
||||||
@@ -398,7 +400,7 @@
|
|||||||
data-engage-preview-url="{{ compose_engage_preview_url }}"
|
data-engage-preview-url="{{ compose_engage_preview_url }}"
|
||||||
data-engage-send-url="{{ compose_engage_send_url }}">
|
data-engage-send-url="{{ compose_engage_send_url }}">
|
||||||
{% for msg in serialized_messages %}
|
{% for msg in serialized_messages %}
|
||||||
<div class="compose-row {% if msg.outgoing %}is-out{% else %}is-in{% endif %}" data-ts="{{ msg.ts }}" data-message-id="{{ msg.id }}" data-author="{{ msg.author|default:''|escape }}" data-display-ts="{{ msg.display_ts|escape }}" data-source-service="{{ msg.source_service|default:''|escape }}" data-source-label="{{ msg.source_label|default:''|escape }}" data-source-message-id="{{ msg.source_message_id|default:''|escape }}" data-direction="{% if msg.outgoing %}outgoing{% else %}incoming{% endif %}"{% if msg.reply_to_id %} data-reply-to-id="{{ msg.reply_to_id }}"{% endif %} data-reply-snippet="{{ msg.display_text|default:msg.text|default:''|truncatechars:120|escape }}">
|
<div class="compose-row {% if msg.outgoing %}is-out{% else %}is-in{% endif %}" data-ts="{{ msg.ts }}" data-message-id="{{ msg.id }}" data-author="{{ msg.author|default:''|escape }}" data-sender-uuid="{{ msg.sender_uuid|default:''|escape }}" data-display-ts="{{ msg.display_ts|escape }}" data-source-service="{{ msg.source_service|default:''|escape }}" data-source-label="{{ msg.source_label|default:''|escape }}" data-source-message-id="{{ msg.source_message_id|default:''|escape }}" data-direction="{% if msg.outgoing %}outgoing{% else %}incoming{% endif %}"{% if msg.reply_to_id %} data-reply-to-id="{{ msg.reply_to_id }}"{% endif %} data-reply-snippet="{{ msg.display_text|default:msg.text|default:''|truncatechars:120|escape }}">
|
||||||
{% if msg.gap_fragments %}
|
{% if msg.gap_fragments %}
|
||||||
{% with gap=msg.gap_fragments.0 %}
|
{% with gap=msg.gap_fragments.0 %}
|
||||||
<p
|
<p
|
||||||
@@ -2785,6 +2787,7 @@
|
|||||||
);
|
);
|
||||||
row.dataset.author = String(msg.author || "");
|
row.dataset.author = String(msg.author || "");
|
||||||
row.dataset.displayTs = String(msg.display_ts || msg.ts || "");
|
row.dataset.displayTs = String(msg.display_ts || msg.ts || "");
|
||||||
|
row.dataset.senderUuid = String(msg.sender_uuid || "");
|
||||||
row.dataset.sourceService = String(msg.source_service || "");
|
row.dataset.sourceService = String(msg.source_service || "");
|
||||||
row.dataset.sourceLabel = String(msg.source_label || "");
|
row.dataset.sourceLabel = String(msg.source_label || "");
|
||||||
row.dataset.sourceMessageId = String(msg.source_message_id || "");
|
row.dataset.sourceMessageId = String(msg.source_message_id || "");
|
||||||
@@ -3451,11 +3454,14 @@
|
|||||||
const text = String(bodyNode ? bodyNode.textContent || "" : "").trim();
|
const text = String(bodyNode ? bodyNode.textContent || "" : "").trim();
|
||||||
const authorRaw = String(row.dataset.author || "").trim();
|
const authorRaw = String(row.dataset.author || "").trim();
|
||||||
const author = authorRaw || (row.classList.contains("is-out") ? "USER" : "CONTACT");
|
const author = authorRaw || (row.classList.contains("is-out") ? "USER" : "CONTACT");
|
||||||
|
const authorIdentifier = String(row.dataset.senderUuid || "").trim();
|
||||||
const when = String(row.dataset.displayTs || "").trim();
|
const when = String(row.dataset.displayTs || "").trim();
|
||||||
return {
|
return {
|
||||||
message_id: String(row.dataset.messageId || ""),
|
message_id: String(row.dataset.messageId || ""),
|
||||||
ts: toInt(row.dataset.ts || 0),
|
ts: toInt(row.dataset.ts || 0),
|
||||||
sender: author,
|
sender: author,
|
||||||
|
author: author,
|
||||||
|
author_identifier: authorIdentifier,
|
||||||
time: when,
|
time: when,
|
||||||
direction: String(row.dataset.direction || (row.classList.contains("is-out") ? "outgoing" : "incoming")),
|
direction: String(row.dataset.direction || (row.classList.contains("is-out") ? "outgoing" : "incoming")),
|
||||||
source_service: String(row.dataset.sourceService || "").trim(),
|
source_service: String(row.dataset.sourceService || "").trim(),
|
||||||
@@ -3467,7 +3473,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selectedExportFields = function () {
|
const selectedExportFields = function () {
|
||||||
const defaults = ["text", "time", "sender"];
|
const defaults = ["text", "time", "sender", "author"];
|
||||||
const picked = exportFieldChecks
|
const picked = exportFieldChecks
|
||||||
.filter(function (node) { return !!(node && node.checked); })
|
.filter(function (node) { return !!(node && node.checked); })
|
||||||
.map(function (node) { return String(node.value || "").trim(); })
|
.map(function (node) { return String(node.value || "").trim(); })
|
||||||
@@ -3486,7 +3492,7 @@
|
|||||||
seen.add(key);
|
seen.add(key);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
const priority = ["time", "sender"];
|
const priority = ["time", "sender", "author"];
|
||||||
const ordered = [];
|
const ordered = [];
|
||||||
priority.forEach(function (key) {
|
priority.forEach(function (key) {
|
||||||
if (normalized.includes(key)) {
|
if (normalized.includes(key)) {
|
||||||
|
|||||||
@@ -3,7 +3,17 @@ from __future__ import annotations
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from core.models import ChatTaskSource, Person, PersonIdentifier, TaskEpic, TaskProject, User
|
from core.models import (
|
||||||
|
ChatSession,
|
||||||
|
ChatTaskSource,
|
||||||
|
DerivedTask,
|
||||||
|
Message,
|
||||||
|
Person,
|
||||||
|
PersonIdentifier,
|
||||||
|
TaskEpic,
|
||||||
|
TaskProject,
|
||||||
|
User,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TasksPagesManagementTests(TestCase):
|
class TasksPagesManagementTests(TestCase):
|
||||||
@@ -116,3 +126,31 @@ class TasksPagesManagementTests(TestCase):
|
|||||||
enabled=True,
|
enabled=True,
|
||||||
).exists()
|
).exists()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_tasks_hub_shows_human_creator_label(self):
|
||||||
|
project = TaskProject.objects.create(user=self.user, name="Creator Test")
|
||||||
|
session = ChatSession.objects.create(user=self.user, identifier=self.pid_signal)
|
||||||
|
origin = Message.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
session=session,
|
||||||
|
ts=1_700_000_000_000,
|
||||||
|
text="task: write docs",
|
||||||
|
sender_uuid="+15551230000",
|
||||||
|
custom_author="OTHER",
|
||||||
|
source_service="signal",
|
||||||
|
source_chat_id="+15551230000",
|
||||||
|
)
|
||||||
|
DerivedTask.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
project=project,
|
||||||
|
title="Write docs",
|
||||||
|
source_service="signal",
|
||||||
|
source_channel="+15551230000",
|
||||||
|
origin_message=origin,
|
||||||
|
reference_code="1",
|
||||||
|
status_snapshot="open",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("tasks_hub"))
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
self.assertContains(response, "Scope Person")
|
||||||
|
|||||||
@@ -550,6 +550,7 @@ def _serialize_message(msg: Message) -> dict:
|
|||||||
"image_urls": image_urls,
|
"image_urls": image_urls,
|
||||||
"hide_text": hide_text,
|
"hide_text": hide_text,
|
||||||
"author": author,
|
"author": author,
|
||||||
|
"sender_uuid": sender_uuid_value,
|
||||||
"outgoing": _is_outgoing(msg),
|
"outgoing": _is_outgoing(msg),
|
||||||
"source_service": source_service,
|
"source_service": source_service,
|
||||||
"source_label": source_label,
|
"source_label": source_label,
|
||||||
|
|||||||
@@ -174,6 +174,53 @@ def _service_label(service: str) -> str:
|
|||||||
return labels.get(key, key.title() if key else "Unknown")
|
return labels.get(key, key.title() if key else "Unknown")
|
||||||
|
|
||||||
|
|
||||||
|
def _creator_label_for_message(user, service: str, message) -> str:
|
||||||
|
msg = message
|
||||||
|
if msg is None:
|
||||||
|
return "Unknown"
|
||||||
|
author_raw = str(getattr(msg, "custom_author", "") or "").strip()
|
||||||
|
author_key = author_raw.upper()
|
||||||
|
sender_identifier = str(getattr(msg, "sender_uuid", "") or "").strip()
|
||||||
|
|
||||||
|
if author_key == "USER":
|
||||||
|
return "You"
|
||||||
|
if author_key == "BOT":
|
||||||
|
return "System Bot"
|
||||||
|
|
||||||
|
if sender_identifier:
|
||||||
|
variants = _person_identifier_scope_variants(service, sender_identifier)
|
||||||
|
person_identifier = (
|
||||||
|
PersonIdentifier.objects.filter(
|
||||||
|
user=user,
|
||||||
|
service=str(service or "").strip().lower(),
|
||||||
|
identifier__in=variants or [sender_identifier],
|
||||||
|
)
|
||||||
|
.select_related("person")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if person_identifier is not None:
|
||||||
|
person_name = str(getattr(person_identifier.person, "name", "") or "").strip()
|
||||||
|
if person_name:
|
||||||
|
return person_name
|
||||||
|
return sender_identifier
|
||||||
|
|
||||||
|
if author_raw:
|
||||||
|
if author_key == "OTHER":
|
||||||
|
return "Other Participant"
|
||||||
|
return author_raw
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_task_creator_labels(user, task_rows):
|
||||||
|
rows = list(task_rows or [])
|
||||||
|
for row in rows:
|
||||||
|
origin = getattr(row, "origin_message", None)
|
||||||
|
service_key = str(getattr(row, "source_service", "") or "").strip().lower()
|
||||||
|
row.creator_label = _creator_label_for_message(user, service_key, origin)
|
||||||
|
row.creator_identifier = str(getattr(origin, "sender_uuid", "") or "").strip()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def _provider_row_map(user):
|
def _provider_row_map(user):
|
||||||
return {
|
return {
|
||||||
str(row.provider or "").strip().lower(): row
|
str(row.provider or "").strip().lower(): row
|
||||||
@@ -410,9 +457,10 @@ class TasksHub(LoginRequiredMixin, View):
|
|||||||
)
|
)
|
||||||
tasks = (
|
tasks = (
|
||||||
DerivedTask.objects.filter(user=request.user)
|
DerivedTask.objects.filter(user=request.user)
|
||||||
.select_related("project", "epic")
|
.select_related("project", "epic", "origin_message")
|
||||||
.order_by("-created_at")[:200]
|
.order_by("-created_at")[:200]
|
||||||
)
|
)
|
||||||
|
tasks = _apply_task_creator_labels(request.user, tasks)
|
||||||
selected_project = None
|
selected_project = None
|
||||||
if scope["selected_project_id"]:
|
if scope["selected_project_id"]:
|
||||||
selected_project = TaskProject.objects.filter(
|
selected_project = TaskProject.objects.filter(
|
||||||
@@ -547,9 +595,10 @@ class TaskProjectDetail(LoginRequiredMixin, View):
|
|||||||
def _context(self, request, project):
|
def _context(self, request, project):
|
||||||
tasks = (
|
tasks = (
|
||||||
DerivedTask.objects.filter(user=request.user, project=project)
|
DerivedTask.objects.filter(user=request.user, project=project)
|
||||||
.select_related("epic")
|
.select_related("epic", "origin_message")
|
||||||
.order_by("-created_at")
|
.order_by("-created_at")
|
||||||
)
|
)
|
||||||
|
tasks = _apply_task_creator_labels(request.user, tasks)
|
||||||
epics = (
|
epics = (
|
||||||
TaskEpic.objects.filter(project=project)
|
TaskEpic.objects.filter(project=project)
|
||||||
.annotate(task_count=Count("derived_tasks"))
|
.annotate(task_count=Count("derived_tasks"))
|
||||||
@@ -613,9 +662,10 @@ class TaskEpicDetail(LoginRequiredMixin, View):
|
|||||||
epic = get_object_or_404(TaskEpic, id=epic_id, project__user=request.user)
|
epic = get_object_or_404(TaskEpic, id=epic_id, project__user=request.user)
|
||||||
tasks = (
|
tasks = (
|
||||||
DerivedTask.objects.filter(user=request.user, epic=epic)
|
DerivedTask.objects.filter(user=request.user, epic=epic)
|
||||||
.select_related("project")
|
.select_related("project", "origin_message")
|
||||||
.order_by("-created_at")
|
.order_by("-created_at")
|
||||||
)
|
)
|
||||||
|
tasks = _apply_task_creator_labels(request.user, tasks)
|
||||||
return render(request, self.template_name, {"epic": epic, "tasks": tasks})
|
return render(request, self.template_name, {"epic": epic, "tasks": tasks})
|
||||||
|
|
||||||
|
|
||||||
@@ -639,9 +689,10 @@ class TaskGroupDetail(LoginRequiredMixin, View):
|
|||||||
source_service__in=service_keys,
|
source_service__in=service_keys,
|
||||||
source_channel__in=variants,
|
source_channel__in=variants,
|
||||||
)
|
)
|
||||||
.select_related("project", "epic")
|
.select_related("project", "epic", "origin_message")
|
||||||
.order_by("-created_at")
|
.order_by("-created_at")
|
||||||
)
|
)
|
||||||
|
tasks = _apply_task_creator_labels(request.user, tasks)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
self.template_name,
|
self.template_name,
|
||||||
@@ -717,11 +768,28 @@ class TaskDetail(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
def get(self, request, task_id):
|
def get(self, request, task_id):
|
||||||
task = get_object_or_404(
|
task = get_object_or_404(
|
||||||
DerivedTask.objects.select_related("project", "epic"),
|
DerivedTask.objects.select_related("project", "epic", "origin_message"),
|
||||||
id=task_id,
|
id=task_id,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
)
|
)
|
||||||
events = task.events.select_related("source_message").order_by("-created_at")
|
events = task.events.select_related("source_message").order_by("-created_at")
|
||||||
|
for row in events:
|
||||||
|
service_hint = str(getattr(task, "source_service", "") or "").strip().lower()
|
||||||
|
event_source_message = getattr(row, "source_message", None)
|
||||||
|
row.actor_display = _creator_label_for_message(
|
||||||
|
request.user,
|
||||||
|
service_hint,
|
||||||
|
event_source_message,
|
||||||
|
)
|
||||||
|
if row.actor_display == "Unknown":
|
||||||
|
raw_actor = str(getattr(row, "actor_identifier", "") or "").strip()
|
||||||
|
if raw_actor:
|
||||||
|
row.actor_display = raw_actor
|
||||||
|
task.creator_label = _creator_label_for_message(
|
||||||
|
request.user,
|
||||||
|
str(getattr(task, "source_service", "") or "").strip().lower(),
|
||||||
|
getattr(task, "origin_message", None),
|
||||||
|
)
|
||||||
sync_events = task.external_sync_events.order_by("-created_at")
|
sync_events = task.external_sync_events.order_by("-created_at")
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
|||||||
Reference in New Issue
Block a user