Provide more context to calls to Segment.
Implementation for DE-1089. Centralize the definition of context into a single method. This is in common/djangoapps/track because the context is originally set there by middleware.
This commit is contained in:
@@ -21,7 +21,6 @@ from functools import total_ordering
|
||||
from importlib import import_module
|
||||
from urllib import urlencode
|
||||
|
||||
import analytics
|
||||
from config_models.models import ConfigurationModel
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
@@ -66,7 +65,7 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.xmodule_django.models import NoneToEmptyManager
|
||||
from openedx.core.djangolib.model_mixins import DeletableByUserValue
|
||||
from track import contexts
|
||||
from track import contexts, segment
|
||||
from util.milestones_helpers import is_entrance_exams_enabled
|
||||
from util.model_utils import emit_field_changed_events, get_changed_fields_dict
|
||||
from util.query import use_read_replica_if_available
|
||||
@@ -739,7 +738,7 @@ class Registration(models.Model):
|
||||
has_segment_key = getattr(settings, 'LMS_SEGMENT_KEY', None)
|
||||
has_mailchimp_id = hasattr(settings, 'MAILCHIMP_NEW_USER_LIST_ID')
|
||||
if has_segment_key and has_mailchimp_id:
|
||||
identity_args = [
|
||||
segment.identify(
|
||||
self.user.id, # pylint: disable=no-member
|
||||
{
|
||||
'email': self.user.email,
|
||||
@@ -751,8 +750,7 @@ class Registration(models.Model):
|
||||
"listId": settings.MAILCHIMP_NEW_USER_LIST_ID
|
||||
}
|
||||
}
|
||||
]
|
||||
analytics.identify(*identity_args)
|
||||
)
|
||||
|
||||
|
||||
class PendingNameChange(DeletableByUserValue, models.Model):
|
||||
@@ -1429,25 +1427,17 @@ class CourseEnrollment(models.Model):
|
||||
'course_id': text_type(self.course_id),
|
||||
'mode': self.mode,
|
||||
}
|
||||
|
||||
segment_properties = {
|
||||
'category': 'conversion',
|
||||
'label': text_type(self.course_id),
|
||||
'org': self.course_id.org,
|
||||
'course': self.course_id.course,
|
||||
'run': self.course_id.run,
|
||||
'mode': self.mode,
|
||||
}
|
||||
with tracker.get_tracker().context(event_name, context):
|
||||
tracker.emit(event_name, data)
|
||||
|
||||
if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
analytics.track(self.user_id, event_name, {
|
||||
'category': 'conversion',
|
||||
'label': text_type(self.course_id),
|
||||
'org': self.course_id.org,
|
||||
'course': self.course_id.course,
|
||||
'run': self.course_id.run,
|
||||
'mode': self.mode,
|
||||
}, context={
|
||||
'ip': tracking_context.get('ip'),
|
||||
'Google Analytics': {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
})
|
||||
segment.track(self.user_id, event_name, segment_properties)
|
||||
|
||||
except: # pylint: disable=bare-except
|
||||
if event_name and self.course_id:
|
||||
|
||||
@@ -72,7 +72,7 @@ class TestActivateAccount(TestCase):
|
||||
LMS_SEGMENT_KEY="testkey",
|
||||
MAILCHIMP_NEW_USER_LIST_ID="listid"
|
||||
)
|
||||
@patch('student.models.analytics.identify')
|
||||
@patch('student.models.segment.identify')
|
||||
def test_activation_with_keys(self, mock_segment_identify):
|
||||
expected_segment_payload = {
|
||||
'email': self.email,
|
||||
@@ -98,16 +98,16 @@ class TestActivateAccount(TestCase):
|
||||
)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY="testkey")
|
||||
@patch('student.models.analytics.identify')
|
||||
@patch('student.models.segment.identify')
|
||||
def test_activation_without_mailchimp_key(self, mock_segment_identify):
|
||||
self.assert_no_tracking(mock_segment_identify)
|
||||
|
||||
@override_settings(MAILCHIMP_NEW_USER_LIST_ID="listid")
|
||||
@patch('student.models.analytics.identify')
|
||||
@patch('student.models.segment.identify')
|
||||
def test_activation_without_segment_key(self, mock_segment_identify):
|
||||
self.assert_no_tracking(mock_segment_identify)
|
||||
|
||||
@patch('student.models.analytics.identify')
|
||||
@patch('student.models.segment.identify')
|
||||
def test_activation_without_keys(self, mock_segment_identify):
|
||||
self.assert_no_tracking(mock_segment_identify)
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ from collections import OrderedDict
|
||||
from logging import getLogger
|
||||
from smtplib import SMTPException
|
||||
|
||||
import analytics
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.mail.message import EmailMessage
|
||||
@@ -79,12 +78,12 @@ from social_core.pipeline import partial
|
||||
from social_core.pipeline.social_auth import associate_by_email
|
||||
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from eventtracking import tracker
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.user_authn import cookies as user_authn_cookies
|
||||
from lms.djangoapps.verify_student.models import SSOVerification
|
||||
from lms.djangoapps.verify_student.utils import earliest_allowed_verification_date
|
||||
from third_party_auth.utils import user_exists
|
||||
from track import segment
|
||||
|
||||
from . import provider
|
||||
|
||||
@@ -657,23 +656,12 @@ def login_analytics(strategy, auth_entry, current_partial=None, *args, **kwargs)
|
||||
elif auth_entry in [AUTH_ENTRY_ACCOUNT_SETTINGS]:
|
||||
event_name = 'edx.bi.user.account.linked'
|
||||
|
||||
if event_name is not None and hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
analytics.track(
|
||||
kwargs['user'].id,
|
||||
event_name,
|
||||
{
|
||||
'category': "conversion",
|
||||
'label': None,
|
||||
'provider': kwargs['backend'].name
|
||||
},
|
||||
context={
|
||||
'ip': tracking_context.get('ip'),
|
||||
'Google Analytics': {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
}
|
||||
)
|
||||
if event_name is not None:
|
||||
segment.track(kwargs['user'].id, event_name, {
|
||||
'category': "conversion",
|
||||
'label': None,
|
||||
'provider': kwargs['backend'].name
|
||||
})
|
||||
|
||||
|
||||
@partial.partial
|
||||
|
||||
@@ -522,13 +522,13 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
|
||||
def test_canceling_authentication_redirects_to_root_when_auth_entry_not_set(self):
|
||||
self.assert_exception_redirect_looks_correct('/')
|
||||
|
||||
def test_full_pipeline_succeeds_for_linking_account(self):
|
||||
@mock.patch('third_party_auth.pipeline.segment.track')
|
||||
def test_full_pipeline_succeeds_for_linking_account(self, _mock_segment_track):
|
||||
# First, create, the request and strategy that store pipeline state,
|
||||
# configure the backend, and mock out wire traffic.
|
||||
request, strategy = self.get_request_and_strategy(
|
||||
auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete')
|
||||
request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy))
|
||||
pipeline.analytics.track = mock.MagicMock()
|
||||
request.user = self.create_user_models_for_existing_account(
|
||||
strategy, 'user@example.com', 'password', self.get_username(), skip_social_auth=True)
|
||||
|
||||
@@ -677,13 +677,13 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
|
||||
self.assert_account_settings_context_looks_correct(
|
||||
account_settings_context(request), duplicate=True, linked=True)
|
||||
|
||||
def test_full_pipeline_succeeds_for_signing_in_to_existing_active_account(self):
|
||||
@mock.patch('third_party_auth.pipeline.segment.track')
|
||||
def test_full_pipeline_succeeds_for_signing_in_to_existing_active_account(self, _mock_segment_track):
|
||||
# First, create, the request and strategy that store pipeline state,
|
||||
# configure the backend, and mock out wire traffic.
|
||||
request, strategy = self.get_request_and_strategy(
|
||||
auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete')
|
||||
strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy))
|
||||
pipeline.analytics.track = mock.MagicMock()
|
||||
user = self.create_user_models_for_existing_account(
|
||||
strategy, 'user@example.com', 'password', self.get_username())
|
||||
self.assert_social_auth_exists_for_user(user, strategy)
|
||||
|
||||
57
common/djangoapps/track/segment.py
Normal file
57
common/djangoapps/track/segment.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Wrapper methods for emitting events to Segment directly (rather than through tracking log events).
|
||||
|
||||
These take advantage of properties that are extracted from incoming requests by track middleware,
|
||||
stored in tracking context objects, and extracted here to be passed to Segment as part of context
|
||||
required by server-side events.
|
||||
|
||||
To use, call "from track import segment", then call segment.track() or segment.identify().
|
||||
|
||||
"""
|
||||
|
||||
import analytics
|
||||
|
||||
from django.conf import settings
|
||||
from eventtracking import tracker
|
||||
|
||||
|
||||
def track(user_id, event_name, properties=None, context=None):
|
||||
"""Wrapper for emitting Segment track event, including augmenting context information from middleware."""
|
||||
|
||||
if event_name is not None and hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
|
||||
properties = properties or {}
|
||||
segment_context = dict(context) if context else {}
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
|
||||
if 'ip' not in segment_context and 'ip' in tracking_context:
|
||||
segment_context['ip'] = tracking_context.get('ip')
|
||||
|
||||
if ('Google Analytics' not in segment_context or 'clientId' not in segment_context['Google Analytics']) and 'client_id' in tracking_context:
|
||||
segment_context['Google Analytics'] = {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
|
||||
if 'userAgent' not in segment_context and 'agent' in tracking_context:
|
||||
segment_context['userAgent'] = tracking_context.get('agent')
|
||||
|
||||
path = tracking_context.get('path')
|
||||
referer = tracking_context.get('referer')
|
||||
page = tracking_context.get('page')
|
||||
if path is not None or referer is not None or page is not None:
|
||||
if 'page' not in segment_context:
|
||||
segment_context['page'] = {}
|
||||
if path is not None and 'path' not in segment_context['page']:
|
||||
segment_context['page']['path'] = path
|
||||
if referer is not None and 'referrer' not in segment_context['page']:
|
||||
segment_context['page']['referrer'] = referer
|
||||
if page is not None and 'url' not in segment_context['page']:
|
||||
segment_context['page']['url'] = page
|
||||
|
||||
analytics.track(user_id, event_name, properties, segment_context)
|
||||
|
||||
|
||||
def identify(user_id, properties, context=None):
|
||||
"""Wrapper for emitting Segment identify event."""
|
||||
if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
|
||||
segment_context = dict(context) if context else {}
|
||||
analytics.identify(user_id, properties, segment_context)
|
||||
149
common/djangoapps/track/tests/test_segment.py
Normal file
149
common/djangoapps/track/tests/test_segment.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""Ensure emitted events contain the fields legacy processors expect to find."""
|
||||
|
||||
import ddt
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from eventtracking import tracker
|
||||
from eventtracking.django import DjangoTracker
|
||||
from mock import sentinel, patch
|
||||
|
||||
from track import segment
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class SegmentTrackTestCase(TestCase):
|
||||
"""Ensure emitted events contain the expected context values."""
|
||||
|
||||
def setUp(self):
|
||||
super(SegmentTrackTestCase, self).setUp()
|
||||
self.tracker = DjangoTracker()
|
||||
tracker.register_tracker(self.tracker)
|
||||
self.properties = {sentinel.key: sentinel.value}
|
||||
|
||||
patcher = patch('track.segment.analytics.track')
|
||||
self.mock_segment_track = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def test_missing_key(self):
|
||||
segment.track(sentinel.user_id, sentinel.name, self.properties)
|
||||
self.assertFalse(self.mock_segment_track.called)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY=None)
|
||||
def test_null_key(self):
|
||||
segment.track(sentinel.user_id, sentinel.name, self.properties)
|
||||
self.assertFalse(self.mock_segment_track.called)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY="testkey")
|
||||
def test_missing_name(self):
|
||||
segment.track(sentinel.user_id, None, self.properties)
|
||||
self.assertFalse(self.mock_segment_track.called)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY="testkey")
|
||||
def test_track_without_tracking_context(self):
|
||||
segment.track(sentinel.user_id, sentinel.name, self.properties)
|
||||
self.assertTrue(self.mock_segment_track.called)
|
||||
args, kwargs = self.mock_segment_track.call_args
|
||||
expected_segment_context = {}
|
||||
self.assertEqual((sentinel.user_id, sentinel.name, self.properties, expected_segment_context), args)
|
||||
|
||||
@ddt.unpack
|
||||
@ddt.data(
|
||||
({'ip': sentinel.ip}, {'ip': sentinel.provided_ip}, {'ip': sentinel.ip}),
|
||||
({'agent': sentinel.agent}, {'userAgent': sentinel.provided_agent}, {'userAgent': sentinel.agent}),
|
||||
({'path': sentinel.path}, {'page': {'path': sentinel.provided_path}}, {'page': {'path': sentinel.path}}),
|
||||
({'referer': sentinel.referer}, {'page': {'referrer': sentinel.provided_referer}}, {'page': {'referrer': sentinel.referer}}),
|
||||
({'page': sentinel.page}, {'page': {'url': sentinel.provided_page}}, {'page': {'url': sentinel.page}}),
|
||||
({'client_id': sentinel.client_id}, {'Google Analytics': {'clientId': sentinel.provided_client_id}}, {'Google Analytics': {'clientId': sentinel.client_id}}),
|
||||
)
|
||||
@override_settings(LMS_SEGMENT_KEY="testkey")
|
||||
def test_track_context_with_stuff(self, tracking_context, provided_context, expected_segment_context):
|
||||
# Test first with tracking and no provided context.
|
||||
with self.tracker.context('test', tracking_context):
|
||||
segment.track(sentinel.user_id, sentinel.name, self.properties)
|
||||
args, kwargs = self.mock_segment_track.call_args
|
||||
self.assertEqual((sentinel.user_id, sentinel.name, self.properties, expected_segment_context), args)
|
||||
|
||||
# Test with provided context and no tracking context.
|
||||
segment.track(sentinel.user_id, sentinel.name, self.properties, provided_context)
|
||||
args, kwargs = self.mock_segment_track.call_args
|
||||
self.assertEqual((sentinel.user_id, sentinel.name, self.properties, provided_context), args)
|
||||
|
||||
# Test with provided context and also tracking context.
|
||||
with self.tracker.context('test', tracking_context):
|
||||
segment.track(sentinel.user_id, sentinel.name, self.properties, provided_context)
|
||||
self.assertTrue(self.mock_segment_track.called)
|
||||
args, kwargs = self.mock_segment_track.call_args
|
||||
self.assertEqual((sentinel.user_id, sentinel.name, self.properties, provided_context), args)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY="testkey")
|
||||
def test_track_with_standard_context(self):
|
||||
|
||||
tracking_context = {
|
||||
'accept_language': sentinel.accept_language,
|
||||
'referer': sentinel.referer,
|
||||
'username': sentinel.username,
|
||||
'session': sentinel.session,
|
||||
'ip': sentinel.ip,
|
||||
'host': sentinel.host,
|
||||
'agent': sentinel.agent,
|
||||
'path': sentinel.path,
|
||||
'user_id': sentinel.user_id,
|
||||
'course_id': sentinel.course_id,
|
||||
'org_id': sentinel.org_id,
|
||||
'client_id': sentinel.client_id,
|
||||
}
|
||||
with self.tracker.context('test', tracking_context):
|
||||
segment.track(sentinel.user_id, sentinel.name, self.properties)
|
||||
|
||||
self.assertTrue(self.mock_segment_track.called)
|
||||
args, kwargs = self.mock_segment_track.call_args
|
||||
|
||||
expected_segment_context = {
|
||||
'ip': sentinel.ip,
|
||||
'Google Analytics': {
|
||||
'clientId': sentinel.client_id,
|
||||
},
|
||||
'userAgent': sentinel.agent,
|
||||
'page': {
|
||||
'path': sentinel.path,
|
||||
'referrer': sentinel.referer,
|
||||
# No URL value.
|
||||
}
|
||||
}
|
||||
self.assertEqual((sentinel.user_id, sentinel.name, self.properties, expected_segment_context), args)
|
||||
|
||||
|
||||
class SegmentIdentifyTestCase(TestCase):
|
||||
"""Ensure emitted events contain the fields legacy processors expect to find."""
|
||||
|
||||
def setUp(self):
|
||||
super(SegmentIdentifyTestCase, self).setUp()
|
||||
patcher = patch('track.segment.analytics.identify')
|
||||
self.mock_segment_identify = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
self.properties = {sentinel.key: sentinel.value}
|
||||
|
||||
def test_missing_key(self):
|
||||
segment.identify(sentinel.user_id, self.properties)
|
||||
self.assertFalse(self.mock_segment_identify.called)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY=None)
|
||||
def test_null_key(self):
|
||||
segment.identify(sentinel.user_id, self.properties)
|
||||
self.assertFalse(self.mock_segment_identify.called)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY="testkey")
|
||||
def test_normal_call(self):
|
||||
segment.identify(sentinel.user_id, self.properties)
|
||||
self.assertTrue(self.mock_segment_identify.called)
|
||||
args, kwargs = self.mock_segment_identify.call_args
|
||||
self.assertEqual((sentinel.user_id, self.properties, {}), args)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY="testkey")
|
||||
def test_call_with_context(self):
|
||||
provided_context = {sentinel.context_key: sentinel.context_value}
|
||||
segment.identify(sentinel.user_id, self.properties, provided_context)
|
||||
self.assertTrue(self.mock_segment_identify.called)
|
||||
args, kwargs = self.mock_segment_identify.call_args
|
||||
self.assertEqual((sentinel.user_id, self.properties, provided_context), args)
|
||||
@@ -38,13 +38,13 @@ class TestCourseGoalsAPI(EventTrackingTestCase, SharedModuleStoreTestCase):
|
||||
|
||||
self.apiUrl = reverse('course_goals_api:v0:course_goal-list')
|
||||
|
||||
@mock.patch('lms.djangoapps.course_goals.views.update_google_analytics')
|
||||
@mock.patch('lms.djangoapps.course_goals.views.segment.track')
|
||||
@override_settings(LMS_SEGMENT_KEY="foobar")
|
||||
def test_add_valid_goal(self, ga_call):
|
||||
""" Ensures a correctly formatted post succeeds."""
|
||||
response = self.post_course_goal(valid=True, goal_key='certify')
|
||||
self.assertEqual(self.get_event(-1)['name'], EVENT_NAME_ADDED)
|
||||
ga_call.assert_called_with(EVENT_NAME_ADDED, self.user.id)
|
||||
ga_call.assert_called_with(self.user.id, EVENT_NAME_ADDED)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
current_goals = CourseGoal.objects.filter(user=self.user, course_key=self.course.id)
|
||||
@@ -68,7 +68,7 @@ class TestCourseGoalsAPI(EventTrackingTestCase, SharedModuleStoreTestCase):
|
||||
status_code=400
|
||||
)
|
||||
|
||||
@mock.patch('lms.djangoapps.course_goals.views.update_google_analytics')
|
||||
@mock.patch('lms.djangoapps.course_goals.views.segment.track')
|
||||
@override_settings(LMS_SEGMENT_KEY="foobar")
|
||||
def test_update_goal(self, ga_call):
|
||||
""" Ensures that repeated course goal post events do not create new instances of the goal. """
|
||||
@@ -77,7 +77,7 @@ class TestCourseGoalsAPI(EventTrackingTestCase, SharedModuleStoreTestCase):
|
||||
self.post_course_goal(valid=True, goal_key='unsure')
|
||||
self.assertEqual(self.get_event(-1)['name'], EVENT_NAME_UPDATED)
|
||||
|
||||
ga_call.assert_called_with(EVENT_NAME_UPDATED, self.user.id)
|
||||
ga_call.assert_called_with(self.user.id, EVENT_NAME_UPDATED)
|
||||
current_goals = CourseGoal.objects.filter(user=self.user, course_key=self.course.id)
|
||||
self.assertEqual(len(current_goals), 1)
|
||||
self.assertEqual(current_goals[0].goal_key, 'unsure')
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Course Goals Views - includes REST API
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
import analytics
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
@@ -17,6 +16,8 @@ from rest_framework import permissions, serializers, viewsets, status
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.response import Response
|
||||
|
||||
from track import segment
|
||||
|
||||
from .api import get_course_goal_options
|
||||
from .models import CourseGoal, GOAL_KEY_CHOICES
|
||||
|
||||
@@ -110,6 +111,7 @@ class CourseGoalViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@receiver(post_save, sender=CourseGoal, dispatch_uid="emit_course_goals_event")
|
||||
def emit_course_goal_event(sender, instance, **kwargs):
|
||||
"""Emit events for both tracking logs and for Segment."""
|
||||
name = 'edx.course.goal.added' if kwargs.get('created', False) else 'edx.course.goal.updated'
|
||||
tracker.emit(
|
||||
name,
|
||||
@@ -117,21 +119,4 @@ def emit_course_goal_event(sender, instance, **kwargs):
|
||||
'goal_key': instance.goal_key,
|
||||
}
|
||||
)
|
||||
if settings.LMS_SEGMENT_KEY:
|
||||
update_google_analytics(name, instance.user.id)
|
||||
|
||||
|
||||
def update_google_analytics(name, user_id):
|
||||
""" Update student course goal for Google Analytics using Segment. """
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
context = {
|
||||
'ip': tracking_context.get('ip'),
|
||||
'Google Analytics': {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
}
|
||||
analytics.track(
|
||||
user_id,
|
||||
name,
|
||||
context=context
|
||||
)
|
||||
segment.track(instance.user.id, name)
|
||||
|
||||
@@ -102,8 +102,9 @@ class FieldOverrideProvider(object):
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
def __init__(self, user):
|
||||
def __init__(self, user, fallback_field_data):
|
||||
self.user = user
|
||||
self.fallback_field_data = fallback_field_data
|
||||
|
||||
@abstractmethod
|
||||
def get(self, block, name, default): # pragma no cover
|
||||
@@ -196,7 +197,7 @@ class OverrideFieldData(FieldData):
|
||||
|
||||
def __init__(self, user, fallback, providers):
|
||||
self.fallback = fallback
|
||||
self.providers = tuple(provider(user) for provider in providers)
|
||||
self.providers = tuple(provider(user, fallback) for provider in providers)
|
||||
|
||||
def get_override(self, block, name):
|
||||
"""
|
||||
|
||||
@@ -2191,9 +2191,9 @@ class GenerateUserCertTests(ModuleStoreTestCase):
|
||||
def test_user_with_passing_grade(self, mock_is_course_passed):
|
||||
# If user has above passing grading then json will return cert generating message and
|
||||
# status valid code
|
||||
# mocking xqueue and analytics
|
||||
# mocking xqueue and Segment analytics
|
||||
|
||||
analytics_patcher = patch('courseware.views.views.analytics')
|
||||
analytics_patcher = patch('courseware.views.views.segment')
|
||||
mock_tracker = analytics_patcher.start()
|
||||
self.addCleanup(analytics_patcher.stop)
|
||||
|
||||
@@ -2211,11 +2211,6 @@ class GenerateUserCertTests(ModuleStoreTestCase):
|
||||
'category': 'certificates',
|
||||
'label': unicode(self.course.id)
|
||||
},
|
||||
|
||||
context={
|
||||
'ip': '127.0.0.1',
|
||||
'Google Analytics': {'clientId': None}
|
||||
}
|
||||
)
|
||||
mock_tracker.reset_mock()
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import urllib
|
||||
from collections import OrderedDict, namedtuple
|
||||
from datetime import datetime
|
||||
|
||||
import analytics
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
@@ -98,6 +97,7 @@ from openedx.features.enterprise_support.api import data_sharing_consent_require
|
||||
from openedx.features.journals.api import get_journals_context
|
||||
from shoppingcart.utils import is_shopping_cart_enabled
|
||||
from student.models import CourseEnrollment, UserTestGroup
|
||||
from track import segment
|
||||
from util.cache import cache, cache_if_anonymous
|
||||
from util.db import outer_atomic
|
||||
from util.milestones_helpers import get_prerequisite_courses_display
|
||||
@@ -1429,24 +1429,11 @@ def _track_successful_certificate_generation(user_id, course_id):
|
||||
None
|
||||
|
||||
"""
|
||||
if settings.LMS_SEGMENT_KEY:
|
||||
event_name = 'edx.bi.user.certificate.generate'
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
|
||||
analytics.track(
|
||||
user_id,
|
||||
event_name,
|
||||
{
|
||||
'category': 'certificates',
|
||||
'label': text_type(course_id)
|
||||
},
|
||||
context={
|
||||
'ip': tracking_context.get('ip'),
|
||||
'Google Analytics': {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
}
|
||||
)
|
||||
event_name = 'edx.bi.user.certificate.generate'
|
||||
segment.track(user_id, event_name, {
|
||||
'category': 'certificates',
|
||||
'label': text_type(course_id)
|
||||
})
|
||||
|
||||
|
||||
@require_http_methods(["GET", "POST"])
|
||||
|
||||
@@ -11,7 +11,6 @@ from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
|
||||
import analytics
|
||||
import pytz
|
||||
from boto.exception import BotoServerError # this is a super-class of SESError and catches connection errors
|
||||
from config_models.models import ConfigurationModel
|
||||
@@ -37,9 +36,11 @@ from courseware.courses import get_course_by_id
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from eventtracking import tracker
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from shoppingcart.pdf import PDFInvoice
|
||||
from student.models import CourseEnrollment, EnrollStatusChange
|
||||
from student.signals import UNENROLL_DONE
|
||||
from track import segment
|
||||
from util.query import use_read_replica_if_available
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
@@ -520,19 +521,12 @@ class Order(models.Model):
|
||||
|
||||
"""
|
||||
try:
|
||||
if settings.LMS_SEGMENT_KEY:
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
analytics.track(self.user.id, event_name, {
|
||||
'orderId': self.id,
|
||||
'total': str(self.total_cost),
|
||||
'currency': self.currency,
|
||||
'products': [item.analytics_data() for item in orderitems]
|
||||
}, context={
|
||||
'ip': tracking_context.get('ip'),
|
||||
'Google Analytics': {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
})
|
||||
segment.track(self.user.id, event_name, {
|
||||
'orderId': self.id,
|
||||
'total': str(self.total_cost),
|
||||
'currency': self.currency,
|
||||
'products': [item.analytics_data() for item in orderitems]
|
||||
})
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# Capturing all exceptions thrown while tracking analytics events. We do not want
|
||||
@@ -1578,7 +1572,7 @@ class PaidCourseRegistration(OrderItem):
|
||||
item.unit_cost = cost
|
||||
item.list_price = cost
|
||||
item.line_desc = _(u'Registration for Course: {course_name}').format(
|
||||
course_name=course.display_name_with_default_escaped)
|
||||
course_name=course.display_name_with_default)
|
||||
item.currency = currency
|
||||
order.currency = currency
|
||||
item.report_comments = item.csv_report_comments
|
||||
@@ -1618,12 +1612,12 @@ class PaidCourseRegistration(OrderItem):
|
||||
Generates instructions when the user has purchased a PaidCourseRegistration.
|
||||
Basically tells the user to visit the dashboard to see their new classes
|
||||
"""
|
||||
notification = _(
|
||||
notification = Text(_(
|
||||
u"Please visit your {link_start}dashboard{link_end} "
|
||||
u"to see your new course."
|
||||
).format(
|
||||
link_start=u'<a href="{url}">'.format(url=reverse('dashboard')),
|
||||
link_end=u'</a>',
|
||||
)).format(
|
||||
link_start=HTML(u'<a href="{url}">').format(url=reverse('dashboard')),
|
||||
link_end=HTML(u'</a>'),
|
||||
)
|
||||
|
||||
return self.pk_with_subclass, set([notification])
|
||||
@@ -1761,7 +1755,7 @@ class CourseRegCodeItem(OrderItem):
|
||||
item.list_price = cost
|
||||
item.qty = qty
|
||||
item.line_desc = _(u'Enrollment codes for Course: {course_name}').format(
|
||||
course_name=course.display_name_with_default_escaped)
|
||||
course_name=course.display_name_with_default)
|
||||
item.currency = currency
|
||||
order.currency = currency
|
||||
item.report_comments = item.csv_report_comments
|
||||
|
||||
@@ -81,7 +81,7 @@ class OrderTest(ModuleStoreTestCase):
|
||||
self.cost = 40
|
||||
|
||||
# Add mock tracker for event testing.
|
||||
patcher = patch('shoppingcart.models.analytics')
|
||||
patcher = patch('shoppingcart.models.segment')
|
||||
self.mock_tracker = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
@@ -288,7 +288,6 @@ class OrderTest(ModuleStoreTestCase):
|
||||
}
|
||||
]
|
||||
},
|
||||
context={'ip': None, 'Google Analytics': {'clientId': None}}
|
||||
)
|
||||
|
||||
def test_purchase_item_failure(self):
|
||||
@@ -862,7 +861,7 @@ class CertificateItemTest(ModuleStoreTestCase):
|
||||
self.mock_tracker = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
analytics_patcher = patch('shoppingcart.models.analytics')
|
||||
analytics_patcher = patch('shoppingcart.models.segment')
|
||||
self.mock_analytics_tracker = analytics_patcher.start()
|
||||
self.addCleanup(analytics_patcher.stop)
|
||||
|
||||
@@ -888,7 +887,6 @@ class CertificateItemTest(ModuleStoreTestCase):
|
||||
}
|
||||
]
|
||||
},
|
||||
context={'ip': None, 'Google Analytics': {'clientId': None}}
|
||||
)
|
||||
|
||||
def test_existing_enrollment(self):
|
||||
@@ -912,7 +910,7 @@ class CertificateItemTest(ModuleStoreTestCase):
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY="foobar")
|
||||
@patch.dict(settings.FEATURES, {'STORE_BILLING_INFO': True})
|
||||
@patch('lms.djangoapps.course_goals.views.update_google_analytics', Mock(return_value=True))
|
||||
@patch('lms.djangoapps.course_goals.views.segment.track', Mock(return_value=True))
|
||||
@patch('student.models.CourseEnrollment.refund_cutoff_date')
|
||||
def test_refund_cert_callback_no_expiration(self, cutoff_date):
|
||||
# When there is no expiration date on a verified mode, the user can always get a refund
|
||||
@@ -951,7 +949,7 @@ class CertificateItemTest(ModuleStoreTestCase):
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY="foobar")
|
||||
@patch.dict(settings.FEATURES, {'STORE_BILLING_INFO': True})
|
||||
@patch('lms.djangoapps.course_goals.views.update_google_analytics', Mock(return_value=True))
|
||||
@patch('lms.djangoapps.course_goals.views.segment.track', Mock(return_value=True))
|
||||
@patch('student.models.CourseEnrollment.refund_cutoff_date')
|
||||
def test_refund_cert_callback_before_expiration(self, cutoff_date):
|
||||
# If the expiration date has not yet passed on a verified mode, the user can be refunded
|
||||
|
||||
@@ -8,7 +8,6 @@ import json
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
import analytics
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
@@ -49,6 +48,7 @@ from openedx.core.lib.log_utils import audit_log
|
||||
from shoppingcart.models import CertificateItem, Order
|
||||
from shoppingcart.processors import get_purchase_endpoint, get_signed_purchase_params
|
||||
from student.models import CourseEnrollment
|
||||
from track import segment
|
||||
from util.db import outer_atomic
|
||||
from util.json_request import JsonResponse
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -913,7 +913,7 @@ class SubmitPhotosView(View):
|
||||
return response
|
||||
|
||||
# Submit the attempt
|
||||
attempt = self._submit_attempt(request.user, face_image, photo_id_image, initial_verification)
|
||||
self._submit_attempt(request.user, face_image, photo_id_image, initial_verification)
|
||||
|
||||
self._fire_event(request.user, "edx.bi.verify.submitted", {"category": "verification"})
|
||||
self._send_confirmation_email(request.user)
|
||||
@@ -1096,15 +1096,7 @@ class SubmitPhotosView(View):
|
||||
Returns: None
|
||||
|
||||
"""
|
||||
if settings.LMS_SEGMENT_KEY:
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
context = {
|
||||
'ip': tracking_context.get('ip'),
|
||||
'Google Analytics': {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
}
|
||||
analytics.track(user.id, event_name, parameters, context=context)
|
||||
segment.track(user.id, event_name, parameters)
|
||||
|
||||
|
||||
@require_POST
|
||||
|
||||
@@ -2,7 +2,6 @@ import datetime
|
||||
import logging
|
||||
import random
|
||||
|
||||
import analytics
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
@@ -17,6 +16,7 @@ from openedx.core.djangoapps.schedules.models import ScheduleExperience
|
||||
from openedx.core.djangoapps.schedules.content_highlights import course_has_highlights
|
||||
from openedx.core.djangoapps.theming.helpers import get_current_site
|
||||
from student.models import CourseEnrollment
|
||||
from track import segment
|
||||
from .config import CREATE_SCHEDULE_WAFFLE_FLAG
|
||||
from .models import Schedule, ScheduleConfig
|
||||
from .tasks import update_course_schedules
|
||||
@@ -53,7 +53,7 @@ def create_schedule(sender, **kwargs): # pylint: disable=unused-argument
|
||||
))
|
||||
|
||||
|
||||
def update_schedules_on_course_start_changed(sender, updated_course_overview, previous_start_date, **kwargs):
|
||||
def update_schedules_on_course_start_changed(sender, updated_course_overview, previous_start_date, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Updates all course schedules start and upgrade_deadline dates based off of
|
||||
the new course overview start date.
|
||||
@@ -136,9 +136,9 @@ def _should_randomly_suppress_schedule_creation(
|
||||
upgrade_deadline_str = None
|
||||
if upgrade_deadline:
|
||||
upgrade_deadline_str = upgrade_deadline.isoformat()
|
||||
analytics.track(
|
||||
segment.track(
|
||||
user_id=enrollment.user.id,
|
||||
event='edx.bi.schedule.suppressed',
|
||||
event_name='edx.bi.schedule.suppressed',
|
||||
properties={
|
||||
'course_id': unicode(enrollment.course_id),
|
||||
'experience_type': experience_type,
|
||||
|
||||
@@ -97,7 +97,7 @@ class CreateScheduleTests(SharedModuleStoreTestCase):
|
||||
self.assert_schedule_created(experience_type=ScheduleExperience.EXPERIENCES.course_updates)
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
@patch('analytics.track')
|
||||
@patch('openedx.core.djangoapps.schedules.signals.segment.track')
|
||||
@patch('openedx.core.djangoapps.schedules.signals.random.random', return_value=0.2)
|
||||
@ddt.data(
|
||||
(0, True),
|
||||
@@ -121,7 +121,7 @@ class CreateScheduleTests(SharedModuleStoreTestCase):
|
||||
else:
|
||||
self.assert_schedule_not_created()
|
||||
mock_track.assert_called_once()
|
||||
assert mock_track.call_args[1].get('event') == 'edx.bi.schedule.suppressed'
|
||||
assert mock_track.call_args[1].get('event_name') == 'edx.bi.schedule.suppressed'
|
||||
|
||||
@patch('openedx.core.djangoapps.schedules.signals.log.exception')
|
||||
@patch('openedx.core.djangoapps.schedules.signals.Schedule.objects.create')
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
API for managing user preferences.
|
||||
"""
|
||||
import logging
|
||||
import analytics
|
||||
from eventtracking import tracker
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
@@ -17,6 +15,7 @@ from pytz import common_timezones, common_timezones_set, country_timezones
|
||||
from six import text_type
|
||||
|
||||
from student.models import User, UserProfile
|
||||
from track import segment
|
||||
from ..errors import (
|
||||
UserAPIInternalError, UserAPIRequestError, UserNotFound, UserNotAuthorized,
|
||||
PreferenceValidationError, PreferenceUpdateError, CountryCodeError
|
||||
@@ -284,21 +283,13 @@ def _track_update_email_opt_in(user_id, organization, opt_in):
|
||||
|
||||
"""
|
||||
event_name = 'edx.bi.user.org_email.opted_in' if opt_in else 'edx.bi.user.org_email.opted_out'
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
|
||||
analytics.track(
|
||||
segment.track(
|
||||
user_id,
|
||||
event_name,
|
||||
{
|
||||
'category': 'communication',
|
||||
'label': organization
|
||||
},
|
||||
context={
|
||||
'ip': tracking_context.get('ip'),
|
||||
'Google Analytics': {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ Much of this file was broken out from views.py, previous history can be found th
|
||||
|
||||
import logging
|
||||
|
||||
import analytics
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate, login as django_login
|
||||
from django.contrib.auth.decorators import login_required
|
||||
@@ -19,7 +18,6 @@ from django.views.decorators.http import require_http_methods
|
||||
from ratelimitbackend.exceptions import RateLimitException
|
||||
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from eventtracking import tracker
|
||||
from openedx.core.djangoapps.user_authn.cookies import set_logged_in_cookies, refresh_jwt_cookies
|
||||
from openedx.core.djangoapps.user_authn.exceptions import AuthFailedError
|
||||
import openedx.core.djangoapps.external_auth.views
|
||||
@@ -34,6 +32,7 @@ from student.models import (
|
||||
)
|
||||
from student.views import send_reactivation_email_for_user
|
||||
from student.forms import send_password_reset_email_for_user
|
||||
from track import segment
|
||||
import third_party_auth
|
||||
from third_party_auth import pipeline, provider
|
||||
from util.json_request import JsonResponse
|
||||
@@ -274,37 +273,28 @@ def _track_user_login(user, request):
|
||||
"""
|
||||
Sends a tracking event for a successful login.
|
||||
"""
|
||||
if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
analytics.identify(
|
||||
user.id,
|
||||
{
|
||||
'email': request.POST['email'],
|
||||
'username': user.username
|
||||
},
|
||||
{
|
||||
# Disable MailChimp because we don't want to update the user's email
|
||||
# and username in MailChimp on every page load. We only need to capture
|
||||
# this data on registration/activation.
|
||||
'MailChimp': False
|
||||
}
|
||||
)
|
||||
|
||||
analytics.track(
|
||||
user.id,
|
||||
"edx.bi.user.account.authenticated",
|
||||
{
|
||||
'category': "conversion",
|
||||
'label': request.POST.get('course_id'),
|
||||
'provider': None
|
||||
},
|
||||
context={
|
||||
'ip': tracking_context.get('ip'),
|
||||
'Google Analytics': {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
}
|
||||
)
|
||||
segment.identify(
|
||||
user.id,
|
||||
{
|
||||
'email': request.POST.get('email'),
|
||||
'username': user.username
|
||||
},
|
||||
{
|
||||
# Disable MailChimp because we don't want to update the user's email
|
||||
# and username in MailChimp on every page load. We only need to capture
|
||||
# this data on registration/activation.
|
||||
'MailChimp': False
|
||||
}
|
||||
)
|
||||
segment.track(
|
||||
user.id,
|
||||
"edx.bi.user.account.authenticated",
|
||||
{
|
||||
'category': "conversion",
|
||||
'label': request.POST.get('course_id'),
|
||||
'provider': None
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
|
||||
@@ -6,7 +6,6 @@ import datetime
|
||||
import json
|
||||
import logging
|
||||
|
||||
import analytics
|
||||
import dogstats_wrapper as dog_stats_api
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import login as django_login
|
||||
@@ -17,7 +16,6 @@ from django.db import transaction
|
||||
from django.dispatch import Signal
|
||||
from django.utils.translation import get_language
|
||||
from django.utils.translation import ugettext as _
|
||||
from eventtracking import tracker
|
||||
# Note that this lives in LMS, so this dependency should be refactored.
|
||||
from notification_prefs.views import enable_notifications
|
||||
from pytz import UTC
|
||||
@@ -44,6 +42,7 @@ from student.models import (
|
||||
create_comments_service_user,
|
||||
)
|
||||
from student.views import compose_and_send_activation_email
|
||||
from track import segment
|
||||
import third_party_auth
|
||||
from third_party_auth import pipeline, provider
|
||||
from third_party_auth.saml import SAP_SUCCESSFACTORS_SAML_KEY
|
||||
@@ -316,7 +315,6 @@ def _link_user_to_third_party_provider(
|
||||
def _track_user_registration(user, profile, params, third_party_provider):
|
||||
""" Track the user's registration. """
|
||||
if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
identity_args = [
|
||||
user.id,
|
||||
{
|
||||
@@ -332,7 +330,7 @@ def _track_user_registration(user, profile, params, third_party_provider):
|
||||
'country': text_type(profile.country),
|
||||
}
|
||||
]
|
||||
|
||||
# Provide additional context only if needed.
|
||||
if hasattr(settings, 'MAILCHIMP_NEW_USER_LIST_ID'):
|
||||
identity_args.append({
|
||||
"MailChimp": {
|
||||
@@ -340,9 +338,8 @@ def _track_user_registration(user, profile, params, third_party_provider):
|
||||
}
|
||||
})
|
||||
|
||||
analytics.identify(*identity_args)
|
||||
|
||||
analytics.track(
|
||||
segment.identify(*identity_args)
|
||||
segment.track(
|
||||
user.id,
|
||||
"edx.bi.user.account.registered",
|
||||
{
|
||||
@@ -350,12 +347,6 @@ def _track_user_registration(user, profile, params, third_party_provider):
|
||||
'label': params.get('course_id'),
|
||||
'provider': third_party_provider.name if third_party_provider else None
|
||||
},
|
||||
context={
|
||||
'ip': tracking_context.get('ip'),
|
||||
'Google Analytics': {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -181,8 +181,8 @@ class TestCreateAccount(SiteMixin, TestCase):
|
||||
"Microsites not implemented in this environment"
|
||||
)
|
||||
@override_settings(LMS_SEGMENT_KEY="testkey")
|
||||
@mock.patch('openedx.core.djangoapps.user_authn.views.register.analytics.track')
|
||||
@mock.patch('openedx.core.djangoapps.user_authn.views.register.analytics.identify')
|
||||
@mock.patch('openedx.core.djangoapps.user_authn.views.register.segment.track')
|
||||
@mock.patch('openedx.core.djangoapps.user_authn.views.register.segment.identify')
|
||||
def test_segment_tracking(self, mock_segment_identify, _):
|
||||
year = datetime.now().year
|
||||
year_of_birth = year - 14
|
||||
|
||||
@@ -4,7 +4,7 @@ students in the Unlocked Group of the ContentTypeGating partition.
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
from lms.djangoapps.courseware.field_overrides import FieldOverrideProvider, disable_overrides
|
||||
from lms.djangoapps.courseware.field_overrides import FieldOverrideProvider
|
||||
from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID
|
||||
from openedx.features.course_duration_limits.config import (
|
||||
CONTENT_TYPE_GATING_FLAG,
|
||||
@@ -31,9 +31,23 @@ class ContentTypeGatingFieldOverride(FieldOverrideProvider):
|
||||
if not problem_eligible_for_content_gating:
|
||||
return default
|
||||
|
||||
# Read the group_access from the fallback field-data service
|
||||
with disable_overrides():
|
||||
original_group_access = block.group_access
|
||||
# We want to fetch the value set by course authors since it should take precedence.
|
||||
# We cannot simply call "block.group_access" to fetch that value even if we disable
|
||||
# field overrides since it will set the group access field to "dirty" with
|
||||
# the value read from the course content. Since most content does not have any
|
||||
# value for this field it will usually be the default empty dict. This field
|
||||
# override changes the value, however, resulting in the LMS thinking that the
|
||||
# field data needs to be written back out to the store. This doesn't work,
|
||||
# however, since this is a read-only setting in the LMS context. After this
|
||||
# call to get() returns, the _dirty_fields dict will be set correctly to contain
|
||||
# the value from this field override. This prevents the system from attempting
|
||||
# to save the overridden value when it thinks it has changed when it hasn't.
|
||||
original_group_access = None
|
||||
if self.fallback_field_data.has(block, 'group_access'):
|
||||
raw_value = self.fallback_field_data.get(block, 'group_access')
|
||||
group_access_field = block.fields.get('group_access')
|
||||
if group_access_field is not None:
|
||||
original_group_access = group_access_field.from_json(raw_value)
|
||||
|
||||
if original_group_access is None:
|
||||
original_group_access = {}
|
||||
|
||||
@@ -3,17 +3,17 @@ Test audit user's access to various content based on content-gating features.
|
||||
"""
|
||||
|
||||
import ddt
|
||||
from django.http import Http404
|
||||
from django.conf import settings
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from mock import patch
|
||||
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from courseware.access_response import IncorrectPartitionGroupError
|
||||
from lms.djangoapps.courseware.module_render import load_single_xblock
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
|
||||
from openedx.core.lib.url_utils import quote_slashes
|
||||
from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID
|
||||
from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
|
||||
from student.tests.factories import TEST_PASSWORD, AdminFactory, CourseEnrollmentFactory, UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
@@ -82,14 +82,14 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
cls.graded_score_weight_blocks[(graded, has_score, weight)] = block
|
||||
|
||||
# add LTI blocks to default course
|
||||
cls.lti_block = ItemFactory.create(
|
||||
cls.blocks_dict['lti_block'] = ItemFactory.create(
|
||||
parent=cls.blocks_dict['vertical'],
|
||||
category='lti_consumer',
|
||||
display_name='lti_consumer',
|
||||
has_score=True,
|
||||
graded=True,
|
||||
)
|
||||
cls.lti_block_not_scored = ItemFactory.create(
|
||||
cls.blocks_dict['lti_block_not_scored'] = ItemFactory.create(
|
||||
parent=cls.blocks_dict['vertical'],
|
||||
category='lti_consumer',
|
||||
display_name='lti_consumer_2',
|
||||
@@ -97,19 +97,32 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
)
|
||||
|
||||
# add ungraded problem for xblock_handler test
|
||||
cls.graded_problem = ItemFactory.create(
|
||||
cls.blocks_dict['graded_problem'] = ItemFactory.create(
|
||||
parent=cls.blocks_dict['vertical'],
|
||||
category='problem',
|
||||
display_name='graded_problem',
|
||||
graded=True,
|
||||
)
|
||||
cls.ungraded_problem = ItemFactory.create(
|
||||
cls.blocks_dict['ungraded_problem'] = ItemFactory.create(
|
||||
parent=cls.blocks_dict['vertical'],
|
||||
category='problem',
|
||||
display_name='ungraded_problem',
|
||||
graded=False,
|
||||
)
|
||||
|
||||
cls.blocks_dict['audit_visible_graded_problem'] = ItemFactory.create(
|
||||
parent=cls.blocks_dict['vertical'],
|
||||
category='problem',
|
||||
display_name='audit_visible_graded_problem',
|
||||
graded=True,
|
||||
group_access={
|
||||
CONTENT_GATING_PARTITION_ID: [
|
||||
settings.CONTENT_TYPE_GATE_GROUP_IDS['limited_access'],
|
||||
settings.CONTENT_TYPE_GATE_GROUP_IDS['full_access']
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
# audit_only course only has an audit track available
|
||||
cls.courses['audit_only'] = cls._create_course(
|
||||
run='audit_only_course_run_1',
|
||||
@@ -205,14 +218,14 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
display_name='Lesson 1 Vertical - Unit 1'
|
||||
)
|
||||
|
||||
for problem_type in component_types:
|
||||
for component_type in component_types:
|
||||
block = ItemFactory.create(
|
||||
parent=blocks_dict['vertical'],
|
||||
category=problem_type,
|
||||
display_name=problem_type,
|
||||
category=component_type,
|
||||
display_name=component_type,
|
||||
graded=True,
|
||||
)
|
||||
blocks_dict[problem_type] = block
|
||||
blocks_dict[component_type] = block
|
||||
|
||||
return {
|
||||
'course': course,
|
||||
@@ -224,14 +237,8 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Asserts that a block in a specific course is gated for a specific user
|
||||
|
||||
This functions asserts whether the passed in block is gated by content type gating.
|
||||
This is determined by checking whether the has_access method called the IncorrectPartitionGroupError.
|
||||
This error gets swallowed up and is raised as a 404, which is why we are checking for a 404 being raised.
|
||||
However, the 404 could also be caused by other errors, which is why the actual assertion is checking
|
||||
whether the IncorrectPartitionGroupError was called.
|
||||
|
||||
Arguments:
|
||||
block: some soft of xblock descriptor, must implement .scope_ids.usage_id
|
||||
block: some sort of xblock descriptor, must implement .scope_ids.usage_id
|
||||
is_gated (bool): if True, this user is expected to be gated from this block
|
||||
user_id (int): id of user, if not set will be set to self.audit_user.id
|
||||
course_id (CourseLocator): id of course, if not set will be set to self.course.id
|
||||
@@ -239,61 +246,52 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
fake_request = self.factory.get('')
|
||||
mock_get_current_request.return_value = fake_request
|
||||
|
||||
with patch.object(IncorrectPartitionGroupError, '__init__',
|
||||
wraps=IncorrectPartitionGroupError.__init__) as mock_access_error:
|
||||
if is_gated:
|
||||
with self.assertRaises(Http404):
|
||||
load_single_xblock(
|
||||
request=fake_request,
|
||||
user_id=user_id,
|
||||
course_id=unicode(course_id),
|
||||
usage_key_string=unicode(block.scope_ids.usage_id),
|
||||
course=None
|
||||
)
|
||||
# check that has_access raised the IncorrectPartitionGroupError in order to gate the block
|
||||
self.assertTrue(mock_access_error.called)
|
||||
else:
|
||||
load_single_xblock(
|
||||
request=fake_request,
|
||||
user_id=user_id,
|
||||
course_id=unicode(course_id),
|
||||
usage_key_string=unicode(block.scope_ids.usage_id),
|
||||
course=None
|
||||
)
|
||||
# check that has_access did not raise the IncorrectPartitionGroupError thereby not gating the block
|
||||
self.assertFalse(mock_access_error.called)
|
||||
# Load a block we know will pass access control checks
|
||||
vertical_xblock = load_single_xblock(
|
||||
request=fake_request,
|
||||
user_id=user_id,
|
||||
course_id=unicode(course_id),
|
||||
usage_key_string=unicode(self.blocks_dict['vertical'].scope_ids.usage_id),
|
||||
course=None
|
||||
)
|
||||
|
||||
def test_lti_audit_access(self):
|
||||
"""
|
||||
LTI stands for learning tools interoperability and is a 3rd party iframe that pulls in learning content from
|
||||
outside sources. This tests that audit users cannot see LTI components with graded content but can see the LTI
|
||||
components which do not have graded content.
|
||||
"""
|
||||
self._assert_block_is_gated(
|
||||
block=self.lti_block,
|
||||
user_id=self.audit_user.id,
|
||||
course_id=self.course.id,
|
||||
is_gated=True
|
||||
)
|
||||
self._assert_block_is_gated(
|
||||
block=self.lti_block_not_scored,
|
||||
user_id=self.audit_user.id,
|
||||
course_id=self.course.id,
|
||||
is_gated=False
|
||||
)
|
||||
runtime = vertical_xblock.runtime
|
||||
|
||||
# This method of fetching the block from the descriptor bypassess access checks
|
||||
problem_block = runtime.get_module(block)
|
||||
|
||||
# Attempt to render the block, this should return different fragments if the content is gated or not.
|
||||
frag = runtime.render(problem_block, 'student_view')
|
||||
if is_gated:
|
||||
assert 'content-paywall' in frag.content
|
||||
else:
|
||||
assert 'content-paywall' not in frag.content
|
||||
|
||||
@ddt.data(
|
||||
*PROBLEM_TYPES
|
||||
('problem', True),
|
||||
('openassessment', True),
|
||||
('drag-and-drop-v2', True),
|
||||
('done', True),
|
||||
('edx_sga', True),
|
||||
('lti_block', True),
|
||||
('ungraded_problem', False),
|
||||
('lti_block_not_scored', False),
|
||||
('audit_visible_graded_problem', False),
|
||||
)
|
||||
def test_audit_fails_access_graded_problems(self, prob_type):
|
||||
block = self.blocks_dict[prob_type]
|
||||
is_gated = True
|
||||
@ddt.unpack
|
||||
def test_access_to_problems(self, prob_type, is_gated):
|
||||
self._assert_block_is_gated(
|
||||
block=block,
|
||||
user_id=self.audit_user.id,
|
||||
block=self.blocks_dict[prob_type],
|
||||
user_id=self.users['audit'].id,
|
||||
course_id=self.course.id,
|
||||
is_gated=is_gated
|
||||
)
|
||||
self._assert_block_is_gated(
|
||||
block=self.blocks_dict[prob_type],
|
||||
user_id=self.users['verified'].id,
|
||||
course_id=self.course.id,
|
||||
is_gated=False
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
*GRADED_SCORE_WEIGHT_TEST_CASES
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
# 4. If the package is not needed in production, add it to another file such
|
||||
# as development.in or testing.in instead.
|
||||
|
||||
analytics-python==1.1.0 # Used for Segment analytics
|
||||
analytics-python==1.2.9 # Used for Segment analytics
|
||||
attrs # Reduces boilerplate code involving class attributes
|
||||
Babel==1.3 # Internationalization utilities, used for date formatting in a few places
|
||||
bleach==2.1.4 # Allowed-list-based HTML sanitizing library that escapes or strips markup and attributes; used for capa and LTI
|
||||
|
||||
@@ -40,7 +40,7 @@ git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.1.6#egg=xblock-d
|
||||
git+https://github.com/open-craft/xblock-poll@add89e14558c30f3c8dc7431e5cd6536fff6d941#egg=xblock-poll==1.5.1
|
||||
-e common/lib/xmodule
|
||||
amqp==1.4.9 # via kombu
|
||||
analytics-python==1.1.0
|
||||
analytics-python==1.2.9
|
||||
aniso8601==4.0.1 # via tincan
|
||||
anyjson==0.3.3 # via kombu
|
||||
appdirs==1.4.3 # via fs
|
||||
|
||||
@@ -44,7 +44,7 @@ git+https://github.com/open-craft/xblock-poll@add89e14558c30f3c8dc7431e5cd6536ff
|
||||
-e common/lib/xmodule
|
||||
alabaster==0.7.12 # via sphinx
|
||||
amqp==1.4.9
|
||||
analytics-python==1.1.0
|
||||
analytics-python==1.2.9
|
||||
aniso8601==4.0.1
|
||||
anyjson==0.3.3
|
||||
apipkg==1.5
|
||||
|
||||
@@ -41,7 +41,7 @@ git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.1.6#egg=xblock-d
|
||||
git+https://github.com/open-craft/xblock-poll@add89e14558c30f3c8dc7431e5cd6536fff6d941#egg=xblock-poll==1.5.1
|
||||
-e common/lib/xmodule
|
||||
amqp==1.4.9
|
||||
analytics-python==1.1.0
|
||||
analytics-python==1.2.9
|
||||
aniso8601==4.0.1
|
||||
anyjson==0.3.3
|
||||
apipkg==1.5 # via execnet
|
||||
|
||||
Reference in New Issue
Block a user