diff --git a/CHANGELOG.md b/CHANGELOG.md index abe2737..80e5e5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.9.3 + +- Refined review queue filtering logic into a shared helper for maintainability. +- Aligned review-segment CSV export with active queue filters used in the UI. +- Bumped release metadata and docs to 0.9.3. +- Documented Granian as the default ASGI command for local startup parity. + ## 0.9.2 - Added batch assignment for review segments directly from the session player. diff --git a/README.md b/README.md index 524cf8f..3e40122 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PyBehaviorLog 0.9.2 +# PyBehaviorLog 0.9.3 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. @@ -57,9 +57,11 @@ source .venv/bin/activate pip install -r requirements.txt python manage.py migrate python manage.py createsuperuser -python manage.py runserver +granian --interface asgi --host 127.0.0.1 --port 8000 config.asgi:application ``` +For ASGI-parity in local development, use Granian (instead of the Django dev server) as shown above. + ## Quick start with Docker ```bash diff --git a/docs/deployment.md b/docs/deployment.md index 3ff2c28..f504f9e 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -56,3 +56,12 @@ coverage run manage.py test coverage report --fail-under=80 pre-commit run --all-files ``` + + +## Local ASGI run + +Use Granian directly to mirror production ASGI behavior: + +```bash +granian --interface asgi --host 127.0.0.1 --port 8000 config.asgi:application +``` diff --git a/tracker/views.py b/tracker/views.py index 2ea8a93..e40dc34 100644 --- a/tracker/views.py +++ b/tracker/views.py @@ -128,7 +128,7 @@ def build_release_metadata() -> dict: """Return a small machine-readable release description for health and ops tooling.""" return { 'application': 'PyBehaviorLog', - 'version': '0.9.2', + 'version': '0.9.3', 'django_target': '6.0.3', 'python_minimum': '3.13', 'asgi': True, @@ -476,6 +476,58 @@ def build_review_queue(user) -> dict: }, } +def _filter_review_segments( + rows: list[ObservationSegment], + *, + user, + project_filter: str = '', + status_filter: str = '', + assignee_filter: str = '', + reviewer_filter: str = '', + query_filter: str = '', +) -> list[ObservationSegment]: + filtered = list(rows) + if project_filter.isdigit(): + filtered = [item for item in filtered if item.session.project_id == int(project_filter)] + + if status_filter == 'open': + filtered = [item for item in filtered if item.status != ObservationSegment.STATUS_DONE] + elif status_filter in { + ObservationSegment.STATUS_TODO, + ObservationSegment.STATUS_IN_PROGRESS, + ObservationSegment.STATUS_DONE, + }: + filtered = [item for item in filtered if item.status == status_filter] + + if assignee_filter == 'me': + filtered = [item for item in filtered if item.assignee_id == user.id] + elif assignee_filter == 'unassigned': + filtered = [item for item in filtered if item.assignee_id is None] + + if reviewer_filter == 'me': + filtered = [item for item in filtered if item.reviewer_id == user.id] + elif reviewer_filter == 'unassigned': + filtered = [item for item in filtered if item.reviewer_id is None] + + query_normalized = query_filter.strip().lower() + if query_normalized: + filtered = [ + item + for item in filtered + if query_normalized in item.title.lower() + or query_normalized in item.session.title.lower() + or query_normalized in item.session.project.name.lower() + ] + + return filtered + + +def _review_queue_project_choices(queue_rows: list[ObservationSegment]) -> list[Project]: + by_id: dict[int, Project] = {} + for item in queue_rows: + by_id[item.session.project_id] = item.session.project + return sorted(by_id.values(), key=lambda project: project.name.lower()) + def _get_owned_category(user, pk: int) -> BehaviorCategory: category = get_object_or_404(BehaviorCategory.objects.select_related('project'), pk=pk)