def user_profile(request, course_key, user_id): """ Renders a response to display the user profile page (shown after clicking on a post author's username). """ try: context = create_user_profile_context(request, course_key, user_id) context['threads'] = add_user_fullname_in_threads(context['threads']) if request.is_ajax(): return utils.JsonResponse({ 'discussion_data': context['threads'], 'page': context['page'], 'num_pages': context['num_pages'], 'annotated_content_info': context['annotated_content_info'], }) else: tab_view = CourseTabView() # To avoid mathjax loading from 'mathjax_include.html' # as that file causes multiple loadings of Mathjax on # 'user_profile' page context['load_mathjax'] = False return tab_view.get(request, unicode(course_key), 'discussion', profile_page_context=context) except User.DoesNotExist: raise Http404 except ValueError: return HttpResponseServerError("Invalid group_id")
def forum_form_discussion(request, course_key): """ Renders the main Discussion page, potentially filtered by a search query """ course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) request.user.is_community_ta = utils.is_user_community_ta( request.user, course.id) if request.is_ajax(): user = cc.User.from_django_user(request.user) user_info = user.to_dict() try: unsafethreads, query_params = get_threads( request, course, user_info) # This might process a search query is_staff = has_permission(request.user, 'openclose_thread', course.id) threads = [ utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads ] except cc.utils.CommentClientMaintenanceError: return HttpResponseServerError( 'Forum is in maintenance mode', status=status.HTTP_503_SERVICE_UNAVAILABLE) except ValueError: return HttpResponseServerError("Invalid group_id") with function_trace("get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads( course_key, threads, request.user, user_info) with function_trace("add_courseware_context"): add_courseware_context(threads, course, request.user) return utils.JsonResponse({ 'discussion_data': add_user_fullname_in_threads( threads ), # TODO: Standardize on 'discussion_data' vs 'threads' 'annotated_content_info': annotated_content_info, 'num_pages': query_params['num_pages'], 'page': query_params['page'], 'corrected_text': query_params['corrected_text'], }) else: course_id = unicode(course.id) tab_view = CourseTabView() return tab_view.get(request, course_id, 'discussion')
def single_thread(request, course_key, discussion_id, thread_id): """ Renders a response to display a single discussion thread. This could either be a page refresh after navigating to a single thread, a direct link to a single thread, or an AJAX call from the discussions UI loading the responses/comments for a single thread. Depending on the HTTP headers, we'll adjust our response accordingly. """ course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) request.user.is_community_ta = utils.is_user_community_ta( request.user, course.id) if request.is_ajax(): cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() is_staff = has_permission(request.user, 'openclose_thread', course.id) thread = _load_thread_for_viewing( request, course, discussion_id=discussion_id, thread_id=thread_id, raise_event=True, ) with function_trace("get_annotated_content_infos"): annotated_content_info = utils.get_annotated_content_infos( course_key, thread, request.user, user_info=user_info) content = utils.prepare_content(thread.to_dict(), course_key, is_staff) with function_trace("add_courseware_context"): add_courseware_context([content], course, request.user) # [COLARAZ_CUSTOM] content = add_user_fullname_in_threads(content) if len(content) == 1: content = content[0] return utils.JsonResponse({ 'content': content, 'annotated_content_info': annotated_content_info, }) else: course_id = unicode(course.id) tab_view = CourseTabView() return tab_view.get(request, course_id, 'discussion', discussion_id=discussion_id, thread_id=thread_id)
def render_to_fragment(self, request, course_id=None, discussion_id=None, thread_id=None, profile_page_context=None, **kwargs): """ Render the discussion board to a fragment. Args: request: The Django request. course_id: The id of the course in question. discussion_id: An optional discussion ID to be focused upon. thread_id: An optional ID of the thread to be shown. Returns: Fragment: The fragment representing the discussion board """ try: course_key = CourseKey.from_string(course_id) base_context = _create_base_discussion_view_context( request, course_key) course_object = get_course_by_id(course_key) # Note: # After the thread is rendered in this fragment, an AJAX # request is made and the thread is completely loaded again # (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) context['threads'] = add_user_fullname_in_threads( context['threads']) course_expiration_fragment = generate_course_expired_fragment( request.user, context['course']) context.update({ 'course_expiration_fragment': course_expiration_fragment, 'course_image_url': course_image_url(course_object), 'course_title': course_object.display_name_with_default, 'course_organization': course_object.display_org_with_default, }) if profile_page_context: # EDUCATOR-2119: styles are hard to reconcile if the profile page isn't also a fragment html = render_to_string( 'discussion/discussion_profile_page.html', profile_page_context) else: html = render_to_string( 'discussion/discussion_board_fragment.html', context) fragment = Fragment(html) self.add_fragment_resource_urls(fragment) inline_js = render_to_string( 'discussion/discussion_board_js.template', context) fragment.add_javascript(inline_js) if not settings.REQUIRE_DEBUG: fragment.add_javascript_url( staticfiles_storage.url( 'discussion/js/discussion_board_factory.js')) return fragment except cc.utils.CommentClientMaintenanceError: log.warning('Forum is in maintenance mode') html = render_to_response('discussion/maintenance_fragment.html', { 'disable_courseware_js': True, 'uses_pattern_library': True, }) return Fragment(html)
def create_user_profile_context(request, course_key, user_id): """ Generate a context dictionary for the user profile. """ user = cc.User.from_django_user(request.user) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) # If user is not enrolled in the course, do not proceed. django_user = User.objects.get(id=user_id) if not CourseEnrollment.is_enrolled(django_user, course.id): raise Http404 query_params = { 'page': request.GET.get('page', 1), 'per_page': THREADS_PER_PAGE, # more than threads_per_page to show more activities } group_id = get_group_id_for_comments_service(request, course_key) if group_id is not None: query_params['group_id'] = group_id profiled_user = cc.User(id=user_id, course_id=course_key, group_id=group_id) else: profiled_user = cc.User(id=user_id, course_id=course_key) threads, page, num_pages = profiled_user.active_threads(query_params) query_params['page'] = page query_params['num_pages'] = num_pages with function_trace("get_metadata_for_threads"): user_info = cc.User.from_django_user(request.user).to_dict() annotated_content_info = utils.get_metadata_for_threads( course_key, threads, request.user, user_info) is_staff = has_permission(request.user, 'openclose_thread', course.id) threads = [ utils.prepare_content(thread, course_key, is_staff) for thread in threads ] with function_trace("add_courseware_context"): add_courseware_context(threads, course, request.user) # TODO: LEARNER-3854: If we actually implement Learner Analytics code, this # code was original protected to not run in user_profile() if is_ajax(). # Someone should determine if that is still necessary (i.e. was that ever # called as is_ajax()) and clean this up as necessary. user_roles = django_user.roles.filter( course_id=course.id).order_by("name").values_list( "name", flat=True).distinct() with function_trace("get_cohort_info"): course_discussion_settings = get_course_discussion_settings( course_key) user_group_id = get_group_id_for_user(request.user, course_discussion_settings) context = _create_base_discussion_view_context(request, course_key) context.update({ 'django_user': django_user, 'django_user_roles': user_roles, 'profiled_user': profiled_user.to_dict(), 'threads': add_user_fullname_in_threads(threads), 'user_group_id': user_group_id, 'annotated_content_info': annotated_content_info, 'page': query_params['page'], 'num_pages': query_params['num_pages'], 'sort_preference': user.default_sort_key, 'learner_profile_page_url': reverse('learner_profile', kwargs={'username': django_user.username}), }) return context
def _create_discussion_board_context(request, base_context, thread=None): """ Returns the template context for rendering the discussion board. """ context = base_context.copy() 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'] user = context['user'] cc_user = cc.User.from_django_user(user) user_info = context['user_info'] if thread: # 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. threads = [thread.to_dict()] for thread in threads: # patch for backward compatibility with comments service if "pinned" not in thread: thread["pinned"] = False thread_pages = 1 root_url = reverse('forum_form_discussion', args=[unicode(course.id)]) else: threads, query_params = get_threads( request, course, user_info) # This might process a search query thread_pages = query_params['num_pages'] root_url = request.path is_staff = has_permission(user, 'openclose_thread', course.id) threads = [ utils.prepare_content(thread, course_key, is_staff) for thread in threads ] with function_trace("get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads( course_key, threads, user, user_info) with function_trace("add_courseware_context"): add_courseware_context(threads, course, user) with function_trace("get_cohort_info"): course_discussion_settings = get_course_discussion_settings(course_key) user_group_id = get_group_id_for_user(user, course_discussion_settings) context.update({ 'root_url': root_url, 'discussion_id': discussion_id, 'thread_id': thread_id, 'threads': add_user_fullname_in_threads(threads), 'thread_pages': thread_pages, 'annotated_content_info': annotated_content_info, 'is_moderator': has_permission(user, "see_all_cohorts", course_key), 'groups': course_settings[ "groups"], # still needed to render _thread_list_template 'user_group_id': user_group_id, # read from container in NewPostView 'sort_preference': cc_user.default_sort_key, 'category_map': course_settings["category_map"], 'course_settings': course_settings, 'is_commentable_divided': is_commentable_divided(course_key, discussion_id, course_discussion_settings), # If the default topic id is None the front-end code will look for a topic that contains "General" 'discussion_default_topic_id': _get_discussion_default_topic_id(course), }) context.update(get_experiment_user_metadata_context( course, user, )) return context
def inline_discussion(request, course_key, discussion_id): """ Renders JSON for DiscussionModules """ with function_trace('get_course_and_user_info'): course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() try: with function_trace('get_threads'): threads, query_params = get_threads( request, course, user_info, discussion_id, per_page=INLINE_THREADS_PER_PAGE) except ValueError: return HttpResponseServerError('Invalid group_id') with function_trace('get_metadata_for_threads'): annotated_content_info = utils.get_metadata_for_threads( course_key, threads, request.user, user_info) with function_trace('determine_group_permissions'): is_staff = has_permission(request.user, 'openclose_thread', course.id) course_discussion_settings = get_course_discussion_settings(course.id) group_names_by_id = get_group_names_by_id(course_discussion_settings) course_is_divided = course_discussion_settings.division_scheme is not CourseDiscussionSettings.NONE with function_trace('prepare_content'): threads = [ utils.prepare_content(thread, course_key, is_staff, course_is_divided, group_names_by_id) for thread in threads ] return utils.JsonResponse({ 'is_commentable_divided': is_commentable_divided(course_key, discussion_id), 'discussion_data': add_user_fullname_in_threads(threads), 'user_info': user_info, 'user_group_id': get_group_id_for_user(request.user, course_discussion_settings), 'annotated_content_info': annotated_content_info, 'page': query_params['page'], 'num_pages': query_params['num_pages'], 'roles': utils.get_role_ids(course_key), 'course_settings': make_course_settings(course, request.user, False) })
def prepare_content(content, course_key, is_staff=False, discussion_division_enabled=None, group_names_by_id=None): """ This function is used to pre-process thread and comment models in various ways before adding them to the HTTP response. This includes fixing empty attribute fields, enforcing author anonymity, and enriching metadata around group ownership and response endorsement. @TODO: not all response pre-processing steps are currently integrated into this function. Arguments: content (dict): A thread or comment. course_key (CourseKey): The course key of the course. is_staff (bool): Whether the user is a staff member. discussion_division_enabled (bool): Whether division of course discussions is enabled. Note that callers of this method do not need to provide this value (it defaults to None)-- it is calculated and then passed to recursive calls of this method. """ fields = [ 'id', 'title', 'body', 'course_id', 'anonymous', 'anonymous_to_peers', 'endorsed', 'parent_id', 'thread_id', 'votes', 'closed', 'created_at', 'updated_at', 'depth', 'type', 'commentable_id', 'comments_count', 'at_position_list', 'children', 'highlighted_title', 'highlighted_body', 'courseware_title', 'courseware_url', 'unread_comments_count', 'read', 'group_id', 'group_name', 'pinned', 'abuse_flaggers', 'stats', 'resp_skip', 'resp_limit', 'resp_total', 'thread_type', 'endorsed_responses', 'non_endorsed_responses', 'non_endorsed_resp_total', 'endorsement', 'context', 'last_activity_at' ] if (content.get('anonymous') is False) and ( (content.get('anonymous_to_peers') is False) or is_staff): fields += ['username', 'user_id'] content = strip_none(extract(content, fields)) if content.get("endorsement"): endorsement = content["endorsement"] endorser = None if endorsement["user_id"]: try: endorser = User.objects.get(pk=endorsement["user_id"]) except User.DoesNotExist: log.error( "User ID %s in endorsement for comment %s but not in our DB.", content.get('user_id'), content.get('id')) # Only reveal endorser if requester can see author or if endorser is staff if (endorser and ("username" in fields or has_permission(endorser, "endorse_comment", course_key))): endorsement["username"] = endorser.username else: del endorsement["user_id"] if discussion_division_enabled is None: discussion_division_enabled = course_discussion_division_enabled( get_course_discussion_settings(course_key)) for child_content_key in [ "children", "endorsed_responses", "non_endorsed_responses" ]: if child_content_key in content: children = [ prepare_content( child, course_key, is_staff, discussion_division_enabled=discussion_division_enabled, group_names_by_id=group_names_by_id) for child in content[child_content_key] ] content[child_content_key] = children if discussion_division_enabled: # Augment the specified thread info to include the group name if a group id is present. if content.get('group_id') is not None: course_discussion_settings = get_course_discussion_settings( course_key) if group_names_by_id: content['group_name'] = group_names_by_id.get( content.get('group_id')) else: content['group_name'] = get_group_name( content.get('group_id'), course_discussion_settings) content['is_commentable_divided'] = is_commentable_divided( course_key, content['commentable_id'], course_discussion_settings) else: # Remove any group information that might remain if the course had previously been divided. content.pop('group_id', None) # [COLARAZ_CUSTOM] adding profile name of user to thread add_user_fullname_in_threads(content) return content