Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions cms/djangoapps/contentstore/toggles.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
CMS feature toggles.
"""
from edx_toggles.toggles import SettingDictToggle, WaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag

# .. toggle_name: FEATURES['ENABLE_EXPORT_GIT']
# .. toggle_implementation: SettingDictToggle
Expand Down Expand Up @@ -138,3 +139,23 @@ def use_new_problem_editor():
Returns a boolean if new problem editor is enabled
"""
return ENABLE_NEW_PROBLEM_EDITOR_FLAG.is_enabled()


# .. toggle_name: contentstore.individualize_anonymous_user_id
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag enables the use of unique anonymous_user_id during studio preview
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2022-05-04
# .. toggle_target_removal_date: 2022-05-30
# .. toggle_tickets: MST-1455
INDIVIDUALIZE_ANONYMOUS_USER_ID = CourseWaffleFlag(
f'{CONTENTSTORE_NAMESPACE}.individualize_anonymous_user_id', __name__
)


def individualize_anonymous_user_id(course_id):
"""
Returns a boolean if individualized anonymous_user_id is enabled on the course
"""
return INDIVIDUALIZE_ANONYMOUS_USER_ID.is_enabled(course_id)
15 changes: 14 additions & 1 deletion cms/djangoapps/contentstore/views/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
from xmodule.util.xmodule_django import add_webpack_to_fragment
from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, ModuleSystem
from cms.djangoapps.xblock_config.models import StudioConfig
from cms.djangoapps.contentstore.toggles import individualize_anonymous_user_id
from cms.lib.xblock.field_data import CmsFieldData
from common.djangoapps.static_replace.services import ReplaceURLService
from common.djangoapps.static_replace.wrapper import replace_urls_wrapper
from common.djangoapps.student.models import anonymous_id_for_user
from common.djangoapps.edxmako.shortcuts import render_to_string
from common.djangoapps.edxmako.services import MakoService
from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService
Expand Down Expand Up @@ -194,6 +196,17 @@ def _preview_module_system(request, descriptor, field_data):
# stick the license wrapper in front
wrappers.insert(0, partial(wrap_with_license, mako_service=mako_service))

preview_anonymous_user_id = 'student'
if individualize_anonymous_user_id(course_id):
# There are blocks (capa, html, and video) where we do not want to scope
# the anonymous_user_id to specific courses. These are captured in the
# block attribute 'requires_per_student_anonymous_id'. Please note,
# the course_id field in AnynomousUserID model is blank if value is None.
if getattr(descriptor, 'requires_per_student_anonymous_id', False):
preview_anonymous_user_id = anonymous_id_for_user(request.user, None)
else:
preview_anonymous_user_id = anonymous_id_for_user(request.user, course_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is working great @schenedx , exactly as expected.

But to make sure your change doesn't get broken somewhere down the line, we need to update the unit tests to test these different cases. Here's what I'd recommend: schen/MST-1455...open-craft:jill/schen/MST-1455

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. I made a PR on the unit tests you wrote above and merged it. Once this PR is going green, I plan to merge as well.

return PreviewModuleSystem(
static_url=settings.STATIC_URL,
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
Expand All @@ -216,8 +229,8 @@ def _preview_module_system(request, descriptor, field_data):
"settings": SettingsService(),
"user": DjangoXBlockUserService(
request.user,
anonymous_user_id='student',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@schenedx This is not enough to fix the issue.. you'll need to actually compute the anonymous_user_id for the current user, and pass it into the UserService as is done by the LMS ModuleSystem.

Alternatively, you can move the logic for computing this value into a new function on the UserService (it will need access to the module descriptor and the course_id) to avoid duplicating code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nudge @schenedx :) Do you have an estimate for when you'll be able to address my comments?
Just planning my week, and want to make sure I allow time for a 2nd review. Thanks!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pomegranited This fell off my radar last week. I apologize. I thought the DjangoXBlockUserService instance already have the get_anynomous_user_id that returns the calculated value, no? Also, this is on CMS. The LMS side has the correct value returned.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries @schenedx :)

Also, this is on CMS. The LMS side has the correct value returned.

Yes. What I'm saying above is the CMS needs to return the same value as the LMS, and this requires some code changes.

I thought the DjangoXBlockUserService instance already have the get_anynomous_user_id that returns the calculated value, no? ([sic: corrected source link])

Yes it does have that method, but there's some issues with using it directly:

  • performance: that method hits the database to get the User object for the given username, so we'd likely want to cache it
  • There's some subtleties of the LMS code -- some blocks expect a per-student anonymous ID, while some expect a per-course anonymous ID. The DjangoXBlockUserService currently has no knowledge of course_id, or of the block's requires_per_student_anonymous_id flag.

All of these issues can be solved, but they require code changes to implement them.

I have no developer experience in this area, so this is difficult for me to setup.

Ah ok.. I misunderstood. Do you have time and interest in making this change? If not, then we should find someone who does. OpenCraft could do it (for a fee), if there's no one in the community who wants to do it?

How do we test the anynomous_user_id with randomized content? Do I just need to set it up on my local environment?

Yep, if you want to implement this change, you'll need a devstack. You can test it manually, but we'd also want some automated tests to protect the change, and ensure that future code changes don't break it.

Also, I think all the side effect of this change @pomegranited have listed is worth a heads up for 2U internal.

Yep, that's worth doing, and can be done as part of the OSPR too. It's always easier to see the effects of a change when the code is in place so it can be tested.

user_role=get_user_role(request.user, course_id),
anonymous_user_id=preview_anonymous_user_id,
),
"partitions": StudioPartitionService(course_id=course_id),
"teams_configuration": TeamsConfigurationService(),
Expand Down
28 changes: 28 additions & 0 deletions cms/djangoapps/contentstore/views/tests/test_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
import ddt
from django.test.client import Client, RequestFactory
from django.test.utils import override_settings
from edx_toggles.toggles.testutils import override_waffle_flag
from web_fragments.fragment import Fragment
from xblock.core import XBlock, XBlockAside

from xmodule.contentstore.django import contentstore
from xmodule.lti_module import LTIBlock
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import (
Expand All @@ -21,6 +23,7 @@
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.test_asides import AsideTestType
from cms.djangoapps.contentstore.utils import reverse_usage_url
from cms.djangoapps.contentstore.toggles import INDIVIDUALIZE_ANONYMOUS_USER_ID
from cms.djangoapps.xblock_config.models import StudioConfig
from common.djangoapps import static_replace
from common.djangoapps.student.tests.factories import UserFactory
Expand Down Expand Up @@ -294,3 +297,28 @@ def test_replace_urls(self):
html = '<a href="/static/id">'
assert self.runtime.replace_urls(html) == \
static_replace.replace_static_urls(html, course_id=self.runtime.course_id)

def test_anonymous_user_id_preview(self):
assert self.runtime.anonymous_student_id == 'student'

@override_waffle_flag(INDIVIDUALIZE_ANONYMOUS_USER_ID, active=True)
def test_anonymous_user_id_individual_per_student(self):
"""Test anonymous_user_id on a block which uses per-student anonymous IDs"""
# Create the runtime with the flag turned on.
runtime = _preview_module_system(
self.request,
descriptor=ItemFactory(category="problem", parent=self.course),
field_data=mock.Mock(),
)
assert runtime.anonymous_student_id == '26262401c528d7c4a6bbeabe0455ec46'

@override_waffle_flag(INDIVIDUALIZE_ANONYMOUS_USER_ID, active=True)
def test_anonymous_user_id_individual_per_course(self):
"""Test anonymous_user_id on a block which uses per-course anonymous IDs"""
# Create the runtime with the flag turned on.
runtime = _preview_module_system(
self.request,
descriptor=ItemFactory(category="lti", parent=self.course, spec=LTIBlock),
field_data=mock.Mock(),
)
assert runtime.anonymous_student_id == 'cf99fd26f9a41d4d9b4069739cc2be7b'