Skip to content

Commit 661e2f7

Browse files
authored
Merge pull request #5 from Smartappli/codex/update-to-version-0.9.2
Release 0.9.2: batch segment assignment, refined review-queue filters, and CSV analytics export
2 parents 4f7d384 + ae18274 commit 661e2f7

7 files changed

Lines changed: 369 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 0.9.2
4+
5+
- Added batch assignment for review segments directly from the session player.
6+
- Added finer review queue filters (project, status, assignee, reviewer, and text search).
7+
- Added CSV analytics export for review segments from the review queue.
8+
39
## 0.9.1
410

511
- Added segment-level review queues and session review segments.

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# PyBehaviorLog 0.9.1
1+
# PyBehaviorLog 0.9.2
22

33
PyBehaviorLog is an ASGI-first behavioral observation platform built with Django 6.0.3. It is designed for research teams who need video-assisted coding, live observations, structured ethograms, review workflows, and exportable analytics without being locked into a desktop-only workflow.
44

5-
## What is in this 0.9.1 archive
5+
## What is in this 0.9.2 archive
66

77
This version extends the earlier CowLog/BORIS-inspired foundations with:
88

@@ -125,3 +125,9 @@ This repository is marked as **AGPL-3.0-only**.
125125
- Added segment-level review queues and assignee/reviewer workflow.
126126
- Added session review segments with CRUD screens and queue dashboard.
127127
- Included review segments in JSON and BORIS-like session exports/imports.
128+
129+
## New in 0.9.2
130+
131+
- Batch assignment of review segments from the session player.
132+
- Finer review queue filtering by project, status, assignee/reviewer, and search text.
133+
- CSV export for review-segment analytics from the review queue.

templates/tracker/review_queue.html

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
<h1>{% trans 'Review queue' %}</h1>
1010
<p>{% trans 'Segment-level assignments for coding, checking, and reviewer handoff across projects.' %}</p>
1111
</div>
12+
<div class="header-actions">
13+
<a href="{% url 'tracker:review_queue_export_segment_analytics_csv' %}" role="button" class="secondary">
14+
{% trans 'Export segment analytics (CSV)' %}
15+
</a>
16+
</div>
1217
</header>
1318

1419
<section class="card-grid">
@@ -34,6 +39,57 @@ <h3>{% trans 'Outstanding overall' %}</h3>
3439
<h3>{% trans 'Segments' %}</h3>
3540
<span class="pill">{{ active_filter }}</span>
3641
</div>
42+
<form method="get" class="timeline-toolbar wrap-controls" style="margin-bottom: 1rem;">
43+
<div>
44+
<label for="filter-select">{% trans 'Queue filter' %}</label>
45+
<select id="filter-select" name="filter">
46+
<option value="assigned" {% if active_filter == 'assigned' %}selected{% endif %}>{% trans 'Assigned to me' %}</option>
47+
<option value="review" {% if active_filter == 'review' %}selected{% endif %}>{% trans 'Waiting for my review' %}</option>
48+
<option value="outstanding" {% if active_filter == 'outstanding' %}selected{% endif %}>{% trans 'Outstanding overall' %}</option>
49+
<option value="all" {% if active_filter == 'all' %}selected{% endif %}>{% trans 'All segments' %}</option>
50+
</select>
51+
</div>
52+
<div>
53+
<label for="project-select">{% trans 'Project' %}</label>
54+
<select id="project-select" name="project">
55+
<option value="">{% trans 'All projects' %}</option>
56+
{% for project in projects %}
57+
<option value="{{ project.id }}" {% if project_filter == project.id|stringformat:'s' %}selected{% endif %}>{{ project.name }}</option>
58+
{% endfor %}
59+
</select>
60+
</div>
61+
<div>
62+
<label for="status-select">{% trans 'Status' %}</label>
63+
<select id="status-select" name="status">
64+
<option value="">{% trans 'All statuses' %}</option>
65+
<option value="open" {% if status_filter == 'open' %}selected{% endif %}>{% trans 'Open only' %}</option>
66+
<option value="todo" {% if status_filter == 'todo' %}selected{% endif %}>{% trans 'To do' %}</option>
67+
<option value="in_progress" {% if status_filter == 'in_progress' %}selected{% endif %}>{% trans 'In progress' %}</option>
68+
<option value="done" {% if status_filter == 'done' %}selected{% endif %}>{% trans 'Done' %}</option>
69+
</select>
70+
</div>
71+
<div>
72+
<label for="assignee-select">{% trans 'Assignee' %}</label>
73+
<select id="assignee-select" name="assignee">
74+
<option value="">{% trans 'Any' %}</option>
75+
<option value="me" {% if assignee_filter == 'me' %}selected{% endif %}>{% trans 'Assigned to me' %}</option>
76+
<option value="unassigned" {% if assignee_filter == 'unassigned' %}selected{% endif %}>{% trans 'Unassigned' %}</option>
77+
</select>
78+
</div>
79+
<div>
80+
<label for="reviewer-select">{% trans 'Reviewer' %}</label>
81+
<select id="reviewer-select" name="reviewer">
82+
<option value="">{% trans 'Any' %}</option>
83+
<option value="me" {% if reviewer_filter == 'me' %}selected{% endif %}>{% trans 'Me' %}</option>
84+
<option value="unassigned" {% if reviewer_filter == 'unassigned' %}selected{% endif %}>{% trans 'Unassigned' %}</option>
85+
</select>
86+
</div>
87+
<div>
88+
<label for="search-input">{% trans 'Search' %}</label>
89+
<input id="search-input" type="text" name="q" value="{{ query_filter }}" placeholder="{% trans 'Project, session, segment' %}">
90+
</div>
91+
<div class="align-end"><button type="submit" class="secondary">{% trans 'Apply filters' %}</button></div>
92+
</form>
3793
<div class="table-wrapper">
3894
<table>
3995
<thead><tr><th>{% trans 'Project' %}</th><th>{% trans 'Session' %}</th><th>{% trans 'Segment' %}</th><th>{% trans 'Range' %}</th><th>{% trans 'Status' %}</th><th>{% trans 'Assignee' %}</th><th>{% trans 'Reviewer' %}</th></tr></thead>

templates/tracker/session_player.html

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,48 @@ <h3>{% trans 'Annotations' %}</h3>
235235
<h3>{% trans 'Review segments' %}</h3>
236236
{% if can_review_session %}<a href="{% url 'tracker:segment_create' session.pk %}" class="secondary" role="button">{% trans 'New segment' %}</a>{% endif %}
237237
</div>
238+
{% if can_review_session %}
239+
<form method="post" action="{% url 'tracker:segment_batch_assign' session.pk %}" class="integrity-box compact-box" style="margin-bottom: 1rem;">
240+
{% csrf_token %}
241+
<div class="timeline-toolbar wrap-controls">
242+
<div>
243+
<label for="batch-assignee">{% trans 'Assignee' %}</label>
244+
<select id="batch-assignee" name="assignee">
245+
<option value="">{% trans 'Unassigned' %}</option>
246+
{% for user in segment_form.fields.assignee.queryset %}
247+
<option value="{{ user.pk }}">{{ user.username }}</option>
248+
{% endfor %}
249+
</select>
250+
<label><input type="checkbox" name="set_assignee" value="1"> {% trans 'Apply assignee' %}</label>
251+
</div>
252+
<div>
253+
<label for="batch-reviewer">{% trans 'Reviewer' %}</label>
254+
<select id="batch-reviewer" name="reviewer">
255+
<option value="">{% trans 'Unassigned' %}</option>
256+
{% for user in segment_form.fields.reviewer.queryset %}
257+
<option value="{{ user.pk }}">{{ user.username }}</option>
258+
{% endfor %}
259+
</select>
260+
<label><input type="checkbox" name="set_reviewer" value="1"> {% trans 'Apply reviewer' %}</label>
261+
</div>
262+
<div>
263+
<label for="batch-status">{% trans 'Status' %}</label>
264+
<select id="batch-status" name="status">
265+
{% for value, label in segment_form.fields.status.choices %}
266+
<option value="{{ value }}">{{ label }}</option>
267+
{% endfor %}
268+
</select>
269+
<label><input type="checkbox" name="set_status" value="1"> {% trans 'Apply status' %}</label>
270+
</div>
271+
<div class="align-end"><button type="submit" class="secondary">{% trans 'Batch assign selected segments' %}</button></div>
272+
</div>
238273
<div class="table-wrapper compact-table-wrapper">
239274
<table>
240-
<thead><tr><th>{% trans 'Title' %}</th><th>{% trans 'Range' %}</th><th>{% trans 'Status' %}</th><th>{% trans 'Assignee' %}</th><th>{% trans 'Reviewer' %}</th><th>{% trans 'Actions' %}</th></tr></thead>
275+
<thead><tr>{% if can_review_session %}<th>{% trans 'Select' %}</th>{% endif %}<th>{% trans 'Title' %}</th><th>{% trans 'Range' %}</th><th>{% trans 'Status' %}</th><th>{% trans 'Assignee' %}</th><th>{% trans 'Reviewer' %}</th><th>{% trans 'Actions' %}</th></tr></thead>
241276
<tbody>
242277
{% for segment in segments %}
243278
<tr>
279+
{% if can_review_session %}<td><input type="checkbox" name="segment_ids" value="{{ segment.pk }}"></td>{% endif %}
244280
<td>{{ segment.title }}</td>
245281
<td>{{ segment.start_seconds }}s → {{ segment.end_seconds }}s</td>
246282
<td>{{ segment.get_status_display }}</td>
@@ -253,12 +289,34 @@ <h3>{% trans 'Review segments' %}</h3>
253289
{% else %}—{% endif %}
254290
</td>
255291
</tr>
292+
{% empty %}
293+
<tr><td colspan="{% if can_review_session %}7{% else %}6{% endif %}">{% trans 'No review segments defined yet.' %}</td></tr>
294+
{% endfor %}
295+
</tbody>
296+
</table>
297+
</div>
298+
</form>
299+
{% else %}
300+
<div class="table-wrapper compact-table-wrapper">
301+
<table>
302+
<thead><tr><th>{% trans 'Title' %}</th><th>{% trans 'Range' %}</th><th>{% trans 'Status' %}</th><th>{% trans 'Assignee' %}</th><th>{% trans 'Reviewer' %}</th><th>{% trans 'Actions' %}</th></tr></thead>
303+
<tbody>
304+
{% for segment in segments %}
305+
<tr>
306+
<td>{{ segment.title }}</td>
307+
<td>{{ segment.start_seconds }}s → {{ segment.end_seconds }}s</td>
308+
<td>{{ segment.get_status_display }}</td>
309+
<td>{{ segment.assignee.username|default:'—' }}</td>
310+
<td>{{ segment.reviewer.username|default:'—' }}</td>
311+
<td></td>
312+
</tr>
256313
{% empty %}
257314
<tr><td colspan="6">{% trans 'No review segments defined yet.' %}</td></tr>
258315
{% endfor %}
259316
</tbody>
260317
</table>
261318
</div>
319+
{% endif %}
262320
</article>
263321

264322
<article>

tracker/tests/test_views.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,67 @@ def test_review_queue_and_segment_crud(self):
399399
segment.refresh_from_db()
400400
self.assertEqual(segment.status, ObservationSegment.STATUS_DONE)
401401

402+
def test_segment_batch_assign_and_review_queue_filters_and_export(self):
403+
session = self.project.sessions.create(title='Batch session', observer=self.user, session_kind='live')
404+
first = ObservationSegment.objects.create(
405+
session=session,
406+
title='Intro segment',
407+
start_seconds='0',
408+
end_seconds='5',
409+
status=ObservationSegment.STATUS_TODO,
410+
assignee=self.user,
411+
reviewer=self.reviewer,
412+
)
413+
second = ObservationSegment.objects.create(
414+
session=session,
415+
title='Core segment',
416+
start_seconds='5',
417+
end_seconds='15',
418+
status=ObservationSegment.STATUS_IN_PROGRESS,
419+
assignee=None,
420+
reviewer=self.reviewer,
421+
)
422+
423+
reviewer_client = Client()
424+
reviewer_client.login(username='reviewer', password='pass12345')
425+
batch_response = reviewer_client.post(
426+
reverse('tracker:segment_batch_assign', args=[session.pk]),
427+
data={
428+
'segment_ids': [first.pk, second.pk],
429+
'set_assignee': '1',
430+
'assignee': self.reviewer.pk,
431+
'set_status': '1',
432+
'status': ObservationSegment.STATUS_DONE,
433+
},
434+
)
435+
self.assertEqual(batch_response.status_code, 302)
436+
first.refresh_from_db()
437+
second.refresh_from_db()
438+
self.assertEqual(first.assignee_id, self.reviewer.pk)
439+
self.assertEqual(second.assignee_id, self.reviewer.pk)
440+
self.assertEqual(first.status, ObservationSegment.STATUS_DONE)
441+
self.assertEqual(second.status, ObservationSegment.STATUS_DONE)
442+
443+
queue_response = reviewer_client.get(
444+
reverse('tracker:review_queue'),
445+
{
446+
'filter': 'all',
447+
'status': 'done',
448+
'assignee': 'me',
449+
'q': 'Core',
450+
},
451+
)
452+
self.assertEqual(queue_response.status_code, 200)
453+
self.assertContains(queue_response, 'Core segment')
454+
self.assertNotContains(queue_response, 'Intro segment')
455+
456+
export_response = reviewer_client.get(
457+
reverse('tracker:review_queue_export_segment_analytics_csv')
458+
)
459+
self.assertEqual(export_response.status_code, 200)
460+
self.assertEqual(export_response['Content-Type'], 'text/csv; charset=utf-8')
461+
self.assertIn('Core segment', export_response.content.decode('utf-8'))
462+
402463
def test_session_export_json_contains_segments(self):
403464
session = self.project.sessions.create(title='Segment export', observer=self.user, session_kind='live')
404465
ObservationSegment.objects.create(

tracker/urls.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
path('health/', views.healthcheck, name='healthcheck'),
99
path('release.json', views.release_metadata_json, name='release_metadata_json'),
1010
path('review-queue/', views.review_queue, name='review_queue'),
11+
path(
12+
'review-queue/export/segment-analytics.csv',
13+
views.review_queue_export_segment_analytics_csv,
14+
name='review_queue_export_segment_analytics_csv',
15+
),
1116
path('', views.home, name='home'),
1217
path('projects/import/', views.project_import_create, name='project_import_create'),
1318
path('projects/new/', views.project_create, name='project_create'),
@@ -110,6 +115,11 @@
110115
path('sessions/<int:pk>/delete/', views.session_delete, name='session_delete'),
111116
path('sessions/<int:pk>/import/json/', views.session_import_json, name='session_import_json'),
112117
path('sessions/<int:pk>/segments/new/', views.segment_create, name='segment_create'),
118+
path(
119+
'sessions/<int:pk>/segments/batch-assign/',
120+
views.segment_batch_assign,
121+
name='segment_batch_assign',
122+
),
113123
path(
114124
'sessions/<int:pk>/workflow/', views.session_workflow_action, name='session_workflow_action'
115125
),

0 commit comments

Comments
 (0)