def _get_thread_and_context(request, thread_id, retrieve_kwargs=None): """ Retrieve the given thread and build a serializer context for it, returning both. This function also enforces access control for the thread (checking both the user's access to the course and to the thread's cohort if applicable). Raises Http404 if the thread does not exist or the user cannot access it. """ retrieve_kwargs = retrieve_kwargs or {} try: if "mark_as_read" not in retrieve_kwargs: retrieve_kwargs["mark_as_read"] = False cc_thread = Thread(id=thread_id).retrieve(**retrieve_kwargs) course_key = CourseKey.from_string(cc_thread["course_id"]) course = _get_course_or_404(course_key, request.user) context = get_context(course, request, cc_thread) if ( not context["is_requester_privileged"] and cc_thread["group_id"] and is_commentable_cohorted(course.id, cc_thread["commentable_id"]) ): requester_cohort = get_cohort_id(request.user, course.id) if requester_cohort is not None and cc_thread["group_id"] != requester_cohort: raise Http404 return cc_thread, context except CommentClientRequestError: # params are validated at a higher level, so the only possible request # error is if the thread doesn't exist raise Http404
def inline_discussion(request, course_key, discussion_id): """ Renders JSON for DiscussionModules """ nr_transaction = newrelic.agent.current_transaction() 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: threads, query_params = get_threads(request, course, discussion_id, per_page=INLINE_THREADS_PER_PAGE) except ValueError: return HttpResponseBadRequest("Invalid group_id") with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): 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 newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course, request.user) return utils.JsonResponse({ 'is_commentable_cohorted': is_commentable_cohorted(course_key, discussion_id), 'discussion_data': threads, 'user_info': user_info, '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) })
def get_group_id_for_comments_service(request, course_key, commentable_id=None): """ Given a user requesting content within a `commentable_id`, determine the group_id which should be passed to the comments service. Returns: int: the group_id to pass to the comments service or None if nothing should be passed Raises: ValueError if the requested group_id is invalid """ if commentable_id is None or is_commentable_cohorted(course_key, commentable_id): if request.method == "GET": requested_group_id = request.GET.get('group_id') elif request.method == "POST": requested_group_id = request.POST.get('group_id') if cached_has_permission(request.user, "see_all_cohorts", course_key): if not requested_group_id: return None try: group_id = int(requested_group_id) get_cohort_by_id(course_key, group_id) except CourseUserGroup.DoesNotExist: raise ValueError else: # regular users always query with their own id. group_id = get_cohort_id(request.user, course_key) return group_id else: # Never pass a group_id to the comments service for a non-cohorted # commentable return None
def _get_thread_and_context(request, thread_id, retrieve_kwargs=None): """ Retrieve the given thread and build a serializer context for it, returning both. This function also enforces access control for the thread (checking both the user's access to the course and to the thread's cohort if applicable). Raises Http404 if the thread does not exist or the user cannot access it. """ retrieve_kwargs = retrieve_kwargs or {} try: if "mark_as_read" not in retrieve_kwargs: retrieve_kwargs["mark_as_read"] = False cc_thread = Thread(id=thread_id).retrieve(**retrieve_kwargs) course_key = CourseKey.from_string(cc_thread["course_id"]) course = _get_course_or_404(course_key, request.user) context = get_context(course, request, cc_thread) if (not context["is_requester_privileged"] and cc_thread["group_id"] and is_commentable_cohorted( course.id, cc_thread["commentable_id"])): requester_cohort = get_cohort_id(request.user, course.id) if requester_cohort is not None and cc_thread[ "group_id"] != requester_cohort: raise Http404 return cc_thread, context except CommentClientRequestError: # params are validated at a higher level, so the only possible request # error is if the thread doesn't exist raise Http404
def get_group_id_for_comments_service(request, course_key, commentable_id=None): """ Given a user requesting content within a `commentable_id`, determine the group_id which should be passed to the comments service. Returns: int: the group_id to pass to the comments service or None if nothing should be passed Raises: ValueError if the requested group_id is invalid """ if commentable_id is None or is_commentable_cohorted(course_key, commentable_id): if request.method == "GET": requested_group_id = request.GET.get('group_id') elif request.method == "POST": requested_group_id = request.POST.get('group_id') if has_permission(request.user, "see_all_cohorts", course_key): if not requested_group_id: return None try: group_id = int(requested_group_id) get_cohort_by_id(course_key, group_id) except CourseUserGroup.DoesNotExist: raise ValueError else: # regular users always query with their own id. group_id = get_cohort_id(request.user, course_key) return group_id else: # Never pass a group_id to the comments service for a non-cohorted # commentable return None
def inline_discussion(request, course_key, discussion_id): """ Renders JSON for DiscussionModules """ nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, 'load_forum', course_key) cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() try: threads, query_params = get_threads(request, course_key, discussion_id, per_page=INLINE_THREADS_PER_PAGE) except ValueError: return HttpResponseBadRequest("Invalid group_id") with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) is_staff = cached_has_permission(request.user, 'openclose_thread', course.id) threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads] with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course) return utils.JsonResponse({ 'is_commentable_cohorted': is_commentable_cohorted(course_key, discussion_id), 'discussion_data': threads, 'user_info': user_info, '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) })
def create_thread(request, thread_data): """ Create a thread. Arguments: request: The django request object used for build_absolute_uri and determining the requesting user. thread_data: The data for the created thread. Returns: The created thread; see discussion_api.views.ThreadViewSet for more detail. """ course_id = thread_data.get("course_id") if not course_id: raise ValidationError({"course_id": ["This field is required."]}) try: course_key = CourseKey.from_string(course_id) course = _get_course_or_404(course_key, request.user) except (Http404, InvalidKeyError): raise ValidationError({"course_id": ["Invalid value."]}) context = get_context(course, request) _check_initializable_thread_fields(thread_data, context) if ( "group_id" not in thread_data and is_commentable_cohorted(course_key, thread_data.get("topic_id")) ): thread_data = thread_data.copy() thread_data["group_id"] = get_cohort_id(request.user, course_key) serializer = ThreadSerializer(data=thread_data, context=context) actions_form = ThreadActionsForm(thread_data) if not (serializer.is_valid() and actions_form.is_valid()): raise ValidationError(dict(serializer.errors.items() + actions_form.errors.items())) serializer.save() cc_thread = serializer.object 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"]) ) return api_thread
def create_thread(request, thread_data): """ Create a thread. Arguments: request: The django request object used for build_absolute_uri and determining the requesting user. thread_data: The data for the created thread. Returns: The created thread; see discussion_api.views.ThreadViewSet for more detail. """ course_id = thread_data.get("course_id") if not course_id: raise ValidationError({"course_id": ["This field is required."]}) try: course_key = CourseKey.from_string(course_id) course = _get_course_or_404(course_key, request.user) except (Http404, InvalidKeyError): raise ValidationError({"course_id": ["Invalid value."]}) context = get_context(course, request) _check_initializable_thread_fields(thread_data, context) if ("group_id" not in thread_data and is_commentable_cohorted( course_key, thread_data.get("topic_id"))): thread_data = thread_data.copy() thread_data["group_id"] = get_cohort_id(request.user, course_key) serializer = ThreadSerializer(data=thread_data, context=context) actions_form = ThreadActionsForm(thread_data) if not (serializer.is_valid() and actions_form.is_valid()): raise ValidationError( dict(serializer.errors.items() + actions_form.errors.items())) serializer.save() cc_thread = serializer.object 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"])) return api_thread
def single_thread(request, course_key, discussion_id, thread_id): """ Renders a response to display a single discussion thread. """ nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course_settings = make_course_settings(course, request.user) cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() is_moderator = has_permission(request.user, "see_all_cohorts", course_key) # Verify that the student has access to this thread if belongs to a discussion module if discussion_id not in utils.get_discussion_categories_ids( course, request.user): raise Http404 # Currently, the front end always loads responses via AJAX, even for this # page; it would be a nice optimization to avoid that extra round trip to # the comments service. try: thread = cc.Thread.find(thread_id).retrieve( recursive=request.is_ajax(), user_id=request.user.id, response_skip=request.GET.get("resp_skip"), response_limit=request.GET.get("resp_limit")) except cc.utils.CommentClientRequestError as e: if e.status_code == 404: raise Http404 raise # verify that the thread belongs to the requesting student's cohort if is_commentable_cohorted(course_key, discussion_id) and not is_moderator: user_group_id = get_cohort_id(request.user, course_key) if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id: raise Http404 is_staff = has_permission(request.user, 'openclose_thread', course.id) if request.is_ajax(): with newrelic.agent.FunctionTrace(nr_transaction, "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 newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context([content], course, request.user) return utils.JsonResponse({ 'content': content, 'annotated_content_info': annotated_content_info, }) else: try: threads, query_params = get_threads(request, course) except ValueError: return HttpResponseBadRequest("Invalid group_id") threads.append(thread.to_dict()) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course, request.user) for thread in threads: # patch for backward compatibility with comments service if "pinned" not in thread: thread["pinned"] = False threads = [ utils.prepare_content(thread, course_key, is_staff) for thread in threads ] with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads( course_key, threads, request.user, user_info) with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): user_cohort = get_cohort_id(request.user, course_key) context = { 'discussion_id': discussion_id, 'csrf': csrf(request)['csrf_token'], 'init': '', # TODO: What is this? 'user_info': _attr_safe_json(user_info), 'annotated_content_info': _attr_safe_json(annotated_content_info), 'course': course, #'recent_active_threads': recent_active_threads, 'course_id': course.id.to_deprecated_string( ), # TODO: Why pass both course and course.id to template? 'thread_id': thread_id, 'threads': _attr_safe_json(threads), 'roles': _attr_safe_json(utils.get_role_ids(course_key)), 'is_moderator': is_moderator, 'thread_pages': query_params['num_pages'], 'is_course_cohorted': is_course_cohorted(course_key), 'flag_moderator': (has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course)), 'cohorts': course_settings["cohorts"], 'user_cohort': user_cohort, 'sort_preference': cc_user.default_sort_key, 'category_map': course_settings["category_map"], 'course_settings': _attr_safe_json(course_settings) } return render_to_response('discussion/index.html', context)
def single_thread(request, course_key, discussion_id, thread_id): """ Renders a response to display a single discussion thread. """ nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course_settings = make_course_settings(course, request.user) cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() is_moderator = has_permission(request.user, "see_all_cohorts", course_key) # Verify that the student has access to this thread if belongs to a discussion module if discussion_id not in utils.get_discussion_categories_ids(course, request.user): raise Http404 # Currently, the front end always loads responses via AJAX, even for this # page; it would be a nice optimization to avoid that extra round trip to # the comments service. try: thread = cc.Thread.find(thread_id).retrieve( recursive=request.is_ajax(), user_id=request.user.id, response_skip=request.GET.get("resp_skip"), response_limit=request.GET.get("resp_limit") ) except cc.utils.CommentClientRequestError as e: if e.status_code == 404: raise Http404 raise # verify that the thread belongs to the requesting student's cohort if is_commentable_cohorted(course_key, discussion_id) and not is_moderator: user_group_id = get_cohort_id(request.user, course_key) if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id: raise Http404 is_staff = has_permission(request.user, 'openclose_thread', course.id) if request.is_ajax(): with newrelic.agent.FunctionTrace(nr_transaction, "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 newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context([content], course, request.user) return utils.JsonResponse({ 'content': content, 'annotated_content_info': annotated_content_info, }) else: try: threads, query_params = get_threads(request, course) except ValueError: return HttpResponseBadRequest("Invalid group_id") threads.append(thread.to_dict()) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course, request.user) for thread in threads: # patch for backward compatibility with comments service if "pinned" not in thread: thread["pinned"] = False threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads] with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): user_cohort = get_cohort_id(request.user, course_key) context = { 'discussion_id': discussion_id, 'csrf': csrf(request)['csrf_token'], 'init': '', # TODO: What is this? 'user_info': _attr_safe_json(user_info), 'annotated_content_info': _attr_safe_json(annotated_content_info), 'course': course, #'recent_active_threads': recent_active_threads, 'course_id': course.id.to_deprecated_string(), # TODO: Why pass both course and course.id to template? 'thread_id': thread_id, 'threads': _attr_safe_json(threads), 'roles': _attr_safe_json(utils.get_role_ids(course_key)), 'is_moderator': is_moderator, 'thread_pages': query_params['num_pages'], 'is_course_cohorted': is_course_cohorted(course_key), 'flag_moderator': ( has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course) ), 'cohorts': course_settings["cohorts"], 'user_cohort': user_cohort, 'sort_preference': cc_user.default_sort_key, 'category_map': course_settings["category_map"], 'course_settings': _attr_safe_json(course_settings) } return render_to_response('discussion/index.html', context)
def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE): """ This may raise an appropriate subclass of cc.utils.CommentClientError if something goes wrong. It may also raise ValueError if the group_id is invalid. """ default_query_params = { 'page': 1, 'per_page': per_page, 'sort_key': 'date', 'sort_order': 'desc', 'text': '', 'course_id': unicode(course.id), 'commentable_id': discussion_id, 'user_id': request.user.id, '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 if not request.GET.get('sort_key'): # If the user did not select a sort key, use their last used sort key cc_user = cc.User.from_django_user(request.user) cc_user.retrieve() # TODO: After the comment service is updated this can just be user.default_sort_key because the service returns the default value default_query_params['sort_key'] = cc_user.get('default_sort_key') or default_query_params['sort_key'] else: # 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 #if the user requested a group explicitly, give them that group, otherwise, if mod, show all, else if student, use cohort is_cohorted = is_commentable_cohorted(course.id, discussion_id) if has_permission(request.user, "see_all_cohorts", course.id): group_id = request.GET.get('group_id') if group_id in ("all", "None"): group_id = None else: group_id = get_cohort_id(request.user, course.id) if not group_id: default_query_params['exclude_groups'] = True if group_id: group_id = int(group_id) try: CourseUserGroup.objects.get(course_id=course.id, id=group_id) except CourseUserGroup.DoesNotExist: if not is_cohorted: group_id = None else: raise ValueError("Invalid Group ID") default_query_params["group_id"] = group_id #so by default, a moderator sees all items, and a student sees his cohort query_params = merge_dict( default_query_params, strip_none( extract( request.GET, [ 'page', 'sort_key', 'sort_order', 'text', 'commentable_ids', 'flagged', 'unread', 'unanswered', ] ) ) ) if not is_cohorted: query_params.pop('group_id', None) threads, page, num_pages, corrected_text = cc.Thread.search(query_params) threads = _set_group_names(course.id, threads) query_params['page'] = page query_params['num_pages'] = num_pages query_params['corrected_text'] = corrected_text return threads, query_params