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) ] eventtracking.tracker.emit(event_name, data)
def is_commentable_divided(course_key, commentable_id, course_discussion_settings=None): """ Args: course_key: CourseKey commentable_id: string course_discussion_settings: CourseDiscussionSettings model instance (optional). If not supplied, it will be retrieved via the course_key. Returns: Bool: is this commentable divided, meaning that learners are divided into groups (either Cohorts or Enrollment Tracks) and only see posts within their group? Raises: Http404 if the course doesn't exist. """ if not course_discussion_settings: course_discussion_settings = get_course_discussion_settings(course_key) course = courses.get_course_by_id(course_key) if not course_discussion_division_enabled( course_discussion_settings) or get_team(commentable_id): # this is the easy case :) ans = False elif (commentable_id in course.top_level_discussion_topic_ids or course_discussion_settings.always_divide_inline_discussions is False): # top level discussions have to be manually configured as divided # (default is not). # Same thing for inline discussions if the default is explicitly set to False in settings ans = commentable_id in course_discussion_settings.divided_discussions else: # inline discussions are divided by default ans = True log.debug(u"is_commentable_divided(%s, %s) = {%s}", course_key, commentable_id, ans) return ans
def is_commentable_divided(course_key, commentable_id, course_discussion_settings=None): """ Args: course_key: CourseKey commentable_id: string course_discussion_settings: CourseDiscussionSettings model instance (optional). If not supplied, it will be retrieved via the course_key. Returns: Bool: is this commentable divided, meaning that learners are divided into groups (either Cohorts or Enrollment Tracks) and only see posts within their group? Raises: Http404 if the course doesn't exist. """ if not course_discussion_settings: course_discussion_settings = get_course_discussion_settings(course_key) course = courses.get_course_by_id(course_key) if not course_discussion_division_enabled(course_discussion_settings) or get_team(commentable_id): # this is the easy case :) ans = False elif ( commentable_id in course.top_level_discussion_topic_ids or course_discussion_settings.always_divide_inline_discussions is False ): # top level discussions have to be manually configured as divided # (default is not). # Same thing for inline discussions if the default is explicitly set to False in settings ans = commentable_id in course_discussion_settings.divided_discussions else: # inline discussions are divided by default ans = True log.debug(u"is_commentable_divided(%s, %s) = {%s}", course_key, commentable_id, ans) return ans
def create_thread(request, course_id, commentable_id): """ Given a course and commentable ID, create the thread """ log.debug("Creating new thread in %r, id %r", course_id, commentable_id) course_key = CourseKey.from_string(course_id) course = get_course_with_access(request.user, 'load', course_key) post = request.POST user = request.user if course.allow_anonymous: anonymous = post.get('anonymous', 'false').lower() == 'true' else: anonymous = False if course.allow_anonymous_to_peers: anonymous_to_peers = post.get('anonymous_to_peers', 'false').lower() == 'true' else: anonymous_to_peers = False if 'title' not in post or not post['title'].strip(): return JsonError(_("Title can't be empty")) if 'body' not in post or not post['body'].strip(): return JsonError(_("Body can't be empty")) params = { 'anonymous': anonymous, 'anonymous_to_peers': anonymous_to_peers, 'commentable_id': commentable_id, 'course_id': str(course_key), 'user_id': user.id, 'thread_type': post["thread_type"], 'body': post["body"], 'title': post["title"], } # Check for whether this commentable belongs to a team, and add the right context if get_team(commentable_id) is not None: params['context'] = ThreadContext.STANDALONE else: params['context'] = ThreadContext.COURSE thread = cc.Thread(**params) # Divide the thread if required try: group_id = get_group_id_for_comments_service(request, course_key, commentable_id) except ValueError: return HttpResponseServerError("Invalid group id for commentable") if group_id is not None: thread.group_id = group_id thread.save() thread_created.send(sender=None, user=user, post=thread) # patch for backward compatibility to comments service if 'pinned' not in thread.attributes: thread['pinned'] = False follow = post.get('auto_subscribe', 'false').lower() == 'true' if follow: cc_user = cc.User.from_django_user(user) cc_user.follow(thread) thread_followed.send(sender=None, user=user, post=thread) data = thread.to_dict() add_courseware_context([data], course, user) track_thread_created_event(request, course, thread, follow) if request.is_ajax(): return ajax_content_response(request, course_key, data) else: return JsonResponse(prepare_content(data, course_key))
def get_threads(request, course, user_info, discussion_id=None, per_page=THREADS_PER_PAGE): """ This may raise an appropriate subclass of cc.utils.CommentClientError if something goes wrong, or ValueError if the group_id is invalid. Arguments: request (WSGIRequest): The user request. course (CourseDescriptorWithMixins): The course object. user_info (dict): The comment client User object as a dict. discussion_id (unicode): Optional discussion id/commentable id for context. per_page (int): Optional number of threads per page. Returns: (tuple of list, dict): A tuple of the list of threads and a dict of the query parameters used for the search. """ default_query_params = { 'page': 1, 'per_page': per_page, 'sort_key': 'activity', 'text': '', 'course_id': six.text_type(course.id), 'user_id': request.user.id, 'context': ThreadContext.COURSE, 'group_id': get_group_id_for_comments_service( request, course.id, discussion_id), # may raise ValueError } # If provided with a discussion id, filter by discussion id in the # comments_service. if discussion_id is not None: default_query_params['commentable_id'] = discussion_id # Use the discussion id/commentable id to determine the context we are going to pass through to the backend. if get_team(discussion_id) is not None: default_query_params['context'] = ThreadContext.STANDALONE if not request.GET.get('sort_key'): # If the user did not select a sort key, use their last used sort key default_query_params['sort_key'] = user_info.get( 'default_sort_key') or default_query_params['sort_key'] elif request.GET.get('sort_key') != user_info.get('default_sort_key'): # If the user clicked a sort key, update their default sort key cc_user = cc.User.from_django_user(request.user) cc_user.default_sort_key = request.GET.get('sort_key') cc_user.save() #there are 2 dimensions to consider when executing a search with respect to group id #is user a moderator #did the user request a group query_params = default_query_params.copy() query_params.update( strip_none( extract(request.GET, [ 'page', 'sort_key', 'text', 'commentable_ids', 'flagged', 'unread', 'unanswered', ]))) paginated_results = cc.Thread.search(query_params) threads = paginated_results.collection # If not provided with a discussion id, filter threads by commentable ids # which are accessible to the current user. if discussion_id is None: discussion_category_ids = set( utils.get_discussion_categories_ids(course, request.user)) threads = [ thread for thread in threads if thread.get('commentable_id') in discussion_category_ids ] for thread in threads: # patch for backward compatibility to comments service if 'pinned' not in thread: thread['pinned'] = False query_params['page'] = paginated_results.page query_params['num_pages'] = paginated_results.num_pages query_params['corrected_text'] = paginated_results.corrected_text return threads, query_params
def get_threads(request, course, user_info, discussion_id=None, per_page=THREADS_PER_PAGE): """ This may raise an appropriate subclass of cc.utils.CommentClientError if something goes wrong, or ValueError if the group_id is invalid. Arguments: request (WSGIRequest): The user request. course (CourseDescriptorWithMixins): The course object. user_info (dict): The comment client User object as a dict. discussion_id (unicode): Optional discussion id/commentable id for context. per_page (int): Optional number of threads per page. Returns: (tuple of list, dict): A tuple of the list of threads and a dict of the query parameters used for the search. """ default_query_params = { 'page': 1, 'per_page': per_page, 'sort_key': 'activity', 'text': '', 'course_id': unicode(course.id), 'user_id': request.user.id, 'context': ThreadContext.COURSE, 'group_id': get_group_id_for_comments_service(request, course.id, discussion_id), # may raise ValueError } # If provided with a discussion id, filter by discussion id in the # comments_service. if discussion_id is not None: default_query_params['commentable_id'] = discussion_id # Use the discussion id/commentable id to determine the context we are going to pass through to the backend. if get_team(discussion_id) is not None: default_query_params['context'] = ThreadContext.STANDALONE if not request.GET.get('sort_key'): # If the user did not select a sort key, use their last used sort key default_query_params['sort_key'] = user_info.get('default_sort_key') or default_query_params['sort_key'] elif request.GET.get('sort_key') != user_info.get('default_sort_key'): # If the user clicked a sort key, update their default sort key cc_user = cc.User.from_django_user(request.user) cc_user.default_sort_key = request.GET.get('sort_key') cc_user.save() #there are 2 dimensions to consider when executing a search with respect to group id #is user a moderator #did the user request a group query_params = default_query_params.copy() query_params.update( strip_none( extract( request.GET, [ 'page', 'sort_key', 'text', 'commentable_ids', 'flagged', 'unread', 'unanswered', ] ) ) ) paginated_results = cc.Thread.search(query_params) threads = paginated_results.collection # If not provided with a discussion id, filter threads by commentable ids # which are accessible to the current user. if discussion_id is None: discussion_category_ids = set(utils.get_discussion_categories_ids(course, request.user)) threads = [ thread for thread in threads if thread.get('commentable_id') in discussion_category_ids ] for thread in threads: # patch for backward compatibility to comments service if 'pinned' not in thread: thread['pinned'] = False query_params['page'] = paginated_results.page query_params['num_pages'] = paginated_results.num_pages query_params['corrected_text'] = paginated_results.corrected_text return threads, query_params
def create_thread(request, course_id, commentable_id): """ Given a course and commentable ID, create the thread """ log.debug(u"Creating new thread in %r, id %r", course_id, commentable_id) course_key = CourseKey.from_string(course_id) course = get_course_with_access(request.user, 'load', course_key) post = request.POST user = request.user if course.allow_anonymous: anonymous = post.get('anonymous', 'false').lower() == 'true' else: anonymous = False if course.allow_anonymous_to_peers: anonymous_to_peers = post.get('anonymous_to_peers', 'false').lower() == 'true' else: anonymous_to_peers = False if 'title' not in post or not post['title'].strip(): return JsonError(_("Title can't be empty")) if 'body' not in post or not post['body'].strip(): return JsonError(_("Body can't be empty")) params = { 'anonymous': anonymous, 'anonymous_to_peers': anonymous_to_peers, 'commentable_id': commentable_id, 'course_id': text_type(course_key), 'user_id': user.id, 'thread_type': post["thread_type"], 'body': post["body"], 'title': post["title"], } # Check for whether this commentable belongs to a team, and add the right context if get_team(commentable_id) is not None: params['context'] = ThreadContext.STANDALONE else: params['context'] = ThreadContext.COURSE thread = cc.Thread(**params) # Divide the thread if required try: group_id = get_group_id_for_comments_service(request, course_key, commentable_id) except ValueError: return HttpResponseServerError("Invalid group id for commentable") if group_id is not None: thread.group_id = group_id thread.save() thread_created.send(sender=None, user=user, post=thread) # patch for backward compatibility to comments service if 'pinned' not in thread.attributes: thread['pinned'] = False follow = post.get('auto_subscribe', 'false').lower() == 'true' if follow: cc_user = cc.User.from_django_user(user) cc_user.follow(thread) thread_followed.send(sender=None, user=user, post=thread) data = thread.to_dict() add_courseware_context([data], course, user) track_thread_created_event(request, course, thread, follow) if request.is_ajax(): return ajax_content_response(request, course_key, data) else: return JsonResponse(prepare_content(data, course_key))
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
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