Skip to content

Commit 3090ad1

Browse files
authored
Merge pull request #64 from edx/ACADEMIC-16455_UserRoleInFeedback
[KBK-1]: user role in feedback
2 parents 988e5b1 + d6d6a95 commit 3090ad1

File tree

5 files changed

+91
-9
lines changed

5 files changed

+91
-9
lines changed

CHANGELOG.rst

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ Change Log
1414
Unreleased
1515
**********
1616

17-
* Add make install-local target for easy devstack installation
17+
3.6.0 – 2023-10-05
18+
**********************************************
19+
20+
* Include user role in summary hook HTML.
21+
* Add make install-local target for easy devstack installation.
1822

1923
3.5.0 – 2023-09-04
2024
**********************************************

ai_aside/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
A plugin containing xblocks and apps supporting GPT and other LLM use on edX.
33
"""
44

5-
__version__ = '3.5.0'
5+
__version__ = '3.6.0'
66

77
default_app_config = "ai_aside.apps.AiAsideConfig"

ai_aside/block.py

+40-5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from xblock.core import XBlock, XBlockAside
1111

1212
from ai_aside.config_api.api import is_summary_enabled
13+
from ai_aside.constants import ATTR_KEY_USER_ID, ATTR_KEY_USER_ROLE
1314
from ai_aside.platform_imports import get_block, get_text_transcript
1415
from ai_aside.text_utils import html_to_text
1516
from ai_aside.waffle import summaries_configuration_enabled as ff_is_summary_config_enabled
@@ -33,6 +34,7 @@
3334
data-content-id="{{data_content_id}}"
3435
data-handler-url="{{data_handler_url}}"
3536
data-last-updated="{{data_last_updated}}"
37+
data-user-role="{{data_user_role}}"
3638
>
3739
</div>
3840
</div>
@@ -133,7 +135,7 @@ def _check_summarizable(block):
133135
return False
134136

135137

136-
def _render_hook_fragment(handler_url, block, summary_items):
138+
def _render_hook_fragment(user_role_string, handler_url, block, summary_items):
137139
"""
138140
Create hook Fragment from block and summarized children.
139141
@@ -142,6 +144,9 @@ def _render_hook_fragment(handler_url, block, summary_items):
142144
"""
143145
last_published = getattr(block, 'published_on', None)
144146
last_edited = getattr(block, 'edited_on', None)
147+
usage_id = block.scope_ids.usage_id
148+
course_key = usage_id.course_key
149+
145150
for item in summary_items:
146151
published = item['published_on']
147152
edited = item['edited_on']
@@ -155,24 +160,25 @@ def _render_hook_fragment(handler_url, block, summary_items):
155160
if last_edited > last_published:
156161
last_updated = last_edited
157162

158-
usage_id = block.scope_ids.usage_id
159-
160163
fragment = Fragment('')
161164
fragment.add_content(
162165
_render_summary(
163166
{
164167
'data_url_api': settings.SUMMARY_HOOK_HOST,
165-
'data_course_id': usage_id.course_key,
168+
'data_course_id': course_key,
166169
'data_content_id': usage_id,
167170
'data_handler_url': handler_url,
168171
'data_last_updated': _format_date(last_updated),
172+
'data_user_role': user_role_string,
169173
'js_url': settings.SUMMARY_HOOK_HOST + settings.SUMMARY_HOOK_JS_PATH,
170174
}
171175
)
172176
)
173177
return fragment
174178

175179

180+
@XBlock.needs('user')
181+
@XBlock.needs('credit')
176182
class SummaryHookAside(XBlockAside):
177183
"""
178184
XBlock aside that injects AI summary javascript.
@@ -254,7 +260,11 @@ def _student_view_can_throw(self, block):
254260
usage_id = block.scope_ids.usage_id
255261
log.info(f'Summary hook injecting into {usage_id}')
256262

257-
return _render_hook_fragment(self._summary_handler_url(), block, items)
263+
return _render_hook_fragment(
264+
self._user_role_string(usage_id.course_key),
265+
self._summary_handler_url(),
266+
block,
267+
items)
258268

259269
def _summary_handler_url(self):
260270
"""
@@ -274,6 +284,31 @@ def _summary_handler_url(self):
274284
handler_url = handler_url.replace('localhost', aispot_lms_name)
275285
return handler_url
276286

287+
def _user_role_string(self, course_key):
288+
return self._user_role_string_from_services(
289+
self.runtime.service(self, 'user'),
290+
self.runtime.service(self, 'credit'),
291+
course_key)
292+
293+
@classmethod
294+
def _user_role_string_from_services(cls, user_service, credit_service, course_key):
295+
"""
296+
Determine and construct the user_role string that gets injected into the block.
297+
"""
298+
user_role = 'unknown'
299+
user = user_service.get_current_user()
300+
if user is not None:
301+
user_role = user.opt_attrs.get(ATTR_KEY_USER_ROLE)
302+
user_enrollment = credit_service.get_credit_state(
303+
user.opt_attrs.get(ATTR_KEY_USER_ID), course_key)
304+
305+
if user_enrollment.get('enrollment_mode'):
306+
user_role = user_role + " " + user_enrollment.get('enrollment_mode')
307+
else:
308+
user_role = 'unknown'
309+
310+
return user_role
311+
277312
@classmethod
278313
def should_apply_to_block(cls, block):
279314
"""

ai_aside/constants.py

+11
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,14 @@
77
# Regex for Usage ID URL patterns (Unit IDs)
88
UNIT_ID_REGEX = r'(?:i4x://?[^/]+/[^/]+/[^/]+/[^@]+(?:@[^/]+)?)|(?:[^/]+)'
99
UNIT_ID_PATTERN = r'(?P<unit_id>{})'.format(UNIT_ID_REGEX)
10+
11+
"""
12+
Constants used by DjangoXBlockUserService
13+
"""
14+
15+
# Optional attributes stored on the XBlockUser
16+
17+
# The personally identifiable user ID.
18+
ATTR_KEY_USER_ID = 'edx-platform.user_id'
19+
# The user's role in the course ('staff', 'instructor', or 'student').
20+
ATTR_KEY_USER_ROLE = 'edx-platform.user_role'

tests/test_block.py

+34-2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
import unittest
33
from datetime import datetime
44
from textwrap import dedent
5-
from unittest.mock import MagicMock, patch
5+
from unittest.mock import MagicMock, Mock, call, patch
66

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

1010
from ai_aside.block import (
11+
SummaryHookAside,
1112
_check_summarizable,
1213
_extract_child_contents,
1314
_format_date,
@@ -52,6 +53,9 @@ def __init__(self, children):
5253
self.scope_ids.usage_id = UsageKey.from_string('block-v1:edX+A+B+type@vertical+block@verticalD')
5354
self.edited_on = date1
5455
self.published_on = date1
56+
my_runtime = MagicMock()
57+
my_runtime.service.return_value.get_current_user.return_value.opt_attrs.get.return_value = "student audit"
58+
self.runtime = my_runtime
5559

5660
def get_children(self):
5761
return self.children
@@ -303,20 +307,48 @@ def test_render_hook_fragment(self):
303307
data-content-id="block-v1:edX+A+B+type@vertical+block@verticalD"
304308
data-handler-url="http://handler.url"
305309
data-last-updated="2023-06-07T08:09:10"
310+
data-user-role="user role string"
306311
>
307312
</div>
308313
</div>
309314
<div id="ai-spot-root"></div>
310315
<script type="text/javascript" src="http://hookhost/jspath" defer="defer"></script>
311316
</div>
312317
'''
313-
fragment = _render_hook_fragment('http://handler.url', block, items)
318+
fragment = _render_hook_fragment('user role string', 'http://handler.url', block, items)
314319
self.assertEqual(
315320
# join and split to ignore whitespace differences
316321
"".join(fragment.body_html()).split(),
317322
"".join(expected).split()
318323
)
319324

325+
def test_user_role_from_services(self):
326+
user_service = Mock()
327+
credit_service = Mock()
328+
user = Mock()
329+
enrollment = Mock()
330+
331+
user_service.get_current_user.return_value = user
332+
user.opt_attrs.get.return_value = 'the_user_role'
333+
credit_service.get_credit_state.return_value = enrollment
334+
enrollment.get.return_value = 'the_enrollment_mode'
335+
336+
expected_role = 'the_user_role the_enrollment_mode'
337+
# pylint: disable=protected-access
338+
returned_role = SummaryHookAside._user_role_string_from_services(user_service, credit_service, "course_key")
339+
self.assertEqual(returned_role, expected_role)
340+
341+
self.assertEqual(credit_service.mock_calls,
342+
[call.get_credit_state('the_user_role', 'course_key'),
343+
call.get_credit_state().get('enrollment_mode'),
344+
call.get_credit_state().get('enrollment_mode')])
345+
346+
user_service.get_current_user.return_value = None
347+
expected_role = 'unknown'
348+
# pylint: disable=protected-access
349+
returned_role = SummaryHookAside._user_role_string_from_services(user_service, credit_service, "course_key")
350+
self.assertEqual(returned_role, expected_role)
351+
320352

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

0 commit comments

Comments
 (0)