Emit events when users vote on forum posts.

This commit is contained in:
Sven Marnach
2015-08-24 17:31:53 +02:00
parent 3f76da0bef
commit cf5d276aea
2 changed files with 126 additions and 141 deletions

View File

@@ -25,13 +25,7 @@ from discussion_api.permissions import (
get_initializable_thread_fields,
)
from discussion_api.serializers import CommentSerializer, ThreadSerializer, get_context
from django_comment_client.base.views import (
THREAD_CREATED_EVENT_NAME,
get_comment_created_event_data,
get_comment_created_event_name,
get_thread_created_event_data,
track_forum_event,
)
from django_comment_client.base.views import track_comment_created_event, track_thread_created_event
from django_comment_common.signals import (
thread_created,
thread_edited,
@@ -566,13 +560,7 @@ def create_thread(request, thread_data):
api_thread = serializer.data
_do_extra_actions(api_thread, cc_thread, thread_data.keys(), actions_form, context)
track_forum_event(
request,
THREAD_CREATED_EVENT_NAME,
course,
cc_thread,
get_thread_created_event_data(cc_thread, followed=actions_form.cleaned_data["following"])
)
track_thread_created_event(request, course, cc_thread, actions_form.cleaned_data["following"])
return api_thread
@@ -616,13 +604,7 @@ def create_comment(request, comment_data):
api_comment = serializer.data
_do_extra_actions(api_comment, cc_comment, comment_data.keys(), actions_form, context)
track_forum_event(
request,
get_comment_created_event_name(cc_comment),
context["course"],
cc_comment,
get_comment_created_event_data(cc_comment, cc_thread["commentable_id"], followed=False)
)
track_comment_created_event(request, context["course"], cc_comment, cc_thread["commentable_id"], followed=False)
return api_comment

View File

@@ -49,10 +49,92 @@ import lms.lib.comment_client as cc
log = logging.getLogger(__name__)
TRACKING_MAX_FORUM_BODY = 2000
_EVENT_NAME_TEMPLATE = 'edx.forum.{obj_type}.{action_name}'
THREAD_CREATED_EVENT_NAME = "edx.forum.thread.created"
RESPONSE_CREATED_EVENT_NAME = 'edx.forum.response.created'
COMMENT_CREATED_EVENT_NAME = 'edx.forum.comment.created'
def track_forum_event(request, event_name, course, obj, data, id_map=None):
"""
Send out an analytics event when a forum event happens. Works for threads,
responses to threads, and comments on those responses.
"""
user = request.user
data['id'] = obj.id
commentable_id = data['commentable_id']
team = get_team(commentable_id)
if team is not None:
data.update(team_id=team.team_id)
if id_map is None:
id_map = get_cached_discussion_id_map(course, [commentable_id], user)
if commentable_id in id_map:
data['category_name'] = id_map[commentable_id]["title"]
data['category_id'] = commentable_id
data['url'] = request.META.get('HTTP_REFERER', '')
data['user_forums_roles'] = [
role.name for role in user.roles.filter(course_id=course.id)
]
data['user_course_roles'] = [
role.role for role in user.courseaccessrole_set.filter(course_id=course.id)
]
tracker.emit(event_name, data)
def track_created_event(request, event_name, course, obj, data):
if len(obj.body) > TRACKING_MAX_FORUM_BODY:
data['truncated'] = True
else:
data['truncated'] = False
data['body'] = obj.body[:TRACKING_MAX_FORUM_BODY]
track_forum_event(request, event_name, course, obj, data)
def track_thread_created_event(request, course, thread, followed):
event_name = _EVENT_NAME_TEMPLATE.format(obj_type='thread', action_name='created')
event_data = {
'commentable_id': thread.commentable_id,
'group_id': thread.get("group_id"),
'thread_type': thread.thread_type,
'title': thread.title,
'anonymous': thread.anonymous,
'anonymous_to_peers': thread.anonymous_to_peers,
'options': {'followed': followed},
# There is a stated desire for an 'origin' property that will state
# whether this thread was created via courseware or the forum.
# However, the view does not contain that data, and including it will
# likely require changes elsewhere.
}
track_created_event(request, event_name, course, thread, event_data)
def track_comment_created_event(request, course, comment, commentable_id, followed):
obj_type = 'comment' if comment.get("parent_id") else 'response'
event_name = _EVENT_NAME_TEMPLATE.format(obj_type=obj_type, action_name='created')
event_data = {
'discussion': {'id': comment.thread_id},
'commentable_id': commentable_id,
'options': {'followed': followed},
}
parent_id = comment.get('parent_id')
if parent_id:
event_data['response'] = {'id': parent_id}
track_created_event(request, event_name, course, comment, event_data)
def track_voted_event(request, course, obj, vote_value, undo_vote=False):
if isinstance(obj, cc.Thread):
obj_type = 'thread'
else:
obj_type = 'response'
event_name = _EVENT_NAME_TEMPLATE.format(obj_type=obj_type, action_name='voted')
event_data = {
'commentable_id': obj.commentable_id,
'target_username': obj.get('username'),
'undo_vote': undo_vote,
'vote_value': vote_value,
}
track_forum_event(request, event_name, course, obj, event_data)
def permitted(fn):
@@ -85,85 +167,6 @@ def ajax_content_response(request, course_key, content):
})
def track_forum_event(request, event_name, course, obj, data, id_map=None):
"""
Send out an analytics event when a forum event happens. Works for threads,
responses to threads, and comments on those responses.
"""
user = request.user
data['id'] = obj.id
commentable_id = data['commentable_id']
team = get_team(commentable_id)
if team is not None:
data.update(team_id=team.team_id)
if id_map is None:
id_map = get_cached_discussion_id_map(course, [commentable_id], user)
if commentable_id in id_map:
data['category_name'] = id_map[commentable_id]["title"]
data['category_id'] = commentable_id
if len(obj.body) > TRACKING_MAX_FORUM_BODY:
data['truncated'] = True
else:
data['truncated'] = False
data['body'] = obj.body[:TRACKING_MAX_FORUM_BODY]
data['url'] = request.META.get('HTTP_REFERER', '')
data['user_forums_roles'] = [
role.name for role in user.roles.filter(course_id=course.id)
]
data['user_course_roles'] = [
role.role for role in user.courseaccessrole_set.filter(course_id=course.id)
]
tracker.emit(event_name, data)
def get_thread_created_event_data(thread, followed):
"""
Get the event data payload for thread creation (excluding fields populated
by track_forum_event)
"""
return {
'commentable_id': thread.commentable_id,
'group_id': thread.get("group_id"),
'thread_type': thread.thread_type,
'title': thread.title,
'anonymous': thread.anonymous,
'anonymous_to_peers': thread.anonymous_to_peers,
'options': {'followed': followed},
# There is a stated desire for an 'origin' property that will state
# whether this thread was created via courseware or the forum.
# However, the view does not contain that data, and including it will
# likely require changes elsewhere.
}
def get_comment_created_event_name(comment):
"""Get the appropriate event name for creating a response/comment"""
return COMMENT_CREATED_EVENT_NAME if comment.get("parent_id") else RESPONSE_CREATED_EVENT_NAME
def get_comment_created_event_data(comment, commentable_id, followed):
"""
Get the event data payload for comment creation (excluding fields populated
by track_forum_event)
"""
event_data = {
'discussion': {'id': comment.thread_id},
'commentable_id': commentable_id,
'options': {'followed': followed},
}
parent_id = comment.get("parent_id")
if parent_id:
event_data['response'] = {'id': parent_id}
return event_data
@require_POST
@login_required
@permitted
@@ -234,12 +237,11 @@ def create_thread(request, course_id, commentable_id):
cc_user = cc.User.from_django_user(user)
cc_user.follow(thread)
event_data = get_thread_created_event_data(thread, follow)
data = thread.to_dict()
add_courseware_context([data], course, user)
track_forum_event(request, THREAD_CREATED_EVENT_NAME, course, thread, event_data)
track_thread_created_event(request, course, thread, follow)
if request.is_ajax():
return ajax_content_response(request, course_key, data)
@@ -330,9 +332,7 @@ def _create_comment(request, course_key, thread_id=None, parent_id=None):
cc_user = cc.User.from_django_user(request.user)
cc_user.follow(comment.thread)
event_name = get_comment_created_event_name(comment)
event_data = get_comment_created_event_data(comment, comment.thread.commentable_id, followed)
track_forum_event(request, event_name, course, comment, event_data)
track_comment_created_event(request, course, comment, comment.thread.commentable_id, followed)
if request.is_ajax():
return ajax_content_response(request, course_key, comment.to_dict())
@@ -456,6 +456,24 @@ def delete_comment(request, course_id, comment_id):
return JsonResponse(prepare_content(comment.to_dict(), course_key))
def _vote_or_unvote(request, course_id, obj, value='up', undo_vote=False):
"""
Vote or unvote for a thread or a response.
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course = get_course_with_access(request.user, 'load', course_key)
user = cc.User.from_django_user(request.user)
if undo_vote:
user.unvote(obj)
# TODO(smarnach): Determine the value of the vote that is undone. Currently, you can
# only cast upvotes in the user interface, so it is assumed that the vote value is 'up'.
# (People could theoretically downvote by handcrafting AJAX requests.)
else:
user.vote(obj, value)
track_voted_event(request, course, obj, value, undo_vote)
return JsonResponse(prepare_content(obj.to_dict(), course_key))
@require_POST
@login_required
@permitted
@@ -463,13 +481,10 @@ def vote_for_comment(request, course_id, comment_id, value):
"""
given a course_id and comment_id,
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = request.user
cc_user = cc.User.from_django_user(user)
comment = cc.Comment.find(comment_id)
cc_user.vote(comment, value)
comment_voted.send(sender=None, user=user, post=comment)
return JsonResponse(prepare_content(comment.to_dict(), course_key))
result = _vote_or_unvote(request, course_id, comment, value)
comment_voted.send(sender=None, user=request.user, post=comment)
return result
@require_POST
@@ -480,11 +495,7 @@ def undo_vote_for_comment(request, course_id, comment_id):
given a course id and comment id, remove vote
ajax only
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = cc.User.from_django_user(request.user)
comment = cc.Comment.find(comment_id)
user.unvote(comment)
return JsonResponse(prepare_content(comment.to_dict(), course_key))
return _vote_or_unvote(request, course_id, cc.Comment.find(comment_id), undo_vote=True)
@require_POST
@@ -495,13 +506,21 @@ def vote_for_thread(request, course_id, thread_id, value):
given a course id and thread id vote for this thread
ajax only
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = request.user
cc_user = cc.User.from_django_user(user)
thread = cc.Thread.find(thread_id)
cc_user.vote(thread, value)
thread_voted.send(sender=None, user=user, post=thread)
return JsonResponse(prepare_content(thread.to_dict(), course_key))
result = _vote_or_unvote(request, course_id, thread, value)
thread_voted.send(sender=None, user=request.user, post=thread)
return result
@require_POST
@login_required
@permitted
def undo_vote_for_thread(request, course_id, thread_id):
"""
given a course id and thread id, remove users vote for thread
ajax only
"""
return _vote_or_unvote(request, course_id, cc.Thread.find(thread_id), undo_vote=True)
@require_POST
@@ -576,22 +595,6 @@ def un_flag_abuse_for_comment(request, course_id, comment_id):
return JsonResponse(prepare_content(comment.to_dict(), course_key))
@require_POST
@login_required
@permitted
def undo_vote_for_thread(request, course_id, thread_id):
"""
given a course id and thread id, remove users vote for thread
ajax only
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = cc.User.from_django_user(request.user)
thread = cc.Thread.find(thread_id)
user.unvote(thread)
return JsonResponse(prepare_content(thread.to_dict(), course_key))
@require_POST
@login_required
@permitted