Commit fade4a10 by Kyle McCormick Committed by Kyle McCormick

Implement event for forum thread views

EDUCATOR-341
parent 4d157db6
...@@ -28,6 +28,10 @@ ERROR_MISSING_TIMESTAMP = 'Required timestamp field not found' ...@@ -28,6 +28,10 @@ ERROR_MISSING_TIMESTAMP = 'Required timestamp field not found'
ERROR_MISSING_RECEIVED_AT = 'Required receivedAt field not found' ERROR_MISSING_RECEIVED_AT = 'Required receivedAt field not found'
FORUM_THREAD_VIEWED_EVENT_LABEL = 'Forum: View Thread'
BI_SCREEN_VIEWED_EVENT_NAME = u'edx.bi.app.navigation.screen'
@require_POST @require_POST
@expect_json @expect_json
@csrf_exempt @csrf_exempt
...@@ -141,11 +145,8 @@ def track_segmentio_event(request): # pylint: disable=too-many-statements ...@@ -141,11 +145,8 @@ def track_segmentio_event(request): # pylint: disable=too-many-statements
if not segment_event_type or (segment_event_type.lower() not in allowed_types): if not segment_event_type or (segment_event_type.lower() not in allowed_types):
return return
if 'name' not in segment_properties:
raise EventValidationError(ERROR_MISSING_NAME)
# Ignore event names that are unsupported # Ignore event names that are unsupported
segment_event_name = segment_properties['name'] segment_event_name = _get_segmentio_event_name(segment_properties)
disallowed_substring_names = [ disallowed_substring_names = [
a.lower() for a in getattr(settings, 'TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES', []) a.lower() for a in getattr(settings, 'TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES', [])
] ]
...@@ -220,10 +221,49 @@ def track_segmentio_event(request): # pylint: disable=too-many-statements ...@@ -220,10 +221,49 @@ def track_segmentio_event(request): # pylint: disable=too-many-statements
context['ip'] = segment_properties.get('context', {}).get('ip', '') context['ip'] = segment_properties.get('context', {}).get('ip', '')
# For Business Intelligence events: Add label to context
if 'label' in segment_properties:
context['label'] = segment_properties['label']
# For Android-sourced Business Intelligence events: add course ID to context
if 'course_id' in segment_properties:
context['course_id'] = segment_properties['course_id']
with tracker.get_tracker().context('edx.segmentio', context): with tracker.get_tracker().context('edx.segmentio', context):
tracker.emit(segment_event_name, segment_properties.get('data', {})) tracker.emit(segment_event_name, segment_properties.get('data', {}))
def _get_segmentio_event_name(event_properties):
"""
Get the name of a SegmentIO event.
Args:
event_properties: dict
The properties of the event, which should contain the event's
name or, in the case of an old Android screen event, its screen
label.
Returns: str
The name (or effective name) of the event.
Note:
In older versions of the Android app, screen-view tracking events
did not have a name. So, in order to capture forum-thread-viewed events
from those old-versioned apps, we have to accept the event based on
its screen label. We return an event name that matches screen-view
events in the iOS app and newer versions of the Android app.
Raises:
EventValidationError if name is missing
"""
if 'name' in event_properties:
return event_properties['name']
elif event_properties.get('label') == FORUM_THREAD_VIEWED_EVENT_LABEL:
return BI_SCREEN_VIEWED_EVENT_NAME
else:
raise EventValidationError(ERROR_MISSING_NAME)
def parse_iso8601_timestamp(timestamp): def parse_iso8601_timestamp(timestamp):
"""Parse a particular type of ISO8601 formatted timestamp""" """Parse a particular type of ISO8601 formatted timestamp"""
return datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%fZ") return datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
"""
Base class for tests related to emitted events to one of the tracking 'views'
(e.g. SegmentIO).
"""
import json
from mock import sentinel
from django.test.client import RequestFactory
from django.test.utils import override_settings
from track.views import segmentio
from track.tests import EventTrackingTestCase
SEGMENTIO_TEST_SECRET = 'anything'
SEGMENTIO_TEST_ENDPOINT = '/segmentio/test/event'
SEGMENTIO_TEST_USER_ID = 10
_MOBILE_SHIM_PROCESSOR = [
{'ENGINE': 'track.shim.LegacyFieldMappingProcessor'},
{'ENGINE': 'track.shim.PrefixedEventProcessor'},
]
@override_settings(
TRACKING_SEGMENTIO_WEBHOOK_SECRET=SEGMENTIO_TEST_SECRET,
TRACKING_IGNORE_URL_PATTERNS=[SEGMENTIO_TEST_ENDPOINT],
TRACKING_SEGMENTIO_ALLOWED_TYPES=['track'],
TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES=[],
TRACKING_SEGMENTIO_SOURCE_MAP={'test-app': 'mobile'},
EVENT_TRACKING_PROCESSORS=_MOBILE_SHIM_PROCESSOR,
)
class SegmentIOTrackingTestCaseBase(EventTrackingTestCase):
"""
Base class for tests that test the processing of Segment events.
"""
def setUp(self):
super(SegmentIOTrackingTestCaseBase, self).setUp()
self.maxDiff = None # pylint: disable=invalid-name
self.request_factory = RequestFactory()
def create_request(self, key=None, **kwargs):
"""Create a fake request that emulates a request from the Segment servers to ours"""
if key is None:
key = SEGMENTIO_TEST_SECRET
request = self.request_factory.post(SEGMENTIO_TEST_ENDPOINT + "?key=" + key, **kwargs)
if 'data' in kwargs:
request.json = json.loads(kwargs['data'])
return request
def post_segmentio_event(self, **kwargs):
"""Post a fake Segment event to the view that processes it"""
request = self.create_request(
data=self.create_segmentio_event_json(**kwargs),
content_type='application/json'
)
segmentio.track_segmentio_event(request)
def create_segmentio_event(self, **kwargs):
"""Populate a fake Segment event with data of interest"""
action = kwargs.get('action', 'Track')
sample_event = {
"userId": kwargs.get('user_id', SEGMENTIO_TEST_USER_ID),
"event": "Did something",
"properties": {
'name': kwargs.get('name', str(sentinel.name)),
'data': kwargs.get('data', {}),
'context': {
'course_id': kwargs.get('course_id') or '',
'app_name': 'edx.mobile.android',
}
},
"channel": 'server',
"context": {
"library": {
"name": kwargs.get('library_name', 'test-app'),
"version": "unknown"
},
"app": {
"version": "1.0.1",
},
'userAgent': str(sentinel.user_agent),
},
"receivedAt": "2014-08-27T16:33:39.100Z",
"timestamp": "2014-08-27T16:33:39.215Z",
"type": action.lower(),
"projectId": "u0j33yjkr8",
"messageId": "qy52hwp4",
"version": 2,
"integrations": {},
"options": {
"library": "unknown",
"providers": {}
},
"action": action
}
if 'context' in kwargs:
sample_event['properties']['context'].update(kwargs['context'])
if 'label' in kwargs:
sample_event['properties']['label'] = kwargs['label']
if kwargs.get('exclude_name') is True:
del sample_event['properties']['name']
return sample_event
def create_segmentio_event_json(self, **kwargs):
"""Return a json string containing a fake Segment event"""
return json.dumps(self.create_segmentio_event(**kwargs))
...@@ -96,12 +96,29 @@ class EventTestMixin(object): ...@@ -96,12 +96,29 @@ class EventTestMixin(object):
kwargs kwargs
) )
def assert_event_emission_count(self, event_name, expected_count):
"""
Verify that the event with the given name was emitted
a specific number of times.
"""
actual_count = 0
for call_args in self.mock_tracker.emit.call_args_list:
if call_args[0][0] == event_name:
actual_count += 1
self.assertEqual(actual_count, expected_count)
def reset_tracker(self): def reset_tracker(self):
""" """
Reset the mock tracker in order to forget about old events. Reset the mock tracker in order to forget about old events.
""" """
self.mock_tracker.reset_mock() self.mock_tracker.reset_mock()
def get_latest_call_args(self):
"""
Return the arguments of the latest call to emit.
"""
return self.mock_tracker.emit.call_args[0]
class PatchMediaTypeMixin(object): class PatchMediaTypeMixin(object):
""" """
......
...@@ -28,6 +28,7 @@ import lms.lib.comment_client as cc ...@@ -28,6 +28,7 @@ import lms.lib.comment_client as cc
from courseware.access import has_access from courseware.access import has_access
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from courseware.views.views import CourseTabView from courseware.views.views import CourseTabView
from django_comment_client.base.views import track_thread_viewed_event
from django_comment_client.constants import TYPE_ENTRY from django_comment_client.constants import TYPE_ENTRY
from django_comment_client.permissions import get_team, has_permission from django_comment_client.permissions import get_team, has_permission
from django_comment_client.utils import ( from django_comment_client.utils import (
...@@ -291,10 +292,13 @@ def single_thread(request, course_key, discussion_id, thread_id): ...@@ -291,10 +292,13 @@ def single_thread(request, course_key, discussion_id, thread_id):
cc_user = cc.User.from_django_user(request.user) cc_user = cc.User.from_django_user(request.user)
user_info = cc_user.to_dict() user_info = cc_user.to_dict()
is_staff = has_permission(request.user, 'openclose_thread', course.id) is_staff = has_permission(request.user, 'openclose_thread', course.id)
thread = _load_thread_for_viewing(
thread = _find_thread(request, course, discussion_id=discussion_id, thread_id=thread_id) request,
if not thread: course,
raise Http404 discussion_id=discussion_id,
thread_id=thread_id,
raise_event=True,
)
with newrelic_function_trace("get_annotated_content_infos"): with newrelic_function_trace("get_annotated_content_infos"):
annotated_content_info = utils.get_annotated_content_infos( annotated_content_info = utils.get_annotated_content_infos(
...@@ -358,6 +362,34 @@ def _find_thread(request, course, discussion_id, thread_id): ...@@ -358,6 +362,34 @@ def _find_thread(request, course, discussion_id, thread_id):
return thread return thread
def _load_thread_for_viewing(request, course, discussion_id, thread_id, raise_event):
"""
Loads the discussion thread with the specified ID and fires an
edx.forum.thread.viewed event.
Args:
request: The Django request.
course_id: The ID of the owning course.
discussion_id: The ID of the owning discussion.
thread_id: The ID of the thread.
raise_event: Whether an edx.forum.thread.viewed tracking event should
be raised
Returns:
The thread in question if the user can see it.
Raises:
Http404 if the thread does not exist or the user cannot
see it.
"""
thread = _find_thread(request, course, discussion_id=discussion_id, thread_id=thread_id)
if not thread:
raise Http404
if raise_event:
track_thread_viewed_event(request, course, thread)
return thread
def _create_base_discussion_view_context(request, course_key): def _create_base_discussion_view_context(request, course_key):
""" """
Returns the default template context for rendering any discussion view. Returns the default template context for rendering any discussion view.
...@@ -393,20 +425,20 @@ def _get_discussion_default_topic_id(course): ...@@ -393,20 +425,20 @@ def _get_discussion_default_topic_id(course):
return entry['id'] return entry['id']
def _create_discussion_board_context(request, course_key, discussion_id=None, thread_id=None): def _create_discussion_board_context(request, base_context, thread=None):
""" """
Returns the template context for rendering the discussion board. Returns the template context for rendering the discussion board.
""" """
context = _create_base_discussion_view_context(request, course_key) context = base_context.copy()
course = context['course'] course = context['course']
course_key = course.id
thread_id = thread.id if thread else None
discussion_id = thread.commentable_id if thread else None
course_settings = context['course_settings'] course_settings = context['course_settings']
user = context['user'] user = context['user']
cc_user = cc.User.from_django_user(user) cc_user = cc.User.from_django_user(user)
user_info = context['user_info'] user_info = context['user_info']
if thread_id: if thread:
thread = _find_thread(request, course, discussion_id=discussion_id, thread_id=thread_id)
if not thread:
raise Http404
# Since we're in page render mode, and the discussions UI will request the thread list itself, # Since we're in page render mode, and the discussions UI will request the thread list itself,
# we need only return the thread information for this one. # we need only return the thread information for this one.
...@@ -637,12 +669,25 @@ class DiscussionBoardFragmentView(EdxFragmentView): ...@@ -637,12 +669,25 @@ class DiscussionBoardFragmentView(EdxFragmentView):
""" """
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
try: try:
context = _create_discussion_board_context( base_context = _create_base_discussion_view_context(request, course_key)
request, # Note:
course_key, # After the thread is rendered in this fragment, an AJAX
discussion_id=discussion_id, # request is made and the thread is completely loaded again
thread_id=thread_id, # (yes, this is something to fix). Because of this, we pass in
# raise_event=False to _load_thread_for_viewing avoid duplicate
# tracking events.
thread = (
_load_thread_for_viewing(
request,
base_context['course'],
discussion_id=discussion_id,
thread_id=thread_id,
raise_event=False,
)
if thread_id
else None
) )
context = _create_discussion_board_context(request, base_context, thread=thread)
html = render_to_string('discussion/discussion_board_fragment.html', context) html = render_to_string('discussion/discussion_board_fragment.html', context)
inline_js = render_to_string('discussion/discussion_board_js.template', context) inline_js = render_to_string('discussion/discussion_board_js.template', context)
......
# This import registers the ForumThreadViewedEventTransformer
import event_transformers # pylint: disable=unused-import
"""
Transformers for Discussion-related events.
"""
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse, NoReverseMatch
from eventtracking.processors.exceptions import EventEmissionExit
from opaque_keys import InvalidKeyError
from opaque_keys.edx.locator import CourseLocator
from django_comment_client.base.views import add_truncated_title_to_event_data
from django_comment_client.permissions import get_team
from django_comment_client.utils import get_cached_discussion_id_map_by_course_id
from track.transformers import EventTransformer, EventTransformerRegistry
from track.views.segmentio import (
BI_SCREEN_VIEWED_EVENT_NAME,
FORUM_THREAD_VIEWED_EVENT_LABEL
)
def _get_string(dictionary, key, del_if_bad=True):
"""
Get a string from a dictionary by key.
If the key is not in the dictionary or does not refer to a string:
- Return None
- Optionally delete the key (del_if_bad)
"""
if key in dictionary:
value = dictionary[key]
if isinstance(value, basestring):
return value
else:
if del_if_bad:
del dictionary[key]
return None
else:
return None
@EventTransformerRegistry.register
class ForumThreadViewedEventTransformer(EventTransformer):
"""
Transformer for forum-thread-viewed mobile navigation events.
"""
match_key = BI_SCREEN_VIEWED_EVENT_NAME
def process_event(self):
"""
Process incoming mobile navigation events.
For forum-thread-viewed events, change their names to
edx.forum.thread.viewed and manipulate their data to conform with
edx.forum.thread.viewed event design.
Throw out other events.
"""
# Get event context dict
# Throw out event if context nonexistent or wrong type
context = self.get('context')
if not isinstance(context, dict):
raise EventEmissionExit()
# Throw out event if it's not a forum thread view
if _get_string(context, 'label', del_if_bad=False) != FORUM_THREAD_VIEWED_EVENT_LABEL:
raise EventEmissionExit()
# Change name and event type
self['name'] = 'edx.forum.thread.viewed'
self['event_type'] = self['name']
# If no event data, set it to an empty dict
if 'event' not in self:
self['event'] = {}
self.event = {}
# Throw out the context dict within the event data
# (different from the context dict extracted above)
if 'context' in self.event:
del self.event['context']
# Parse out course key
course_id_string = _get_string(context, 'course_id') if context else None
course_id = None
if course_id_string:
try:
course_id = CourseLocator.from_string(course_id_string)
except InvalidKeyError:
pass
# Change 'thread_id' field to 'id'
thread_id = _get_string(self.event, 'thread_id')
if thread_id:
del self.event['thread_id']
self.event['id'] = thread_id
# Change 'topic_id' to 'commentable_id'
commentable_id = _get_string(self.event, 'topic_id')
if commentable_id:
del self.event['topic_id']
self.event['commentable_id'] = commentable_id
# Change 'action' to 'title' and truncate
title = _get_string(self.event, 'action')
if title is not None:
del self.event['action']
add_truncated_title_to_event_data(self.event, title)
# Change 'author' to 'target_username'
author = _get_string(self.event, 'author')
if author is not None:
del self.event['author']
self.event['target_username'] = author
# Load user
username = _get_string(self, 'username')
user = None
if username:
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
pass
# If in a category, add category name and ID
if course_id and commentable_id and user:
id_map = get_cached_discussion_id_map_by_course_id(course_id, [commentable_id], user)
if commentable_id in id_map:
self.event['category_name'] = id_map[commentable_id]['title']
self.event['category_id'] = commentable_id
# Add thread URL
if course_id and commentable_id and thread_id:
url_kwargs = {
'course_id': course_id_string,
'discussion_id': commentable_id,
'thread_id': thread_id
}
try:
self.event['url'] = reverse('single_thread', kwargs=url_kwargs)
except NoReverseMatch:
pass
# Add user's forum and course roles
if course_id and user:
self.event['user_forums_roles'] = [
role.name for role in user.roles.filter(course_id=course_id)
]
self.event['user_course_roles'] = [
role.role for role in user.courseaccessrole_set.filter(course_id=course_id)
]
# Add team ID
if commentable_id:
team = get_team(commentable_id)
if team:
self.event['team_id'] = team.team_id
...@@ -45,7 +45,7 @@ from django_comment_common.signals import ( ...@@ -45,7 +45,7 @@ from django_comment_common.signals import (
thread_voted thread_voted
) )
from django_comment_common.utils import ThreadContext from django_comment_common.utils import ThreadContext
from eventtracking import tracker import eventtracking
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
from util.file import store_uploaded_file from util.file import store_uploaded_file
...@@ -82,7 +82,7 @@ def track_forum_event(request, event_name, course, obj, data, id_map=None): ...@@ -82,7 +82,7 @@ def track_forum_event(request, event_name, course, obj, data, id_map=None):
role.role for role in user.courseaccessrole_set.filter(course_id=course.id) role.role for role in user.courseaccessrole_set.filter(course_id=course.id)
] ]
tracker.emit(event_name, data) eventtracking.tracker.emit(event_name, data)
def track_created_event(request, event_name, course, obj, data): def track_created_event(request, event_name, course, obj, data):
...@@ -97,7 +97,7 @@ def track_created_event(request, event_name, course, obj, data): ...@@ -97,7 +97,7 @@ def track_created_event(request, event_name, course, obj, data):
track_forum_event(request, event_name, course, obj, data) track_forum_event(request, event_name, course, obj, data)
def add_truncated_title_to_event_data(event_data, full_title): def add_truncated_title_to_event_data(event_data, full_title): # pylint: disable=invalid-name
event_data['title_truncated'] = (len(full_title) > TRACKING_MAX_FORUM_TITLE) event_data['title_truncated'] = (len(full_title) > TRACKING_MAX_FORUM_TITLE)
event_data['title'] = full_title[:TRACKING_MAX_FORUM_TITLE] event_data['title'] = full_title[:TRACKING_MAX_FORUM_TITLE]
...@@ -158,6 +158,19 @@ def track_voted_event(request, course, obj, vote_value, undo_vote=False): ...@@ -158,6 +158,19 @@ def track_voted_event(request, course, obj, vote_value, undo_vote=False):
track_forum_event(request, event_name, course, obj, event_data) track_forum_event(request, event_name, course, obj, event_data)
def track_thread_viewed_event(request, course, thread):
"""
Send analytics event for a viewed thread.
"""
event_name = _EVENT_NAME_TEMPLATE.format(obj_type='thread', action_name='viewed')
event_data = {}
event_data['commentable_id'] = thread.commentable_id
if hasattr(thread, 'username'):
event_data['target_username'] = thread.username
add_truncated_title_to_event_data(event_data, thread.title)
track_forum_event(request, event_name, course, thread, event_data)
def permitted(func): def permitted(func):
""" """
View decorator to verify the user is authorized to access this endpoint. View decorator to verify the user is authorized to access this endpoint.
......
...@@ -130,11 +130,19 @@ def get_accessible_discussion_xblocks(course, user, include_all=False): # pylin ...@@ -130,11 +130,19 @@ def get_accessible_discussion_xblocks(course, user, include_all=False): # pylin
Return a list of all valid discussion xblocks in this course that Return a list of all valid discussion xblocks in this course that
are accessible to the given user. are accessible to the given user.
""" """
all_xblocks = modulestore().get_items(course.id, qualifiers={'category': 'discussion'}, include_orphans=False) return get_accessible_discussion_xblocks_by_course_id(course.id, user, include_all=include_all)
def get_accessible_discussion_xblocks_by_course_id(course_id, user, include_all=False): # pylint: disable=invalid-name
"""
Return a list of all valid discussion xblocks in this course that
are accessible to the given user.
"""
all_xblocks = modulestore().get_items(course_id, qualifiers={'category': 'discussion'}, include_orphans=False)
return [ return [
xblock for xblock in all_xblocks xblock for xblock in all_xblocks
if has_required_keys(xblock) and (include_all or has_access(user, 'load', xblock, course.id)) if has_required_keys(xblock) and (include_all or has_access(user, 'load', xblock, course_id))
] ]
...@@ -178,19 +186,27 @@ def get_cached_discussion_id_map(course, discussion_ids, user): ...@@ -178,19 +186,27 @@ def get_cached_discussion_id_map(course, discussion_ids, user):
Returns a dict mapping discussion_ids to respective discussion xblock metadata if it is cached and visible to the Returns a dict mapping discussion_ids to respective discussion xblock metadata if it is cached and visible to the
user. If not, returns the result of get_discussion_id_map user. If not, returns the result of get_discussion_id_map
""" """
return get_cached_discussion_id_map_by_course_id(course.id, discussion_ids, user)
def get_cached_discussion_id_map_by_course_id(course_id, discussion_ids, user): # pylint: disable=invalid-name
"""
Returns a dict mapping discussion_ids to respective discussion xblock metadata if it is cached and visible to the
user. If not, returns the result of get_discussion_id_map
"""
try: try:
entries = [] entries = []
for discussion_id in discussion_ids: for discussion_id in discussion_ids:
key = get_cached_discussion_key(course.id, discussion_id) key = get_cached_discussion_key(course_id, discussion_id)
if not key: if not key:
continue continue
xblock = modulestore().get_item(key) xblock = modulestore().get_item(key)
if not (has_required_keys(xblock) and has_access(user, 'load', xblock, course.id)): if not (has_required_keys(xblock) and has_access(user, 'load', xblock, course_id)):
continue continue
entries.append(get_discussion_id_map_entry(xblock)) entries.append(get_discussion_id_map_entry(xblock))
return dict(entries) return dict(entries)
except DiscussionIdMapIsNotCached: except DiscussionIdMapIsNotCached:
return get_discussion_id_map(course, user) return get_discussion_id_map_by_course_id(course_id, user)
def get_discussion_id_map(course, user): def get_discussion_id_map(course, user):
...@@ -198,7 +214,16 @@ def get_discussion_id_map(course, user): ...@@ -198,7 +214,16 @@ def get_discussion_id_map(course, user):
Transform the list of this course's discussion xblocks (visible to a given user) into a dictionary of metadata keyed Transform the list of this course's discussion xblocks (visible to a given user) into a dictionary of metadata keyed
by discussion_id. by discussion_id.
""" """
return dict(map(get_discussion_id_map_entry, get_accessible_discussion_xblocks(course, user))) return get_discussion_id_map_by_course_id(course.id, user)
def get_discussion_id_map_by_course_id(course_id, user): # pylint: disable=invalid-name
"""
Transform the list of this course's discussion xblocks (visible to a given user) into a dictionary of metadata keyed
by discussion_id.
"""
xblocks = get_accessible_discussion_xblocks_by_course_id(course_id, user)
return dict(map(get_discussion_id_map_entry, xblocks))
def _filter_unstarted_categories(category_map, course): def _filter_unstarted_categories(category_map, course):
......
...@@ -739,7 +739,7 @@ if FEATURES.get('ENABLE_SQL_TRACKING_LOGS'): ...@@ -739,7 +739,7 @@ if FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
TRACKING_SEGMENTIO_WEBHOOK_SECRET = None TRACKING_SEGMENTIO_WEBHOOK_SECRET = None
TRACKING_SEGMENTIO_ALLOWED_TYPES = ['track'] TRACKING_SEGMENTIO_ALLOWED_TYPES = ['track']
TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES = ['.bi.'] TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES = []
TRACKING_SEGMENTIO_SOURCE_MAP = { TRACKING_SEGMENTIO_SOURCE_MAP = {
'analytics-android': 'mobile', 'analytics-android': 'mobile',
'analytics-ios': 'mobile', 'analytics-ios': 'mobile',
......
...@@ -5,15 +5,7 @@ import settings ...@@ -5,15 +5,7 @@ import settings
import models import models
from eventtracking import tracker from eventtracking import tracker
from .utils import ( import utils
CommentClientPaginatedResult,
CommentClientRequestError,
extract,
merge_dict,
perform_request,
strip_blank,
strip_none
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -61,15 +53,15 @@ class Thread(models.Model): ...@@ -61,15 +53,15 @@ class Thread(models.Model):
default_params = {'page': 1, default_params = {'page': 1,
'per_page': 20, 'per_page': 20,
'course_id': query_params['course_id']} 'course_id': query_params['course_id']}
params = merge_dict(default_params, strip_blank(strip_none(query_params))) params = utils.merge_dict(default_params, utils.strip_blank(utils.strip_none(query_params)))
if query_params.get('text'): if query_params.get('text'):
url = cls.url(action='search') url = cls.url(action='search')
else: else:
url = cls.url(action='get_all', params=extract(params, 'commentable_id')) url = cls.url(action='get_all', params=utils.extract(params, 'commentable_id'))
if params.get('commentable_id'): if params.get('commentable_id'):
del params['commentable_id'] del params['commentable_id']
response = perform_request( response = utils.perform_request(
'get', 'get',
url, url,
params, params,
...@@ -107,7 +99,7 @@ class Thread(models.Model): ...@@ -107,7 +99,7 @@ class Thread(models.Model):
) )
) )
return CommentClientPaginatedResult( return utils.CommentClientPaginatedResult(
collection=response.get('collection', []), collection=response.get('collection', []),
page=response.get('page', 1), page=response.get('page', 1),
num_pages=response.get('num_pages', 1), num_pages=response.get('num_pages', 1),
...@@ -149,9 +141,9 @@ class Thread(models.Model): ...@@ -149,9 +141,9 @@ class Thread(models.Model):
'resp_skip': kwargs.get('response_skip'), 'resp_skip': kwargs.get('response_skip'),
'resp_limit': kwargs.get('response_limit'), 'resp_limit': kwargs.get('response_limit'),
} }
request_params = strip_none(request_params) request_params = utils.strip_none(request_params)
response = perform_request( response = utils.perform_request(
'get', 'get',
url, url,
request_params, request_params,
...@@ -166,9 +158,9 @@ class Thread(models.Model): ...@@ -166,9 +158,9 @@ class Thread(models.Model):
elif voteable.type == 'comment': elif voteable.type == 'comment':
url = _url_for_flag_comment(voteable.id) url = _url_for_flag_comment(voteable.id)
else: else:
raise CommentClientRequestError("Can only flag/unflag threads or comments") raise utils.CommentClientRequestError("Can only flag/unflag threads or comments")
params = {'user_id': user.id} params = {'user_id': user.id}
response = perform_request( response = utils.perform_request(
'put', 'put',
url, url,
params, params,
...@@ -183,13 +175,13 @@ class Thread(models.Model): ...@@ -183,13 +175,13 @@ class Thread(models.Model):
elif voteable.type == 'comment': elif voteable.type == 'comment':
url = _url_for_unflag_comment(voteable.id) url = _url_for_unflag_comment(voteable.id)
else: else:
raise CommentClientRequestError("Can only flag/unflag for threads or comments") raise utils.CommentClientRequestError("Can only flag/unflag for threads or comments")
params = {'user_id': user.id} params = {'user_id': user.id}
#if you're an admin, when you unflag, remove ALL flags #if you're an admin, when you unflag, remove ALL flags
if removeAll: if removeAll:
params['all'] = True params['all'] = True
response = perform_request( response = utils.perform_request(
'put', 'put',
url, url,
params, params,
...@@ -201,7 +193,7 @@ class Thread(models.Model): ...@@ -201,7 +193,7 @@ class Thread(models.Model):
def pin(self, user, thread_id): def pin(self, user, thread_id):
url = _url_for_pin_thread(thread_id) url = _url_for_pin_thread(thread_id)
params = {'user_id': user.id} params = {'user_id': user.id}
response = perform_request( response = utils.perform_request(
'put', 'put',
url, url,
params, params,
...@@ -213,7 +205,7 @@ class Thread(models.Model): ...@@ -213,7 +205,7 @@ class Thread(models.Model):
def un_pin(self, user, thread_id): def un_pin(self, user, thread_id):
url = _url_for_un_pin_thread(thread_id) url = _url_for_un_pin_thread(thread_id)
params = {'user_id': user.id} params = {'user_id': user.id}
response = perform_request( response = utils.perform_request(
'put', 'put',
url, url,
params, params,
......
...@@ -3,7 +3,7 @@ import settings ...@@ -3,7 +3,7 @@ import settings
import models import models
from .utils import CommentClientPaginatedResult, CommentClientRequestError, merge_dict, perform_request import utils
class User(models.Model): class User(models.Model):
...@@ -36,7 +36,7 @@ class User(models.Model): ...@@ -36,7 +36,7 @@ class User(models.Model):
Calls cs_comments_service to mark thread as read for the user Calls cs_comments_service to mark thread as read for the user
""" """
params = {'source_type': source.type, 'source_id': source.id} params = {'source_type': source.type, 'source_id': source.id}
perform_request( utils.perform_request(
'post', 'post',
_url_for_read(self.id), _url_for_read(self.id),
params, params,
...@@ -46,7 +46,7 @@ class User(models.Model): ...@@ -46,7 +46,7 @@ class User(models.Model):
def follow(self, source): def follow(self, source):
params = {'source_type': source.type, 'source_id': source.id} params = {'source_type': source.type, 'source_id': source.id}
response = perform_request( response = utils.perform_request(
'post', 'post',
_url_for_subscription(self.id), _url_for_subscription(self.id),
params, params,
...@@ -56,7 +56,7 @@ class User(models.Model): ...@@ -56,7 +56,7 @@ class User(models.Model):
def unfollow(self, source): def unfollow(self, source):
params = {'source_type': source.type, 'source_id': source.id} params = {'source_type': source.type, 'source_id': source.id}
response = perform_request( response = utils.perform_request(
'delete', 'delete',
_url_for_subscription(self.id), _url_for_subscription(self.id),
params, params,
...@@ -70,9 +70,9 @@ class User(models.Model): ...@@ -70,9 +70,9 @@ class User(models.Model):
elif voteable.type == 'comment': elif voteable.type == 'comment':
url = _url_for_vote_comment(voteable.id) url = _url_for_vote_comment(voteable.id)
else: else:
raise CommentClientRequestError("Can only vote / unvote for threads or comments") raise utils.CommentClientRequestError("Can only vote / unvote for threads or comments")
params = {'user_id': self.id, 'value': value} params = {'user_id': self.id, 'value': value}
response = perform_request( response = utils.perform_request(
'put', 'put',
url, url,
params, params,
...@@ -87,9 +87,9 @@ class User(models.Model): ...@@ -87,9 +87,9 @@ class User(models.Model):
elif voteable.type == 'comment': elif voteable.type == 'comment':
url = _url_for_vote_comment(voteable.id) url = _url_for_vote_comment(voteable.id)
else: else:
raise CommentClientRequestError("Can only vote / unvote for threads or comments") raise utils.CommentClientRequestError("Can only vote / unvote for threads or comments")
params = {'user_id': self.id} params = {'user_id': self.id}
response = perform_request( response = utils.perform_request(
'delete', 'delete',
url, url,
params, params,
...@@ -100,11 +100,11 @@ class User(models.Model): ...@@ -100,11 +100,11 @@ class User(models.Model):
def active_threads(self, query_params={}): def active_threads(self, query_params={}):
if not self.course_id: if not self.course_id:
raise CommentClientRequestError("Must provide course_id when retrieving active threads for the user") raise utils.CommentClientRequestError("Must provide course_id when retrieving active threads for the user")
url = _url_for_user_active_threads(self.id) url = _url_for_user_active_threads(self.id)
params = {'course_id': self.course_id.to_deprecated_string()} params = {'course_id': self.course_id.to_deprecated_string()}
params = merge_dict(params, query_params) params = utils.merge_dict(params, query_params)
response = perform_request( response = utils.perform_request(
'get', 'get',
url, url,
params, params,
...@@ -116,11 +116,11 @@ class User(models.Model): ...@@ -116,11 +116,11 @@ class User(models.Model):
def subscribed_threads(self, query_params={}): def subscribed_threads(self, query_params={}):
if not self.course_id: if not self.course_id:
raise CommentClientRequestError("Must provide course_id when retrieving subscribed threads for the user") raise utils.CommentClientRequestError("Must provide course_id when retrieving subscribed threads for the user")
url = _url_for_user_subscribed_threads(self.id) url = _url_for_user_subscribed_threads(self.id)
params = {'course_id': self.course_id.to_deprecated_string()} params = {'course_id': self.course_id.to_deprecated_string()}
params = merge_dict(params, query_params) params = utils.merge_dict(params, query_params)
response = perform_request( response = utils.perform_request(
'get', 'get',
url, url,
params, params,
...@@ -128,7 +128,7 @@ class User(models.Model): ...@@ -128,7 +128,7 @@ class User(models.Model):
metric_tags=self._metric_tags, metric_tags=self._metric_tags,
paged_results=True paged_results=True
) )
return CommentClientPaginatedResult( return utils.CommentClientPaginatedResult(
collection=response.get('collection', []), collection=response.get('collection', []),
page=response.get('page', 1), page=response.get('page', 1),
num_pages=response.get('num_pages', 1), num_pages=response.get('num_pages', 1),
...@@ -144,19 +144,19 @@ class User(models.Model): ...@@ -144,19 +144,19 @@ class User(models.Model):
if self.attributes.get('group_id'): if self.attributes.get('group_id'):
retrieve_params['group_id'] = self.group_id retrieve_params['group_id'] = self.group_id
try: try:
response = perform_request( response = utils.perform_request(
'get', 'get',
url, url,
retrieve_params, retrieve_params,
metric_action='model.retrieve', metric_action='model.retrieve',
metric_tags=self._metric_tags, metric_tags=self._metric_tags,
) )
except CommentClientRequestError as e: except utils.CommentClientRequestError as e:
if e.status_code == 404: if e.status_code == 404:
# attempt to gracefully recover from a previous failure # attempt to gracefully recover from a previous failure
# to sync this user to the comments service. # to sync this user to the comments service.
self.save() self.save()
response = perform_request( response = utils.perform_request(
'get', 'get',
url, url,
retrieve_params, retrieve_params,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment