Add authors to tasks

This commit is contained in:
2026-03-03 17:43:12 +00:00
parent 18351abb00
commit 8ea2afb259
10 changed files with 190 additions and 23 deletions

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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)) {

View File

@@ -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")

View File

@@ -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,

View File

@@ -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,