diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index 6bf764268d..a17b864e3b 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -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:
diff --git a/common/djangoapps/student/tests/test_activate_account.py b/common/djangoapps/student/tests/test_activate_account.py
index aacbbbcf21..8ea34465ea 100644
--- a/common/djangoapps/student/tests/test_activate_account.py
+++ b/common/djangoapps/student/tests/test_activate_account.py
@@ -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)
diff --git a/common/djangoapps/third_party_auth/pipeline.py b/common/djangoapps/third_party_auth/pipeline.py
index 4682cdab72..e18525e4ba 100644
--- a/common/djangoapps/third_party_auth/pipeline.py
+++ b/common/djangoapps/third_party_auth/pipeline.py
@@ -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
diff --git a/common/djangoapps/third_party_auth/tests/specs/base.py b/common/djangoapps/third_party_auth/tests/specs/base.py
index 1cf8965fbc..0a01fddc17 100644
--- a/common/djangoapps/third_party_auth/tests/specs/base.py
+++ b/common/djangoapps/third_party_auth/tests/specs/base.py
@@ -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)
diff --git a/common/djangoapps/track/segment.py b/common/djangoapps/track/segment.py
new file mode 100644
index 0000000000..b2824435e6
--- /dev/null
+++ b/common/djangoapps/track/segment.py
@@ -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)
diff --git a/common/djangoapps/track/tests/test_segment.py b/common/djangoapps/track/tests/test_segment.py
new file mode 100644
index 0000000000..8af0a9be08
--- /dev/null
+++ b/common/djangoapps/track/tests/test_segment.py
@@ -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)
diff --git a/lms/djangoapps/course_goals/tests/test_api.py b/lms/djangoapps/course_goals/tests/test_api.py
index edca8d9fa5..2d29d97067 100644
--- a/lms/djangoapps/course_goals/tests/test_api.py
+++ b/lms/djangoapps/course_goals/tests/test_api.py
@@ -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')
diff --git a/lms/djangoapps/course_goals/views.py b/lms/djangoapps/course_goals/views.py
index 8a63470825..a6dd7b1151 100644
--- a/lms/djangoapps/course_goals/views.py
+++ b/lms/djangoapps/course_goals/views.py
@@ -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)
diff --git a/lms/djangoapps/courseware/field_overrides.py b/lms/djangoapps/courseware/field_overrides.py
index 2340f01a5e..d2c3d93ffa 100644
--- a/lms/djangoapps/courseware/field_overrides.py
+++ b/lms/djangoapps/courseware/field_overrides.py
@@ -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):
"""
diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py
index 383ca77b79..fda91efde1 100644
--- a/lms/djangoapps/courseware/tests/test_views.py
+++ b/lms/djangoapps/courseware/tests/test_views.py
@@ -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()
diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py
index 0a1cf45d36..8a718822f9 100644
--- a/lms/djangoapps/courseware/views/views.py
+++ b/lms/djangoapps/courseware/views/views.py
@@ -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"])
diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py
index 5473e4c9f8..0c586d422b 100644
--- a/lms/djangoapps/shoppingcart/models.py
+++ b/lms/djangoapps/shoppingcart/models.py
@@ -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''.format(url=reverse('dashboard')),
- link_end=u'',
+ )).format(
+ link_start=HTML(u'').format(url=reverse('dashboard')),
+ link_end=HTML(u''),
)
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
diff --git a/lms/djangoapps/shoppingcart/tests/test_models.py b/lms/djangoapps/shoppingcart/tests/test_models.py
index ccc51f8051..c4a7896b06 100644
--- a/lms/djangoapps/shoppingcart/tests/test_models.py
+++ b/lms/djangoapps/shoppingcart/tests/test_models.py
@@ -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
diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py
index 2a33bb06c1..27d8e7c1d8 100644
--- a/lms/djangoapps/verify_student/views.py
+++ b/lms/djangoapps/verify_student/views.py
@@ -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
diff --git a/openedx/core/djangoapps/schedules/signals.py b/openedx/core/djangoapps/schedules/signals.py
index e1aed707a1..f6d9dedb3d 100644
--- a/openedx/core/djangoapps/schedules/signals.py
+++ b/openedx/core/djangoapps/schedules/signals.py
@@ -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,
diff --git a/openedx/core/djangoapps/schedules/tests/test_signals.py b/openedx/core/djangoapps/schedules/tests/test_signals.py
index a6281460c8..438c5ab93b 100644
--- a/openedx/core/djangoapps/schedules/tests/test_signals.py
+++ b/openedx/core/djangoapps/schedules/tests/test_signals.py
@@ -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')
diff --git a/openedx/core/djangoapps/user_api/preferences/api.py b/openedx/core/djangoapps/user_api/preferences/api.py
index fa348eb38f..2cc02ebedf 100644
--- a/openedx/core/djangoapps/user_api/preferences/api.py
+++ b/openedx/core/djangoapps/user_api/preferences/api.py
@@ -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')
- }
- }
)
diff --git a/openedx/core/djangoapps/user_authn/views/login.py b/openedx/core/djangoapps/user_authn/views/login.py
index 5d173acc77..7798c7e8bf 100644
--- a/openedx/core/djangoapps/user_authn/views/login.py
+++ b/openedx/core/djangoapps/user_authn/views/login.py
@@ -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
diff --git a/openedx/core/djangoapps/user_authn/views/register.py b/openedx/core/djangoapps/user_authn/views/register.py
index 66009b957b..802725fb3d 100644
--- a/openedx/core/djangoapps/user_authn/views/register.py
+++ b/openedx/core/djangoapps/user_authn/views/register.py
@@ -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')
- }
- }
)
diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_register.py b/openedx/core/djangoapps/user_authn/views/tests/test_register.py
index 98a0d3bfcf..65b7c27b4e 100644
--- a/openedx/core/djangoapps/user_authn/views/tests/test_register.py
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_register.py
@@ -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
diff --git a/openedx/features/content_type_gating/field_override.py b/openedx/features/content_type_gating/field_override.py
index c01d0c13aa..42d22a811a 100644
--- a/openedx/features/content_type_gating/field_override.py
+++ b/openedx/features/content_type_gating/field_override.py
@@ -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 = {}
diff --git a/openedx/features/content_type_gating/tests/test_access.py b/openedx/features/content_type_gating/tests/test_access.py
index 2ad97ffa9c..3fb1a8ea0f 100644
--- a/openedx/features/content_type_gating/tests/test_access.py
+++ b/openedx/features/content_type_gating/tests/test_access.py
@@ -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
diff --git a/requirements/edx/base.in b/requirements/edx/base.in
index 6ed70056f8..30be70acce 100644
--- a/requirements/edx/base.in
+++ b/requirements/edx/base.in
@@ -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
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index da96919c72..a20ad9c079 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -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
diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt
index afacd49761..e24d30b9c7 100644
--- a/requirements/edx/development.txt
+++ b/requirements/edx/development.txt
@@ -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
diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt
index 3b0fbe003f..8013ec0448 100644
--- a/requirements/edx/testing.txt
+++ b/requirements/edx/testing.txt
@@ -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