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 """ course_discussion_settings = get_course_discussion_settings(course_key) if commentable_id is None or is_commentable_divided(course_key, commentable_id, course_discussion_settings): 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 group_id = int(requested_group_id) _verify_group_exists(group_id, course_discussion_settings) else: # regular users always query with their own id. group_id = get_group_id_for_user(request.user, course_discussion_settings) return group_id else: # Never pass a group_id to the comments service for a non-divided # commentable return None
def inline_discussion(request, course_key, discussion_id): """ Renders JSON for DiscussionModules """ 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, 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) 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) course_discussion_settings = get_course_discussion_settings(course.id) return utils.JsonResponse({ 'is_commentable_divided': is_commentable_divided(course_key, discussion_id), 'discussion_data': 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) })
def patch(self, request, course_id): """ Implement a handler for the PATCH method. """ if request.content_type != MergePatchParser.media_type: raise UnsupportedMediaType(request.content_type) kwargs = self._get_request_kwargs(course_id) form = CourseDiscussionSettingsForm(kwargs, request_user=request.user) if not form.is_valid(): raise ValidationError(form.errors) course = form.cleaned_data['course'] course_key = form.cleaned_data['course_key'] discussion_settings = get_course_discussion_settings(course_key) serializer = DiscussionSettingsSerializer( data=request.data, partial=True, course=course, discussion_settings=discussion_settings ) if not serializer.is_valid(): raise ValidationError(serializer.errors) settings_to_change = serializer.validated_data['settings_to_change'] try: discussion_settings = set_course_discussion_settings(course_key, **settings_to_change) except ValueError as e: raise ValidationError(unicode(e)) return Response(status=status.HTTP_204_NO_CONTENT)
def post(self, request, course_id, rolename): """ Implement a handler for the POST method. """ kwargs = self._get_request_kwargs(course_id, rolename) form = CourseDiscussionRolesForm(kwargs, request_user=request.user) if not form.is_valid(): raise ValidationError(form.errors) course_id = form.cleaned_data['course_key'] rolename = form.cleaned_data['rolename'] serializer = DiscussionRolesSerializer(data=request.data) if not serializer.is_valid(): raise ValidationError(serializer.errors) action = serializer.validated_data['action'] user = serializer.validated_data['user'] try: update_forum_role(course_id, user, rolename, action) except Role.DoesNotExist: raise ValidationError("Role '{}' does not exist".format(rolename)) role = form.cleaned_data['role'] data = {'course_id': course_id, 'users': role.users.all()} context = {'course_discussion_settings': get_course_discussion_settings(course_id)} serializer = DiscussionRolesListSerializer(data, context=context) return Response(serializer.data)
def get_context(course, request, thread=None): """ Returns a context appropriate for use with ThreadSerializer or (if thread is provided) CommentSerializer. """ # TODO: cache staff_user_ids and ta_user_ids if we need to improve perf staff_user_ids = { user.id for role in Role.objects.filter( name__in=[FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR], course_id=course.id ) for user in role.users.all() } ta_user_ids = { user.id for role in Role.objects.filter(name=FORUM_ROLE_COMMUNITY_TA, course_id=course.id) for user in role.users.all() } requester = request.user cc_requester = CommentClientUser.from_django_user(requester).retrieve() cc_requester["course_id"] = course.id course_discussion_settings = get_course_discussion_settings(course.id) return { "course": course, "request": request, "thread": thread, "discussion_division_enabled": course_discussion_division_enabled(course_discussion_settings), "group_ids_to_names": get_group_names_by_id(course_discussion_settings), "is_requester_privileged": requester.id in staff_user_ids or requester.id in ta_user_ids, "staff_user_ids": staff_user_ids, "ta_user_ids": ta_user_ids, "cc_requester": cc_requester, }
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 ThreadNotFoundError if the thread does not exist or the user cannot access it. """ retrieve_kwargs = retrieve_kwargs or {} try: if "with_responses" not in retrieve_kwargs: retrieve_kwargs["with_responses"] = False 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(course_key, request.user) context = get_context(course, request, cc_thread) course_discussion_settings = get_course_discussion_settings(course_key) if ( not context["is_requester_privileged"] and cc_thread["group_id"] and is_commentable_divided(course.id, cc_thread["commentable_id"], course_discussion_settings) ): requester_group_id = get_group_id_for_user(request.user, course_discussion_settings) if requester_group_id is not None and cc_thread["group_id"] != requester_group_id: raise ThreadNotFoundError("Thread not found.") 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 ThreadNotFoundError("Thread not found.")
def get(self, request, course_id): """ Implement a handler for the GET method. """ kwargs = self._get_request_kwargs(course_id) form = CourseDiscussionSettingsForm(kwargs, request_user=request.user) if not form.is_valid(): raise ValidationError(form.errors) course_key = form.cleaned_data['course_key'] course = form.cleaned_data['course'] discussion_settings = get_course_discussion_settings(course_key) return self._get_representation(course, course_key, discussion_settings)
def test_enabling_cohorts_does_not_change_division_scheme(self): """ Verify that enabling cohorts on a course does not automatically set the discussion division_scheme to cohort. """ config_course_cohorts(self.course, is_cohorted=False, discussion_division_scheme=CourseDiscussionSettings.NONE) response = self.get_handler(self.course, handler=course_cohort_settings_handler) expected_response = self.get_expected_response() expected_response['is_cohorted'] = False self.assertEqual(response, expected_response) self.assertEqual( CourseDiscussionSettings.NONE, get_course_discussion_settings(self.course.id).division_scheme ) expected_response['is_cohorted'] = True response = self.patch_handler(self.course, data=expected_response, handler=course_cohort_settings_handler) self.assertEqual(response, expected_response) self.assertEqual( CourseDiscussionSettings.NONE, get_course_discussion_settings(self.course.id).division_scheme )
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': 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 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") user = request.user if not course_id: raise ValidationError({"course_id": ["This field is required."]}) try: course_key = CourseKey.from_string(course_id) course = _get_course(course_key, user) except InvalidKeyError: raise ValidationError({"course_id": ["Invalid value."]}) context = get_context(course, request) _check_initializable_thread_fields(thread_data, context) discussion_settings = get_course_discussion_settings(course_key) if ( "group_id" not in thread_data and is_commentable_divided(course_key, thread_data.get("topic_id"), discussion_settings) ): thread_data = thread_data.copy() thread_data["group_id"] = get_group_id_for_user(user, discussion_settings) 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.instance thread_created.send(sender=None, user=user, post=cc_thread) api_thread = serializer.data _do_extra_actions(api_thread, cc_thread, thread_data.keys(), actions_form, context, request) track_thread_created_event(request, course, cc_thread, actions_form.cleaned_data["following"]) return api_thread
def make_course_settings(course, user): """ Generate a JSON-serializable model for course settings, which will be used to initialize a DiscussionCourseSettings object on the client. """ course_discussion_settings = get_course_discussion_settings(course.id) group_names_by_id = get_group_names_by_id(course_discussion_settings) return { 'is_discussion_division_enabled': course_discussion_division_enabled(course_discussion_settings), 'allow_anonymous': course.allow_anonymous, 'allow_anonymous_to_peers': course.allow_anonymous_to_peers, 'groups': [ {"id": str(group_id), "name": group_name} for group_id, group_name in group_names_by_id.iteritems() ], 'category_map': utils.get_discussion_category_map(course, user) }
def get_user_group_ids(course_id, content, user=None): """ Given a user, course ID, and the content of the thread or comment, returns the group ID for the current user and the user that posted the thread/comment. """ content_user_group_id = None user_group_id = None if course_id is not None: course_discussion_settings = get_course_discussion_settings(course_id) if content.get('username'): try: content_user = get_user_by_username_or_email(content.get('username')) content_user_group_id = get_group_id_for_user(content_user, course_discussion_settings) except User.DoesNotExist: content_user_group_id = None user_group_id = get_group_id_for_user(user, course_discussion_settings) if user else None return user_group_id, content_user_group_id
def get(self, request, course_id, rolename): """ Implement a handler for the GET method. """ kwargs = self._get_request_kwargs(course_id, rolename) form = CourseDiscussionRolesForm(kwargs, request_user=request.user) if not form.is_valid(): raise ValidationError(form.errors) course_id = form.cleaned_data['course_key'] role = form.cleaned_data['role'] data = {'course_id': course_id, 'users': role.users.all()} context = {'course_discussion_settings': get_course_discussion_settings(course_id)} serializer = DiscussionRolesListSerializer(data, context=context) return Response(serializer.data)
def test(user, per, operator="or"): if isinstance(per, basestring): if per in CONDITIONS: return _check_condition(user, per, content) if 'group_' in per: # If a course does not have divided discussions # or a course has divided discussions, but the current user's content group does not equal # the content group of the commenter/poster, # then the current user does not have group edit permissions. division_scheme = get_course_discussion_settings(course_id).division_scheme if (division_scheme is CourseDiscussionSettings.NONE or user_group_id is None or content_user_group is None or user_group_id != content_user_group): return False return has_permission(user, per, course_id=course_id) elif isinstance(per, list) and operator in ["and", "or"]: results = [test(user, x, operator="and") for x in per] if operator == "or": return True in results elif operator == "and": return False not in results
def _find_thread(request, course, discussion_id, thread_id): """ Finds the discussion thread with the specified ID. 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. Returns: The thread in question if the user can see it, else None. """ try: thread = cc.Thread.find(thread_id).retrieve( with_responses=request.is_ajax(), 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: return None # Verify that the student has access to this thread if belongs to a course discussion module thread_context = getattr(thread, "context", "course") if thread_context == "course" and not utils.discussion_category_id_access(course, request.user, discussion_id): return None # verify that the thread belongs to the requesting student's group is_moderator = has_permission(request.user, "see_all_cohorts", course.id) course_discussion_settings = get_course_discussion_settings(course.id) if is_commentable_divided(course.id, discussion_id, course_discussion_settings) and not is_moderator: user_group_id = get_group_id_for_user(request.user, course_discussion_settings) if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id: return None return thread
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 get_group_id_for_user_from_cache(user, course_id): """ Caches the results of get_group_id_for_user, but serializes the course_id instead of the course_discussions_settings object as cache keys. """ return get_group_id_for_user(user, get_course_discussion_settings(course_id))
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': 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 get_discussion_category_map(course, user, divided_only_if_explicit=False, exclude_unstarted=True): """ Transform the list of this course's discussion xblocks into a recursive dictionary structure. This is used to render the discussion category map in the discussion tab sidebar for a given user. Args: course: Course for which to get the ids. user: User to check for access. divided_only_if_explicit (bool): If True, inline topics are marked is_divided only if they are explicitly listed in CourseDiscussionSettings.discussion_topics. Example: >>> example = { >>> "entries": { >>> "General": { >>> "sort_key": "General", >>> "is_divided": True, >>> "id": "i4x-edx-eiorguegnru-course-foobarbaz" >>> } >>> }, >>> "children": [ >>> ["General", "entry"], >>> ["Getting Started", "subcategory"] >>> ], >>> "subcategories": { >>> "Getting Started": { >>> "subcategories": {}, >>> "children": [ >>> ["Working with Videos", "entry"], >>> ["Videos on edX", "entry"] >>> ], >>> "entries": { >>> "Working with Videos": { >>> "sort_key": None, >>> "is_divided": False, >>> "id": "d9f970a42067413cbb633f81cfb12604" >>> }, >>> "Videos on edX": { >>> "sort_key": None, >>> "is_divided": False, >>> "id": "98d8feb5971041a085512ae22b398613" >>> } >>> } >>> } >>> } >>> } """ unexpanded_category_map = defaultdict(list) xblocks = get_accessible_discussion_xblocks(course, user) discussion_settings = get_course_discussion_settings(course.id) discussion_division_enabled = course_discussion_division_enabled( discussion_settings) divided_discussion_ids = discussion_settings.divided_discussions for xblock in xblocks: discussion_id = xblock.discussion_id title = xblock.discussion_target sort_key = xblock.sort_key category = " / ".join( [x.strip() for x in xblock.discussion_category.split("/")]) # Handle case where xblock.start is None entry_start_date = xblock.start if xblock.start else datetime.max.replace( tzinfo=UTC) unexpanded_category_map[category].append({ "title": title, "id": discussion_id, "sort_key": sort_key, "start_date": entry_start_date }) category_map = { "entries": defaultdict(dict), "subcategories": defaultdict(dict) } for category_path, entries in unexpanded_category_map.items(): node = category_map["subcategories"] path = [x.strip() for x in category_path.split("/")] # Find the earliest start date for the entries in this category category_start_date = None for entry in entries: if category_start_date is None or entry[ "start_date"] < category_start_date: category_start_date = entry["start_date"] for level in path[:-1]: if level not in node: node[level] = { "subcategories": defaultdict(dict), "entries": defaultdict(dict), "sort_key": level, "start_date": category_start_date } else: if node[level]["start_date"] > category_start_date: node[level]["start_date"] = category_start_date node = node[level]["subcategories"] level = path[-1] if level not in node: node[level] = { "subcategories": defaultdict(dict), "entries": defaultdict(dict), "sort_key": level, "start_date": category_start_date } else: if node[level]["start_date"] > category_start_date: node[level]["start_date"] = category_start_date divide_all_inline_discussions = ( # pylint: disable=invalid-name not divided_only_if_explicit and discussion_settings.always_divide_inline_discussions) dupe_counters = defaultdict( lambda: 0) # counts the number of times we see each title for entry in entries: is_entry_divided = (discussion_division_enabled and (divide_all_inline_discussions or entry["id"] in divided_discussion_ids)) title = entry["title"] if node[level]["entries"][title]: # If we've already seen this title, append an incrementing number to disambiguate # the category from other categores sharing the same title in the course discussion UI. dupe_counters[title] += 1 title = u"{title} ({counter})".format( title=title, counter=dupe_counters[title]) node[level]["entries"][title] = { "id": entry["id"], "sort_key": entry["sort_key"], "start_date": entry["start_date"], "is_divided": is_entry_divided } # TODO. BUG! : course location is not unique across multiple course runs! # (I think Kevin already noticed this) Need to send course_id with requests, store it # in the backend. for topic, entry in course.discussion_topics.items(): category_map['entries'][topic] = { "id": entry["id"], "sort_key": entry.get("sort_key", topic), "start_date": datetime.now(UTC), "is_divided": (discussion_division_enabled and entry["id"] in divided_discussion_ids) } _sort_map_entries(category_map, course.discussion_sort_alpha) return _filter_unstarted_categories( category_map, course) if exclude_unstarted else category_map
def prepare_content(content, course_key, is_staff=False, discussion_division_enabled=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) 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) 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) return content
def get_thread_list( request, course_key, page, page_size, topic_id_list=None, text_search=None, following=False, view=None, order_by="last_activity_at", order_direction="desc", requested_fields=None, ): """ Return the list of all discussion threads pertaining to the given course Parameters: request: The django request objects used for build_absolute_uri course_key: The key of the course to get discussion threads for page: The page number (1-indexed) to retrieve page_size: The number of threads to retrieve per page topic_id_list: The list of topic_ids to get the discussion threads for text_search A text search query string to match following: If true, retrieve only threads the requester is following view: filters for either "unread" or "unanswered" threads order_by: The key in which to sort the threads by. The only values are "last_activity_at", "comment_count", and "vote_count". The default is "last_activity_at". order_direction: The direction in which to sort the threads by. The default and only value is "desc". This will be removed in a future major version. requested_fields: Indicates which additional fields to return for each thread. (i.e. ['profile_image']) Note that topic_id_list, text_search, and following are mutually exclusive. Returns: A paginated result containing a list of threads; see discussion_api.views.ThreadViewSet for more detail. Raises: ValidationError: if an invalid value is passed for a field. ValueError: if more than one of the mutually exclusive parameters is provided CourseNotFoundError: if the requesting user does not have access to the requested course PageNotFoundError: if page requested is beyond the last """ exclusive_param_count = sum( 1 for param in [topic_id_list, text_search, following] if param) if exclusive_param_count > 1: # pragma: no cover raise ValueError( "More than one mutually exclusive param passed to get_thread_list") cc_map = { "last_activity_at": "activity", "comment_count": "comments", "vote_count": "votes" } if order_by not in cc_map: raise ValidationError({ "order_by": [ "Invalid value. '{}' must be 'last_activity_at', 'comment_count', or 'vote_count'" .format(order_by) ] }) if order_direction != "desc": raise ValidationError({ "order_direction": ["Invalid value. '{}' must be 'desc'".format(order_direction)] }) course = _get_course(course_key, request.user) context = get_context(course, request) query_params = { "user_id": unicode(request.user.id), "group_id": (None if context["is_requester_privileged"] else get_group_id_for_user( request.user, get_course_discussion_settings(course.id))), "page": page, "per_page": page_size, "text": text_search, "sort_key": cc_map.get(order_by), } if view: if view in ["unread", "unanswered"]: query_params[view] = "true" else: ValidationError({ "view": [ "Invalid value. '{}' must be 'unread' or 'unanswered'". format(view) ] }) if following: paginated_results = context["cc_requester"].subscribed_threads( query_params) else: query_params["course_id"] = unicode(course.id) query_params["commentable_ids"] = ",".join( topic_id_list) if topic_id_list else None query_params["text"] = text_search paginated_results = Thread.search(query_params) # The comments service returns the last page of results if the requested # page is beyond the last page, but we want be consistent with DRF's general # behavior and return a PageNotFoundError in that case if paginated_results.page != page: raise PageNotFoundError("Page not found (No results on this page).") results = _serialize_discussion_entities(request, context, paginated_results.collection, requested_fields, DiscussionEntity.thread) paginator = DiscussionAPIPagination(request, paginated_results.page, paginated_results.num_pages, paginated_results.thread_count) return paginator.get_paginated_response({ "results": results, "text_search_rewrite": paginated_results.corrected_text, })
def _create_discussion_board_context(request, course_key, discussion_id=None, thread_id=None): """ Returns the template context for rendering the discussion board. """ context = _create_base_discussion_view_context(request, course_key) course = context['course'] course_settings = context['course_settings'] user = context['user'] cc_user = cc.User.from_django_user(user) user_info = context['user_info'] if thread_id: 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, # 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 newrelic_function_trace("get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, user, user_info) with newrelic_function_trace("add_courseware_context"): add_courseware_context(threads, course, user) with newrelic_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': 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), # TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts 'upgrade_link': check_and_get_upgrade_link(request, user, course.id), 'upgrade_price': get_cosmetic_verified_display_price(course), # ENDTODO }) 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': 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 _create_discussion_board_context(request, course_key, discussion_id=None, thread_id=None): """ Returns the template context for rendering the discussion board. """ context = _create_base_discussion_view_context(request, course_key) course = context['course'] course_settings = context['course_settings'] user = context['user'] cc_user = cc.User.from_django_user(user) user_info = context['user_info'] if thread_id: 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, # 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 newrelic_function_trace("get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads( course_key, threads, user, user_info) with newrelic_function_trace("add_courseware_context"): add_courseware_context(threads, course, user) with newrelic_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': 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), # TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts 'upgrade_link': check_and_get_upgrade_link(request, user, course.id), 'upgrade_price': get_cosmetic_verified_display_price(course), # ENDTODO # 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), }) return context
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': 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 get_thread_list( request, course_key, page, page_size, topic_id_list=None, text_search=None, following=False, view=None, order_by="last_activity_at", order_direction="desc", requested_fields=None, ): """ Return the list of all discussion threads pertaining to the given course Parameters: request: The django request objects used for build_absolute_uri course_key: The key of the course to get discussion threads for page: The page number (1-indexed) to retrieve page_size: The number of threads to retrieve per page topic_id_list: The list of topic_ids to get the discussion threads for text_search A text search query string to match following: If true, retrieve only threads the requester is following view: filters for either "unread" or "unanswered" threads order_by: The key in which to sort the threads by. The only values are "last_activity_at", "comment_count", and "vote_count". The default is "last_activity_at". order_direction: The direction in which to sort the threads by. The default and only value is "desc". This will be removed in a future major version. requested_fields: Indicates which additional fields to return for each thread. (i.e. ['profile_image']) Note that topic_id_list, text_search, and following are mutually exclusive. Returns: A paginated result containing a list of threads; see discussion_api.views.ThreadViewSet for more detail. Raises: ValidationError: if an invalid value is passed for a field. ValueError: if more than one of the mutually exclusive parameters is provided CourseNotFoundError: if the requesting user does not have access to the requested course PageNotFoundError: if page requested is beyond the last """ exclusive_param_count = sum(1 for param in [topic_id_list, text_search, following] if param) if exclusive_param_count > 1: # pragma: no cover raise ValueError("More than one mutually exclusive param passed to get_thread_list") cc_map = {"last_activity_at": "activity", "comment_count": "comments", "vote_count": "votes"} if order_by not in cc_map: raise ValidationError({ "order_by": ["Invalid value. '{}' must be 'last_activity_at', 'comment_count', or 'vote_count'".format(order_by)] }) if order_direction != "desc": raise ValidationError({ "order_direction": ["Invalid value. '{}' must be 'desc'".format(order_direction)] }) course = _get_course(course_key, request.user) context = get_context(course, request) query_params = { "user_id": unicode(request.user.id), "group_id": ( None if context["is_requester_privileged"] else get_group_id_for_user(request.user, get_course_discussion_settings(course.id)) ), "page": page, "per_page": page_size, "text": text_search, "sort_key": cc_map.get(order_by), } if view: if view in ["unread", "unanswered"]: query_params[view] = "true" else: ValidationError({ "view": ["Invalid value. '{}' must be 'unread' or 'unanswered'".format(view)] }) if following: paginated_results = context["cc_requester"].subscribed_threads(query_params) else: query_params["course_id"] = unicode(course.id) query_params["commentable_ids"] = ",".join(topic_id_list) if topic_id_list else None query_params["text"] = text_search paginated_results = Thread.search(query_params) # The comments service returns the last page of results if the requested # page is beyond the last page, but we want be consistent with DRF's general # behavior and return a PageNotFoundError in that case if paginated_results.page != page: raise PageNotFoundError("Page not found (No results on this page).") results = _serialize_discussion_entities( request, context, paginated_results.collection, requested_fields, DiscussionEntity.thread ) paginator = DiscussionAPIPagination( request, paginated_results.page, paginated_results.num_pages, paginated_results.thread_count ) return paginator.get_paginated_response({ "results": results, "text_search_rewrite": paginated_results.corrected_text, })
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). """ user = cc.User.from_django_user(request.user) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) try: # 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 } try: group_id = get_group_id_for_comments_service(request, course_key) except ValueError: return HttpResponseServerError("Invalid group_id") 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 newrelic_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 newrelic_function_trace("add_courseware_context"): add_courseware_context(threads, course, request.user) if request.is_ajax(): return utils.JsonResponse({ 'discussion_data': threads, 'page': query_params['page'], 'num_pages': query_params['num_pages'], 'annotated_content_info': annotated_content_info, }) else: user_roles = django_user.roles.filter( course_id=course.id).order_by("name").values_list( "name", flat=True).distinct() with newrelic_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': 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 render_to_response( 'discussion/discussion_profile_page.html', context) except User.DoesNotExist: raise Http404
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). """ user = cc.User.from_django_user(request.user) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) try: # 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 } try: group_id = get_group_id_for_comments_service(request, course_key) except ValueError: return HttpResponseServerError("Invalid group_id") 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 newrelic_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 newrelic_function_trace("add_courseware_context"): add_courseware_context(threads, course, request.user) if request.is_ajax(): return utils.JsonResponse({ 'discussion_data': threads, 'page': query_params['page'], 'num_pages': query_params['num_pages'], 'annotated_content_info': annotated_content_info, }) else: user_roles = django_user.roles.filter( course_id=course.id ).order_by("name").values_list("name", flat=True).distinct() with newrelic_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': 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 render_to_response('discussion/discussion_profile_page.html', context) except User.DoesNotExist: raise Http404
def get_discussion_category_map(course, user, divided_only_if_explicit=False, exclude_unstarted=True): """ Transform the list of this course's discussion xblocks into a recursive dictionary structure. This is used to render the discussion category map in the discussion tab sidebar for a given user. Args: course: Course for which to get the ids. user: User to check for access. divided_only_if_explicit (bool): If True, inline topics are marked is_divided only if they are explicitly listed in CourseDiscussionSettings.discussion_topics. Example: >>> example = { >>> "entries": { >>> "General": { >>> "sort_key": "General", >>> "is_divided": True, >>> "id": "i4x-edx-eiorguegnru-course-foobarbaz" >>> } >>> }, >>> "children": [ >>> ["General", "entry"], >>> ["Getting Started", "subcategory"] >>> ], >>> "subcategories": { >>> "Getting Started": { >>> "subcategories": {}, >>> "children": [ >>> ["Working with Videos", "entry"], >>> ["Videos on edX", "entry"] >>> ], >>> "entries": { >>> "Working with Videos": { >>> "sort_key": None, >>> "is_divided": False, >>> "id": "d9f970a42067413cbb633f81cfb12604" >>> }, >>> "Videos on edX": { >>> "sort_key": None, >>> "is_divided": False, >>> "id": "98d8feb5971041a085512ae22b398613" >>> } >>> } >>> } >>> } >>> } """ unexpanded_category_map = defaultdict(list) xblocks = get_accessible_discussion_xblocks(course, user) discussion_settings = get_course_discussion_settings(course.id) discussion_division_enabled = course_discussion_division_enabled(discussion_settings) divided_discussion_ids = discussion_settings.divided_discussions for xblock in xblocks: discussion_id = xblock.discussion_id title = xblock.discussion_target sort_key = xblock.sort_key category = " / ".join([x.strip() for x in xblock.discussion_category.split("/")]) # Handle case where xblock.start is None entry_start_date = xblock.start if xblock.start else datetime.max.replace(tzinfo=pytz.UTC) unexpanded_category_map[category].append({"title": title, "id": discussion_id, "sort_key": sort_key, "start_date": entry_start_date}) category_map = {"entries": defaultdict(dict), "subcategories": defaultdict(dict)} for category_path, entries in unexpanded_category_map.items(): node = category_map["subcategories"] path = [x.strip() for x in category_path.split("/")] # Find the earliest start date for the entries in this category category_start_date = None for entry in entries: if category_start_date is None or entry["start_date"] < category_start_date: category_start_date = entry["start_date"] for level in path[:-1]: if level not in node: node[level] = {"subcategories": defaultdict(dict), "entries": defaultdict(dict), "sort_key": level, "start_date": category_start_date} else: if node[level]["start_date"] > category_start_date: node[level]["start_date"] = category_start_date node = node[level]["subcategories"] level = path[-1] if level not in node: node[level] = {"subcategories": defaultdict(dict), "entries": defaultdict(dict), "sort_key": level, "start_date": category_start_date} else: if node[level]["start_date"] > category_start_date: node[level]["start_date"] = category_start_date divide_all_inline_discussions = ( # pylint: disable=invalid-name not divided_only_if_explicit and discussion_settings.always_divide_inline_discussions ) dupe_counters = defaultdict(lambda: 0) # counts the number of times we see each title for entry in entries: is_entry_divided = ( discussion_division_enabled and ( divide_all_inline_discussions or entry["id"] in divided_discussion_ids ) ) title = entry["title"] if node[level]["entries"][title]: # If we've already seen this title, append an incrementing number to disambiguate # the category from other categores sharing the same title in the course discussion UI. dupe_counters[title] += 1 title = u"{title} ({counter})".format(title=title, counter=dupe_counters[title]) node[level]["entries"][title] = {"id": entry["id"], "sort_key": entry["sort_key"], "start_date": entry["start_date"], "is_divided": is_entry_divided} # TODO. BUG! : course location is not unique across multiple course runs! # (I think Kevin already noticed this) Need to send course_id with requests, store it # in the backend. for topic, entry in course.discussion_topics.items(): category_map['entries'][topic] = { "id": entry["id"], "sort_key": entry.get("sort_key", topic), "start_date": datetime.now(UTC()), "is_divided": ( discussion_division_enabled and entry["id"] in divided_discussion_ids ) } _sort_map_entries(category_map, course.discussion_sort_alpha) return _filter_unstarted_categories(category_map, course) if exclude_unstarted else category_map
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': 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) })
def prepare_content(content, course_key, is_staff=False, discussion_division_enabled=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) 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) 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) return content
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': 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 course_discussions_settings_handler(request, course_key_string): """ The restful handler for divided discussion setting requests. Requires JSON. This will raise 404 if user is not staff. GET Returns the JSON representation of divided discussion settings for the course. PATCH Updates the divided discussion settings for the course. Returns the JSON representation of updated settings. """ course_key = CourseKey.from_string(course_key_string) course = get_course_with_access(request.user, 'staff', course_key) discussion_settings = get_course_discussion_settings(course_key) if request.method == 'PATCH': divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions( course, discussion_settings) settings_to_change = {} if 'divided_course_wide_discussions' in request.json or 'divided_inline_discussions' in request.json: divided_course_wide_discussions = request.json.get( 'divided_course_wide_discussions', divided_course_wide_discussions) divided_inline_discussions = request.json.get( 'divided_inline_discussions', divided_inline_discussions) settings_to_change[ 'divided_discussions'] = divided_course_wide_discussions + divided_inline_discussions if 'always_divide_inline_discussions' in request.json: settings_to_change[ 'always_divide_inline_discussions'] = request.json.get( 'always_divide_inline_discussions') if 'division_scheme' in request.json: settings_to_change['division_scheme'] = request.json.get( 'division_scheme') if not settings_to_change: return JsonResponse({"error": unicode("Bad Request")}, 400) try: if settings_to_change: discussion_settings = set_course_discussion_settings( course_key, **settings_to_change) except ValueError as err: # Note: error message not translated because it is not exposed to the user (UI prevents this state). return JsonResponse({"error": unicode(err)}, 400) divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions( course, discussion_settings) return JsonResponse({ 'id': discussion_settings.id, 'divided_inline_discussions': divided_inline_discussions, 'divided_course_wide_discussions': divided_course_wide_discussions, 'always_divide_inline_discussions': discussion_settings.always_divide_inline_discussions, 'division_scheme': discussion_settings.division_scheme, 'available_division_schemes': available_division_schemes(course_key) })
def course_discussions_settings_handler(request, course_key_string): """ The restful handler for divided discussion setting requests. Requires JSON. This will raise 404 if user is not staff. GET Returns the JSON representation of divided discussion settings for the course. PATCH Updates the divided discussion settings for the course. Returns the JSON representation of updated settings. """ course_key = CourseKey.from_string(course_key_string) course = get_course_with_access(request.user, 'staff', course_key) discussion_settings = get_course_discussion_settings(course_key) if request.method == 'PATCH': divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions( course, discussion_settings ) settings_to_change = {} if 'divided_course_wide_discussions' in request.json or 'divided_inline_discussions' in request.json: divided_course_wide_discussions = request.json.get( 'divided_course_wide_discussions', divided_course_wide_discussions ) divided_inline_discussions = request.json.get( 'divided_inline_discussions', divided_inline_discussions ) settings_to_change['divided_discussions'] = divided_course_wide_discussions + divided_inline_discussions if 'always_divide_inline_discussions' in request.json: settings_to_change['always_divide_inline_discussions'] = request.json.get( 'always_divide_inline_discussions' ) if 'division_scheme' in request.json: settings_to_change['division_scheme'] = request.json.get( 'division_scheme' ) if not settings_to_change: return JsonResponse({"error": unicode("Bad Request")}, 400) try: if settings_to_change: discussion_settings = set_course_discussion_settings(course_key, **settings_to_change) except ValueError as err: # Note: error message not translated because it is not exposed to the user (UI prevents this state). return JsonResponse({"error": unicode(err)}, 400) divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions( course, discussion_settings ) return JsonResponse({ 'id': discussion_settings.id, 'divided_inline_discussions': divided_inline_discussions, 'divided_course_wide_discussions': divided_course_wide_discussions, 'always_divide_inline_discussions': discussion_settings.always_divide_inline_discussions, 'division_scheme': discussion_settings.division_scheme, 'available_division_schemes': available_division_schemes(course_key) })
def get_group_id_for_user_from_cache(user, course_id): """ Caches the results of get_group_id_for_user, but serializes the course_id instead of the course_discussions_settings object as cache keys. """ return get_group_id_for_user(user, get_course_discussion_settings(course_id))