From fa9dbd4ef880d99578fe74a5eb83e564a2745d00 Mon Sep 17 00:00:00 2001 From: Sara Burns Date: Thu, 8 Oct 2026 13:45:36 -0400 Subject: [PATCH 1/2] feat: include video start & end time in course block --- .../sinks/course_overview_sink.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/platform_plugin_aspects/sinks/course_overview_sink.py b/src/platform_plugin_aspects/sinks/course_overview_sink.py index 62cf879f..acd11cc3 100644 --- a/src/platform_plugin_aspects/sinks/course_overview_sink.py +++ b/src/platform_plugin_aspects/sinks/course_overview_sink.py @@ -177,6 +177,14 @@ def serialize_xblock(self, item, detached_xblock_types, dump_id, time_last_dumpe "completion_mode": getattr(item, "completion_mode", ""), } + if block_type == "video": + json_data["video_start_time"] = XBlockSink.get_seconds( + getattr(item, "start_time", None) + ) + json_data["video_end_time"] = XBlockSink.get_seconds( + getattr(item, "end_time", None) + ) + # Core table data, if things change here it's a big deal. serialized_block = { "org": course_key.org, @@ -194,6 +202,18 @@ def serialize_xblock(self, item, detached_xblock_types, dump_id, time_last_dumpe return serialized_block + @staticmethod + def get_seconds(value): + """ + Convert a video RelativeTime field (a timedelta) to seconds. + Args: + value: a timedelta, or None if the field is not set. + Returns: the number of seconds as a float, 0.0 if not set. + """ + if not value: + return 0.0 + return value.total_seconds() + @staticmethod def strip_branch_and_version(location): """ From e1bd433f5a27bc7d0676ad2ae679a813756e477c Mon Sep 17 00:00:00 2001 From: Sara Burns Date: Fri, 9 Oct 2026 09:48:29 -0400 Subject: [PATCH 2/2] test: add tests for video clip start and end times Co-Authored-By: Claude Opus 5.5 --- .../sinks/tests/test_course_overview_sink.py | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/platform_plugin_aspects/sinks/tests/test_course_overview_sink.py b/src/platform_plugin_aspects/sinks/tests/test_course_overview_sink.py index 2bb58011..0bbf35d3 100644 --- a/src/platform_plugin_aspects/sinks/tests/test_course_overview_sink.py +++ b/src/platform_plugin_aspects/sinks/tests/test_course_overview_sink.py @@ -4,7 +4,7 @@ import json import logging -from datetime import datetime +from datetime import datetime, timedelta from unittest.mock import MagicMock, patch import pytest @@ -17,6 +17,7 @@ from platform_plugin_aspects.sinks import CourseOverviewSink, XBlockSink from platform_plugin_aspects.tasks import dump_course_to_clickhouse from test_utils.helpers import ( + FakeXBlock, check_block_csv_matcher, check_overview_csv_matcher, course_factory, @@ -413,3 +414,40 @@ def _check_item_serialized_location( _check_item_serialized_location(results[31], 0, "completable") _check_item_serialized_location(results[32], 0, "aggregator") _check_item_serialized_location(results[33], 0, "excluded") + + +@pytest.mark.parametrize( + "start_time,end_time,expected_start,expected_end", + [ + (timedelta(seconds=60), timedelta(seconds=240), 60.0, 240.0), + (timedelta(seconds=90.5), None, 90.5, 0.0), + (None, timedelta(seconds=30), 0.0, 30.0), + (timedelta(0), timedelta(0), 0.0, 0.0), + ], +) +def test_xblock_video_clip_times(start_time, end_time, expected_start, expected_end): + """ + Test that video start and end times serialize as seconds. + """ + block = FakeXBlock("Video", block_type="video") + block.start_time = start_time + block.end_time = end_time + + sink = XBlockSink(connection_overrides={}, log=MagicMock()) + result = sink.serialize_xblock(block, [], "xyz", "2023-09-05") + + assert result["xblock_data_json"]["video_start_time"] == expected_start + assert result["xblock_data_json"]["video_end_time"] == expected_end + + +def test_xblock_non_video_has_no_clip_times(): + """ + Test that non-video blocks do not get video clip times. + """ + block = FakeXBlock("Problem", block_type="problem") + + sink = XBlockSink(connection_overrides={}, log=MagicMock()) + result = sink.serialize_xblock(block, [], "xyz", "2023-09-05") + + assert "video_start_time" not in result["xblock_data_json"] + assert "video_end_time" not in result["xblock_data_json"]