From 1b2431e1d5f5a967dea745c9518359855d9f54c4 Mon Sep 17 00:00:00 2001 From: Andy Shultz Date: Mon, 28 Aug 2023 11:59:54 -0400 Subject: [PATCH] feat: generate last-updated timestamp into ai hook div also refactors to test that div and switches timestamps to ISO format generally --- CHANGELOG.rst | 7 +++++ ai_aside/__init__.py | 2 +- ai_aside/block.py | 67 ++++++++++++++++++++++++++++++-------------- tests/test_block.py | 58 ++++++++++++++++++++++++++++++++++---- 4 files changed, 107 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b4859a5..af1c372 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,13 @@ Change Log Unreleased ********** +3.4.0 – 2023-08-30 +********************************************** + +* Include last updated timestamp in summary hook HTML, derived from the blocks. +* Also somewhat reformats timestamps in the handler return to conform to ISO standard. + + 3.3.1 – 2023-08-21 ********************************************** diff --git a/ai_aside/__init__.py b/ai_aside/__init__.py index ff67971..066a33d 100644 --- a/ai_aside/__init__.py +++ b/ai_aside/__init__.py @@ -2,6 +2,6 @@ A plugin containing xblocks and apps supporting GPT and other LLM use on edX. """ -__version__ = '3.3.1' +__version__ = '3.4.0' default_app_config = "ai_aside.apps.AiAsideConfig" diff --git a/ai_aside/block.py b/ai_aside/block.py index c64bb5e..4cae721 100644 --- a/ai_aside/block.py +++ b/ai_aside/block.py @@ -32,6 +32,7 @@ data-course-id="{{data_course_id}}" data-content-id="{{data_content_id}}" data-handler-url="{{data_handler_url}}" + data-last-updated="{{data_last_updated}}" > @@ -42,7 +43,7 @@ def _format_date(date): - return date.strftime('%Y-%m-%d %H:%M:%S') if isinstance(date, datetime) else None + return date.isoformat() if isinstance(date, datetime) else None def _staff_user(block): @@ -132,6 +133,46 @@ def _check_summarizable(block): return False +def _render_hook_fragment(handler_url, block, summary_items): + """ + Create hook Fragment from block and summarized children. + + Gathers data for the summary hook HTML, passes it into _render_summary + to get the HTML and packages that into a Fragment. + """ + last_published = getattr(block, 'published_on', None) + last_edited = getattr(block, 'edited_on', None) + for item in summary_items: + published = item['published_on'] + edited = item['edited_on'] + if published and published > last_published: + last_published = published + if edited and edited > last_edited: + last_edited = edited + + # we only need to know when the last time was that anything happened + last_updated = last_published + if last_edited > last_published: + last_updated = last_edited + + usage_id = block.scope_ids.usage_id + + fragment = Fragment('') + fragment.add_content( + _render_summary( + { + 'data_url_api': settings.SUMMARY_HOOK_HOST, + 'data_course_id': usage_id.course_key, + 'data_content_id': usage_id, + 'data_handler_url': handler_url, + 'data_last_updated': _format_date(last_updated), + 'js_url': settings.SUMMARY_HOOK_HOST + settings.SUMMARY_HOOK_JS_PATH, + } + ) + ) + return fragment + + class SummaryHookAside(XBlockAside): """ XBlock aside that injects AI summary javascript. @@ -155,7 +196,7 @@ def summary_handler(self, request=None, suffix=None): # pylint: disable=unused- return Response(status=404) published_on = getattr(block, 'published_on', None) - edited_on = getattr(block, 'published_on', None) + edited_on = getattr(block, 'edited_on', None) data = [] @@ -205,31 +246,15 @@ def _student_view_can_throw(self, block): This function can throw exceptions. """ - fragment = Fragment('') + length, items = _parse_children_contents(block) - # Check if there is content that worths summarizing - length, _ = _parse_children_contents(block) if length < settings.SUMMARY_HOOK_MIN_SIZE: - return fragment + return Fragment('') usage_id = block.scope_ids.usage_id - log.info(f'Summary hook injecting into {usage_id}') - handler_url = self._summary_handler_url() - - fragment.add_content( - _render_summary( - { - 'data_url_api': settings.SUMMARY_HOOK_HOST, - 'data_course_id': usage_id.course_key, - 'data_content_id': usage_id, - 'data_handler_url': handler_url, - 'js_url': settings.SUMMARY_HOOK_HOST + settings.SUMMARY_HOOK_JS_PATH, - } - ) - ) - return fragment + return _render_hook_fragment(self._summary_handler_url(), block, items) def _summary_handler_url(self): """ diff --git a/tests/test_block.py b/tests/test_block.py index 51d5012..7164bc0 100644 --- a/tests/test_block.py +++ b/tests/test_block.py @@ -5,10 +5,19 @@ from unittest.mock import MagicMock, patch from django.test import TestCase, override_settings +from opaque_keys.edx.keys import UsageKey -from ai_aside.block import _check_summarizable, _extract_child_contents, _format_date, _parse_children_contents +from ai_aside.block import ( + _check_summarizable, + _extract_child_contents, + _format_date, + _parse_children_contents, + _render_hook_fragment, +) fake_transcript = 'This is the text version from the transcript' +date1 = datetime(2023, 1, 2, 3, 4, 5) +date2 = datetime(2023, 6, 7, 8, 9, 10) def fake_get_transcript(child, lang=None, output_format='SRT', youtube_id=None): # pylint: disable=unused-argument @@ -39,12 +48,19 @@ class FakeBlock: "Fake block for testing, returns given children" def __init__(self, children): self.children = children + self.scope_ids = lambda: None + self.scope_ids.usage_id = UsageKey.from_string('block-v1:edX+A+B+type@vertical+block@verticalD') + self.edited_on = date1 + self.published_on = date1 def get_children(self): return self.children -@override_settings(SUMMARY_HOOK_MIN_SIZE=40, HTML_TAGS_TO_REMOVE=['script', 'style', 'test']) +@override_settings(SUMMARY_HOOK_MIN_SIZE=40, + SUMMARY_HOOK_HOST='http://hookhost', + SUMMARY_HOOK_JS_PATH='/jspath', + HTML_TAGS_TO_REMOVE=['script', 'style', 'test']) class TestSummaryHookAside(TestCase): """Summary hook aside tests""" def setUp(self): @@ -62,9 +78,8 @@ def setUp(self): patch.dict('sys.modules', modules).start() def test_format_date(self): - date = datetime(2023, 5, 1, 12, 0, 0) - formatted_date = _format_date(date) - self.assertEqual(formatted_date, '2023-05-01 12:00:00') + formatted_date = _format_date(date1) + self.assertEqual(formatted_date, '2023-01-02T03:04:05') def test_format_date_with_invalid_input(self): invalid_date = '2023-05-01' @@ -269,6 +284,39 @@ def test_parse_children_contents_with_invalid_children(self): self.assertEqual(length, 0) self.assertEqual(items, []) + def test_render_hook_fragment(self): + block = FakeBlock([]) + items = [{ + 'published_on': date1, + 'edited_on': date1, + }, { + 'published_on': date2, + 'edited_on': date1, + }] + expected = ''' +
 
+
+
+
+
+
+
+ +
+ ''' + fragment = _render_hook_fragment('http://handler.url', block, items) + self.assertEqual( + # join and split to ignore whitespace differences + "".join(fragment.body_html()).split(), + "".join(expected).split() + ) + @override_settings(SUMMARY_HOOK_MIN_SIZE=40, HTML_TAGS_TO_REMOVE=['script', 'style', 'test']) class TestSummaryHookAsideMissingTranscript(TestCase):