Skip to content

Commit

Permalink
feat: generate user-role and enrollment into ai hook div
Browse files Browse the repository at this point in the history
  • Loading branch information
aethant committed Oct 6, 2023
1 parent 988e5b1 commit f688669
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 9 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ Change Log
Unreleased
**********

* Add make install-local target for easy devstack installation
3.6.0 – 2023-10-05
**********************************************

* Include user role in summary hook HTML.
* Add make install-local target for easy devstack installation.

3.5.0 – 2023-09-04
**********************************************
Expand Down
2 changes: 1 addition & 1 deletion ai_aside/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
A plugin containing xblocks and apps supporting GPT and other LLM use on edX.
"""

__version__ = '3.5.0'
__version__ = '3.6.0'

default_app_config = "ai_aside.apps.AiAsideConfig"
43 changes: 38 additions & 5 deletions ai_aside/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from xblock.core import XBlock, XBlockAside

from ai_aside.config_api.api import is_summary_enabled
from ai_aside.constants import ATTR_KEY_USER_ID, ATTR_KEY_USER_ROLE
from ai_aside.platform_imports import get_block, get_text_transcript
from ai_aside.text_utils import html_to_text
from ai_aside.waffle import summaries_configuration_enabled as ff_is_summary_config_enabled
Expand All @@ -33,6 +34,7 @@
data-content-id="{{data_content_id}}"
data-handler-url="{{data_handler_url}}"
data-last-updated="{{data_last_updated}}"
data-user-role="{{data_user_role}}"
>
</div>
</div>
Expand Down Expand Up @@ -133,15 +135,19 @@ def _check_summarizable(block):
return False


def _render_hook_fragment(handler_url, block, summary_items):
def _render_hook_fragment(user_role_string, 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.
"""
print(dir(block))
last_published = getattr(block, 'published_on', None)
last_edited = getattr(block, 'edited_on', None)
usage_id = block.scope_ids.usage_id
course_key = usage_id.course_key

for item in summary_items:
published = item['published_on']
edited = item['edited_on']
Expand All @@ -155,24 +161,25 @@ def _render_hook_fragment(handler_url, block, summary_items):
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_course_id': course_key,
'data_content_id': usage_id,
'data_handler_url': handler_url,
'data_last_updated': _format_date(last_updated),
'data_user_role': user_role_string,
'js_url': settings.SUMMARY_HOOK_HOST + settings.SUMMARY_HOOK_JS_PATH,
}
)
)
return fragment


@XBlock.needs('user')
@XBlock.needs('credit')
class SummaryHookAside(XBlockAside):
"""
XBlock aside that injects AI summary javascript.
Expand Down Expand Up @@ -254,7 +261,11 @@ def _student_view_can_throw(self, block):
usage_id = block.scope_ids.usage_id
log.info(f'Summary hook injecting into {usage_id}')

return _render_hook_fragment(self._summary_handler_url(), block, items)
return _render_hook_fragment(
self._user_role_string(usage_id.course_key),
self._summary_handler_url(),
block,
items)

def _summary_handler_url(self):
"""
Expand All @@ -274,6 +285,28 @@ def _summary_handler_url(self):
handler_url = handler_url.replace('localhost', aispot_lms_name)
return handler_url

def _user_role_string(self, course_key):
return self._user_role_string_from_services(
self.runtime.service(self, 'user'),
self.runtime.service(self, 'credit'),
course_key)

@classmethod
def _user_role_string_from_services(cls, user_service, credit_service, course_key):
"""
Determine and construct the user_role string that gets injected into the block.
"""
user_role = 'unknown'
user = user_service.get_current_user()
if user is not None:
user_role = user.opt_attrs.get(ATTR_KEY_USER_ROLE)
user_enrollment = credit_service.get_credit_state(
user.opt_attrs.get(ATTR_KEY_USER_ID), course_key)

if user_enrollment.get('enrollment_mode') is not None:
user_role = user_role + " " + user_enrollment.get('enrollment_mode')
return user_role

@classmethod
def should_apply_to_block(cls, block):
"""
Expand Down
11 changes: 11 additions & 0 deletions ai_aside/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,14 @@
# Regex for Usage ID URL patterns (Unit IDs)
UNIT_ID_REGEX = r'(?:i4x://?[^/]+/[^/]+/[^/]+/[^@]+(?:@[^/]+)?)|(?:[^/]+)'
UNIT_ID_PATTERN = r'(?P<unit_id>{})'.format(UNIT_ID_REGEX)

"""
Constants used by DjangoXBlockUserService
"""

# Optional attributes stored on the XBlockUser

# The personally identifiable user ID.
ATTR_KEY_USER_ID = 'edx-platform.user_id'
# The user's role in the course ('staff', 'instructor', or 'student').
ATTR_KEY_USER_ROLE = 'edx-platform.user_role'
36 changes: 34 additions & 2 deletions tests/test_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
import unittest
from datetime import datetime
from textwrap import dedent
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, Mock, call, patch

from django.test import TestCase, override_settings
from opaque_keys.edx.keys import UsageKey

from ai_aside.block import (
SummaryHookAside,
_check_summarizable,
_extract_child_contents,
_format_date,
Expand Down Expand Up @@ -52,6 +53,9 @@ def __init__(self, children):
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
my_runtime = MagicMock()
my_runtime.service.return_value.get_current_user.return_value.opt_attrs.get.return_value = "student audit"
self.runtime = my_runtime

def get_children(self):
return self.children
Expand Down Expand Up @@ -303,20 +307,48 @@ def test_render_hook_fragment(self):
data-content-id="block-v1:edX+A+B+type@vertical+block@verticalD"
data-handler-url="http://handler.url"
data-last-updated="2023-06-07T08:09:10"
data-user-role="user role string"
>
</div>
</div>
<div id="ai-spot-root"></div>
<script type="text/javascript" src="http://hookhost/jspath" defer="defer"></script>
</div>
'''
fragment = _render_hook_fragment('http://handler.url', block, items)
fragment = _render_hook_fragment('user role string', 'http://handler.url', block, items)
self.assertEqual(
# join and split to ignore whitespace differences
"".join(fragment.body_html()).split(),
"".join(expected).split()
)

def test_user_role_from_services(self):
user_service = Mock()
credit_service = Mock()
user = Mock()
enrollment = Mock()

user_service.get_current_user.return_value = user
user.opt_attrs.get.return_value = 'the_user_role'
credit_service.get_credit_state.return_value = enrollment
enrollment.get.return_value = 'the_enrollment_mode'

expected_role = 'the_user_role the_enrollment_mode'
# pylint: disable=protected-access
returned_role = SummaryHookAside._user_role_string_from_services(user_service, credit_service, "course_key")
self.assertEqual(returned_role, expected_role)

self.assertEqual(credit_service.mock_calls,
[call.get_credit_state('the_user_role', 'course_key'),
call.get_credit_state().get('enrollment_mode'),
call.get_credit_state().get('enrollment_mode')])

user_service.get_current_user.return_value = None
expected_role = 'unknown'
# pylint: disable=protected-access
returned_role = SummaryHookAside._user_role_string_from_services(user_service, credit_service, "course_key")
self.assertEqual(returned_role, expected_role)


@override_settings(SUMMARY_HOOK_MIN_SIZE=40, HTML_TAGS_TO_REMOVE=['script', 'style', 'test'])
class TestSummaryHookAsideMissingTranscript(TestCase):
Expand Down

0 comments on commit f688669

Please sign in to comment.