diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 3311e2eb85..25a133d19e 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -31,6 +31,11 @@ from capa.capa_problem import LoncapaProblem, LoncapaSystem from capa.inputtypes import Status from capa.responsetypes import LoncapaProblemError, ResponseError, StudentInputError from capa.util import convert_files_to_filenames, get_inner_html_from_xpath +from common.djangoapps.xblock_django.constants import ( + ATTR_KEY_ANONYMOUS_USER_ID, + ATTR_KEY_USER_IS_STAFF, + ATTR_KEY_USER_ID, +) from openedx.core.djangolib.markup import HTML, Text from xmodule.contentstore.django import contentstore from xmodule.editing_module import EditingMixin @@ -113,7 +118,7 @@ class Randomization(String): to_json = from_json -@XBlock.wants('user') +@XBlock.needs('user') @XBlock.needs('i18n') @XBlock.wants('call_to_action') class ProblemBlock( @@ -784,9 +789,10 @@ class ProblemBlock( """ if self.rerandomize == RANDOMIZATION.NEVER: self.seed = 1 - elif self.rerandomize == RANDOMIZATION.PER_STUDENT and hasattr(self.runtime, 'seed'): + elif self.rerandomize == RANDOMIZATION.PER_STUDENT: + user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_ID) or 0 # see comment on randomization_bin - self.seed = randomization_bin(self.runtime.seed, str(self.location).encode('utf-8')) + self.seed = randomization_bin(user_id, str(self.location).encode('utf-8')) else: self.seed = struct.unpack('i', os.urandom(4))[0] @@ -801,9 +807,13 @@ class ProblemBlock( if text is None: text = self.data + user_service = self.runtime.service(self, 'user') + anonymous_student_id = user_service.get_current_user().opt_attrs.get(ATTR_KEY_ANONYMOUS_USER_ID) + seed = user_service.get_current_user().opt_attrs.get(ATTR_KEY_USER_ID) or 0 + capa_system = LoncapaSystem( ajax_url=self.ajax_url, - anonymous_student_id=self.runtime.anonymous_student_id, + anonymous_student_id=anonymous_student_id, cache=self.runtime.cache, can_execute_unsafe_code=self.runtime.can_execute_unsafe_code, get_python_lib_zip=self.runtime.get_python_lib_zip, @@ -812,7 +822,7 @@ class ProblemBlock( i18n=self.runtime.service(self, "i18n"), node_path=self.runtime.node_path, render_template=self.runtime.render_template, - seed=self.runtime.seed, # Why do we do this if we have self.seed? + seed=seed, # Why do we do this if we have self.seed? STATIC_URL=self.runtime.STATIC_URL, xqueue=self.runtime.xqueue, matlab_api_key=self.matlab_api_key @@ -1412,6 +1422,7 @@ class ProblemBlock( """ Is the user allowed to see an answer? """ + user_is_staff = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_IS_STAFF) if not self.correctness_available(): # If correctness is being withheld, then don't show answers either. return False @@ -1419,7 +1430,7 @@ class ProblemBlock( return False elif self.showanswer == SHOWANSWER.NEVER: return False - elif self.runtime.user_is_staff: + elif user_is_staff: # This is after the 'never' check because admins can see the answer # unless the problem explicitly prevents it return True @@ -1459,10 +1470,11 @@ class ProblemBlock( Limits access to the correct/incorrect flags, messages, and problem score. """ + user_is_staff = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_IS_STAFF) return ShowCorrectness.correctness_available( show_correctness=self.show_correctness, due_date=self.close_date, - has_staff_access=self.runtime.user_is_staff, + has_staff_access=user_is_staff, ) def update_score(self, data): @@ -1777,7 +1789,8 @@ class ProblemBlock( # If the user is a staff member, include # the full exception, including traceback, # in the response - if self.runtime.user_is_staff: + user_is_staff = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_IS_STAFF) + if user_is_staff: msg = f"Staff debug info: {traceback.format_exc()}" # Otherwise, display just an error message, diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index b72c61bc02..0329bd0bc2 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -17,6 +17,7 @@ from path import Path as path from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Boolean, List, Scope, String +from common.djangoapps.xblock_django.constants import ATTR_KEY_ANONYMOUS_USER_ID from xmodule.contentstore.content import StaticContent from xmodule.editing_module import EditingMixin from xmodule.edxnotes_utils import edxnotes @@ -42,6 +43,7 @@ _ = lambda text: text @XBlock.needs("i18n") +@XBlock.needs("user") class HtmlBlockMixin( # lint-amnesty, pylint: disable=abstract-method XmlMixin, EditingMixin, XModuleDescriptorToXBlockMixin, XModuleToXBlockMixin, HTMLSnippet, ResourceTemplates, XModuleMixin, @@ -117,8 +119,9 @@ class HtmlBlockMixin( # lint-amnesty, pylint: disable=abstract-method """ Returns html required for rendering the block. """ if self.data: data = self.data - if getattr(self.runtime, 'anonymous_student_id', None): - data = data.replace("%%USER_ID%%", self.runtime.anonymous_student_id) + user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_ANONYMOUS_USER_ID) + if user_id: + data = data.replace("%%USER_ID%%", user_id) data = data.replace("%%COURSE_ID%%", str(self.scope_ids.usage_id.context_key)) return data return self.data diff --git a/common/lib/xmodule/xmodule/lti_module.py b/common/lib/xmodule/xmodule/lti_module.py index 99f7db03c7..f38cb482e8 100644 --- a/common/lib/xmodule/xmodule/lti_module.py +++ b/common/lib/xmodule/xmodule/lti_module.py @@ -78,6 +78,7 @@ from xmodule.mako_module import MakoTemplateBlockBase from openedx.core.djangolib.markup import HTML, Text from xmodule.editing_module import EditingMixin +from common.djangoapps.xblock_django.constants import ATTR_KEY_ANONYMOUS_USER_ID from xmodule.lti_2_util import LTI20BlockMixin, LTIError from xmodule.raw_module import EmptyDataRawMixin from xmodule.util.xmodule_django import add_webpack_to_fragment @@ -269,6 +270,7 @@ class LTIFields: @XBlock.needs("i18n") +@XBlock.needs("user") class LTIBlock( LTIFields, LTI20BlockMixin, @@ -529,7 +531,10 @@ class LTIBlock( return Response(template, content_type='text/html') def get_user_id(self): - user_id = self.runtime.anonymous_student_id + """ + Returns the current user ID, URL-escaped so it is safe to use as a URL component. + """ + user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_ANONYMOUS_USER_ID) assert user_id is not None return str(parse.quote(user_id)) @@ -671,7 +676,8 @@ class LTIBlock( # To test functionality test in LMS if callable(self.runtime.get_real_user): - real_user_object = self.runtime.get_real_user(self.runtime.anonymous_student_id) + user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_ANONYMOUS_USER_ID) + real_user_object = self.runtime.get_real_user(user_id) try: self.user_email = real_user_object.email # lint-amnesty, pylint: disable=attribute-defined-outside-init except AttributeError: diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 7e00105621..6ccd4fea8a 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -33,6 +33,7 @@ from xmodule.x_module import ( XModuleToXBlockMixin, ) +from common.djangoapps.xblock_django.constants import ATTR_KEY_USER_ID, ATTR_KEY_USER_IS_STAFF from openedx.core.djangoapps.agreements.toggles import is_integrity_signature_enabled from .exceptions import NotFoundError @@ -378,7 +379,7 @@ class SequenceBlock( is_hidden_after_due = False if self._required_prereq(): - if self.runtime.user_is_staff: + if self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_IS_STAFF): banner_text = _( 'This subsection is unlocked for learners when they meet the prerequisite requirements.' ) @@ -459,7 +460,7 @@ class SequenceBlock( prereq_met = True prereq_meta_info = {} if self._required_prereq(): - if self.runtime.user_is_staff: + if self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_IS_STAFF): banner_text = _( 'This subsection is unlocked for learners when they meet the prerequisite requirements.' ) @@ -553,7 +554,7 @@ class SequenceBlock( """ hidden_date = course.end if course.self_paced else self.due return ( - self.runtime.user_is_staff or + self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_IS_STAFF) or self.verify_current_content_visibility(hidden_date, self.hide_after_due) ) @@ -643,8 +644,9 @@ class SequenceBlock( """ gating_service = self.runtime.service(self, 'gating') if gating_service: + user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_ID) fulfilled = gating_service.is_gate_fulfilled( - self.course_id, self.location, self.runtime.user_id + self.course_id, self.location, user_id ) return fulfilled @@ -692,7 +694,8 @@ class SequenceBlock( comes to determining whether a student is allowed to access this, with other checks being done in has_access calls. """ - if self.runtime.user_is_staff or context.get('specific_masquerade', False): + user_is_staff = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_IS_STAFF) + if user_is_staff or context.get('specific_masquerade', False): return False # We're not allowed to see it because of pre-reqs that haven't been @@ -723,7 +726,8 @@ class SequenceBlock( """ gating_service = self.runtime.service(self, 'gating') if gating_service: - return gating_service.compute_is_prereq_met(self.location, self.runtime.user_id, recalc_on_unmet) + user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_ID) + return gating_service.compute_is_prereq_met(self.location, user_id, recalc_on_unmet) return True, {} @@ -915,8 +919,10 @@ class SequenceBlock( self.is_time_limited ) if feature_enabled: - user_id = self.runtime.user_id - user_role_in_course = 'staff' if self.runtime.user_is_staff else 'student' + current_user = self.runtime.service(self, 'user').get_current_user() + user_id = current_user.opt_attrs.get(ATTR_KEY_USER_ID) + user_is_staff = current_user.opt_attrs.get(ATTR_KEY_USER_IS_STAFF) + user_role_in_course = 'staff' if user_is_staff else 'student' course_id = self.runtime.course_id content_id = self.location diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index d0c1639bc2..d6ac518e95 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -41,6 +41,7 @@ from xblock.reference.plugins import FSService from xblock.runtime import KvsFieldData from common.djangoapps import static_replace +from common.djangoapps.xblock_django.constants import ATTR_KEY_USER_ID from capa.xqueue_interface import XQueueInterface from lms.djangoapps.courseware.access import get_user_role, has_access from lms.djangoapps.courseware.entrance_exams import user_can_skip_entrance_exam, user_has_passed_entrance_exam @@ -564,8 +565,10 @@ def get_module_system_for_user( handle_event(block, event) else: context = contexts.course_context_from_course_id(course_id) - if block.runtime.user_id: - context['user_id'] = block.runtime.user_id + user_id = user_service.get_current_user().opt_attrs.get(ATTR_KEY_USER_ID) + if user_id: + context['user_id'] = user_id + context['asides'] = {} for aside in block.runtime.get_asides(block): if hasattr(aside, 'get_event_context'): diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 45afaff3f1..ffb06d35ca 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -45,6 +45,7 @@ from common.djangoapps.course_modes.models import CourseMode # lint-amnesty, py from common.djangoapps.student.tests.factories import GlobalStaffFactory from common.djangoapps.student.tests.factories import RequestFactoryNoCsrf from common.djangoapps.student.tests.factories import UserFactory +from common.djangoapps.xblock_django.constants import ATTR_KEY_ANONYMOUS_USER_ID from lms.djangoapps.courseware import module_render as render from lms.djangoapps.courseware.access_response import AccessResponse from lms.djangoapps.courseware.courses import get_course_info_section, get_course_with_access @@ -1923,7 +1924,7 @@ class TestAnonymousStudentId(SharedModuleStoreTestCase, LoginEnrollmentTestCase) if hasattr(xblock_class, 'module_class'): descriptor.module_class = xblock_class.module_class - return render.get_module_for_descriptor_internal( + module = render.get_module_for_descriptor_internal( user=self.user, descriptor=descriptor, student_data=Mock(spec=FieldData, name='student_data'), @@ -1932,7 +1933,9 @@ class TestAnonymousStudentId(SharedModuleStoreTestCase, LoginEnrollmentTestCase) xqueue_callback_url_prefix=Mock(name='xqueue_callback_url_prefix'), # XQueue Callback Url Prefix request_token='request_token', course=self.course, - ).xmodule_runtime.anonymous_student_id + ) + current_user = module.xmodule_runtime.service(module, 'user').get_current_user() + return current_user.opt_attrs.get(ATTR_KEY_ANONYMOUS_USER_ID) @ddt.data(*PER_STUDENT_ANONYMIZED_DESCRIPTORS) def test_per_student_anonymized_id(self, descriptor_class): diff --git a/lms/djangoapps/edxnotes/decorators.py b/lms/djangoapps/edxnotes/decorators.py index f92ca2939b..6c5d0ca6d9 100644 --- a/lms/djangoapps/edxnotes/decorators.py +++ b/lms/djangoapps/edxnotes/decorators.py @@ -6,8 +6,10 @@ Decorators related to edXNotes. import json from django.conf import settings +from xblock.exceptions import NoSuchServiceError from common.djangoapps.edxmako.shortcuts import render_to_string +from common.djangoapps.xblock_django.constants import ATTR_KEY_ANONYMOUS_USER_ID def edxnotes(cls): @@ -40,7 +42,11 @@ def edxnotes(cls): # - Harvard Annotation Tool is enabled for the course # - the feature flag or `edxnotes` setting of the course is set to False # - the user is not authenticated - user = self.runtime.get_real_user(self.runtime.anonymous_student_id) + try: + user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_ANONYMOUS_USER_ID) + user = self.runtime.get_real_user(user_id) + except NoSuchServiceError: + user = None if is_studio or not is_feature_enabled(course, user): return original_get_html(self, *args, **kwargs) diff --git a/lms/djangoapps/lms_xblock/runtime.py b/lms/djangoapps/lms_xblock/runtime.py index 7a74c4fa4c..5ab2fe946b 100644 --- a/lms/djangoapps/lms_xblock/runtime.py +++ b/lms/djangoapps/lms_xblock/runtime.py @@ -98,13 +98,9 @@ class UserTagsService: COURSE_SCOPE = user_course_tag_api.COURSE_SCOPE - def __init__(self, runtime): - self.runtime = runtime - - def _get_current_user(self): - """Returns the real, not anonymized, current user.""" - real_user = self.runtime.get_real_user(self.runtime.anonymous_student_id) - return real_user + def __init__(self, user, course_id): + self._user = user + self._course_id = course_id def get_tag(self, scope, key): """ @@ -117,8 +113,8 @@ class UserTagsService: raise ValueError(f"unexpected scope {scope}") return user_course_tag_api.get_course_tag( - self._get_current_user(), - self.runtime.course_id, key + self._user, + self._course_id, key ) def set_tag(self, scope, key, value): @@ -133,8 +129,8 @@ class UserTagsService: raise ValueError(f"unexpected scope {scope}") return user_course_tag_api.set_course_tag( - self._get_current_user(), - self.runtime.course_id, key, value + self._user, + self._course_id, key, value ) @@ -142,24 +138,28 @@ class LmsModuleSystem(ModuleSystem): # pylint: disable=abstract-method """ ModuleSystem specialized to the LMS """ - def __init__(self, user=None, **kwargs): + def __init__(self, user, **kwargs): request_cache_dict = DEFAULT_REQUEST_CACHE.data store = modulestore() + course_id = kwargs.get('course_id') services = kwargs.setdefault('services', {}) if user and user.is_authenticated: - services['completion'] = CompletionService(user=user, context_key=kwargs.get('course_id')) + services['completion'] = CompletionService(user=user, context_key=course_id) services['fs'] = xblock.reference.plugins.FSService() services['i18n'] = ModuleI18nService services['library_tools'] = LibraryToolsService(store, user_id=user.id if user else None) services['partitions'] = PartitionService( - course_id=kwargs.get('course_id'), + course_id=course_id, cache=request_cache_dict ) services['settings'] = SettingsService() - services['user_tags'] = UserTagsService(self) + services['user_tags'] = UserTagsService( + user=user, + course_id=course_id, + ) if badges_enabled(): - services['badging'] = BadgingService(course_id=kwargs.get('course_id'), modulestore=store) + services['badging'] = BadgingService(course_id=course_id, modulestore=store) self.request_token = kwargs.pop('request_token', None) services['teams'] = TeamsService() services['teams_configuration'] = TeamsConfigurationService() diff --git a/lms/djangoapps/lms_xblock/test/test_runtime.py b/lms/djangoapps/lms_xblock/test/test_runtime.py index 67632e7e0c..42c20de05b 100644 --- a/lms/djangoapps/lms_xblock/test/test_runtime.py +++ b/lms/djangoapps/lms_xblock/test/test_runtime.py @@ -66,6 +66,7 @@ class TestHandlerUrl(TestCase): render_template=Mock(), replace_urls=str, course_id=self.course_key, + user=Mock(), descriptor_runtime=Mock(), ) @@ -125,18 +126,14 @@ class TestUserServiceAPI(TestCase): self.course_id = CourseLocator("org", "course", "run") self.user = UserFactory.create() - def mock_get_real_user(_anon_id): - """Just returns the test user""" - return self.user - self.runtime = LmsModuleSystem( static_url='/static', track_function=Mock(), get_module=Mock(), render_template=Mock(), replace_urls=str, + user=self.user, course_id=self.course_id, - get_real_user=mock_get_real_user, descriptor_runtime=Mock(), ) self.scope = 'course' @@ -192,6 +189,7 @@ class TestBadgingService(ModuleStoreTestCase): render_template=Mock(), replace_urls=str, course_id=self.course_id, + user=self.user, get_real_user=mock_get_real_user, descriptor_runtime=Mock(), ) @@ -247,6 +245,7 @@ class TestI18nService(ModuleStoreTestCase): render_template=Mock(), replace_urls=str, course_id=self.course.id, + user=Mock(), descriptor_runtime=Mock(), )