def update_thread(request, thread_id, update_data): """ Update a thread. Arguments: request: The django request object used for build_absolute_uri and determining the requesting user. thread_id: The id for the thread to update. update_data: The data to update in the thread. Returns: The updated thread; see discussion_api.views.ThreadViewSet for more detail. """ cc_thread, context = _get_thread_and_context(request, thread_id) _check_editable_fields(cc_thread, update_data, context) serializer = ThreadSerializer(cc_thread, data=update_data, partial=True, context=context) actions_form = ThreadActionsForm(update_data) if not (serializer.is_valid() and actions_form.is_valid()): raise ValidationError(dict(serializer.errors.items() + actions_form.errors.items())) # Only save thread object if some of the edited fields are in the thread data, not extra actions if set(update_data) - set(actions_form.fields): serializer.save() thread_edited.send(sender=None, user=request.user, post=cc_thread) api_thread = serializer.data _do_extra_actions(api_thread, cc_thread, update_data.keys(), actions_form, context) return api_thread
def test_create_missing_field(self): for field in self.minimal_data: data = self.minimal_data.copy() data.pop(field) serializer = ThreadSerializer(data=data) self.assertFalse(serializer.is_valid()) self.assertEqual(serializer.errors, {field: ["This field is required."]})
def test_type(self): data = self.minimal_data.copy() data["type"] = "question" self.save_and_reserialize(data) data["type"] = "invalid_type" serializer = ThreadSerializer(data=data) self.assertFalse(serializer.is_valid())
def test_create_empty_string(self, value): data = self.minimal_data.copy() data.update({field: value for field in ["topic_id", "title", "raw_body"]}) serializer = ThreadSerializer(data=data, context=get_context(self.course, self.request)) self.assertFalse(serializer.is_valid()) self.assertEqual( serializer.errors, {field: ["This field may not be blank."] for field in ["topic_id", "title", "raw_body"]} )
def save_and_reserialize(self, data): """ Create a serializer with the given data, ensure that it is valid, save the result, and return the full thread data from the serializer. """ serializer = ThreadSerializer(data=data, context=get_context(self.course, self.request)) self.assertTrue(serializer.is_valid()) serializer.save() return serializer.data
def test_create_type(self): self.register_post_thread_response({"id": "test_id", "username": self.user.username}) data = self.minimal_data.copy() data["type"] = "question" self.save_and_reserialize(data) data["type"] = "invalid_type" serializer = ThreadSerializer(data=data) self.assertFalse(serializer.is_valid())
def test_update_course_id(self): serializer = ThreadSerializer( self.existing_thread, data={"course_id": "some/other/course"}, partial=True, context=get_context(self.course, self.request), ) self.assertFalse(serializer.is_valid()) self.assertEqual(serializer.errors, {"course_id": ["This field is not allowed in an update."]})
def test_update_course_id(self): serializer = ThreadSerializer(self.existing_thread, data={"course_id": "some/other/course"}, partial=True, context=get_context( self.course, self.request)) self.assertFalse(serializer.is_valid()) self.assertEqual( serializer.errors, {"course_id": ["This field is not allowed in an update."]})
def test_update_empty_string(self, value): serializer = ThreadSerializer( self.existing_thread, data={field: value for field in ["topic_id", "title", "raw_body"]}, partial=True, context=get_context(self.course, self.request), ) self.assertFalse(serializer.is_valid()) self.assertEqual( serializer.errors, {field: ["This field may not be blank."] for field in ["topic_id", "title", "raw_body"]} )
def save_and_reserialize(self, data, instance=None): """ Create a serializer with the given data and (if updating) instance, ensure that it is valid, save the result, and return the full thread data from the serializer. """ serializer = ThreadSerializer( instance, data=data, partial=(instance is not None), context=get_context(self.course, self.request) ) self.assertTrue(serializer.is_valid()) serializer.save() return serializer.data
def test_update_empty_string(self, value): serializer = ThreadSerializer( self.existing_thread, data={field: value for field in ["topic_id", "title", "raw_body"]}, partial=True, context=get_context(self.course, self.request) ) self.assertFalse(serializer.is_valid()) self.assertEqual( serializer.errors, {field: ["This field may not be blank."] for field in ["topic_id", "title", "raw_body"]} )
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_or_404(course_key, 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(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 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) 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 save_and_reserialize(self, data, instance=None): """ Create a serializer with the given data and (if updating) instance, ensure that it is valid, save the result, and return the full thread data from the serializer. """ serializer = ThreadSerializer(instance, data=data, partial=(instance is not None), context=get_context( self.course, self.request)) self.assertTrue(serializer.is_valid()) serializer.save() return serializer.data
def create_thread(request, thread_data): """ Create a thread. Parameters: 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 = CourseLocator.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) serializer = ThreadSerializer(data=thread_data, context=context) extras_form = ThreadCreateExtrasForm(thread_data) if not (serializer.is_valid() and extras_form.is_valid()): raise ValidationError(dict(serializer.errors.items() + extras_form.errors.items())) serializer.save() thread = serializer.object ret = serializer.data following = extras_form.cleaned_data["following"] if following: context["cc_requester"].follow(thread) ret["following"] = True track_forum_event( request, THREAD_CREATED_EVENT_NAME, course, thread, get_thread_created_event_data(thread, followed=following) ) return serializer.data
def serialize(self, thread): """ Create a serializer with an appropriate context and use it to serialize the given thread, returning the result. """ return ThreadSerializer(thread, context=get_context(self.course, self.request)).data
def update_thread(request, thread_id, update_data): """ Update a thread. Arguments: request: The django request object used for build_absolute_uri and determining the requesting user. thread_id: The id for the thread to update. update_data: The data to update in the thread. Returns: The updated thread; see discussion_api.views.ThreadViewSet for more detail. """ cc_thread, context = _get_thread_and_context( request, thread_id, retrieve_kwargs={"with_responses": True}) _check_editable_fields(cc_thread, update_data, context) serializer = ThreadSerializer(cc_thread, data=update_data, partial=True, context=context) actions_form = ThreadActionsForm(update_data) if not (serializer.is_valid() and actions_form.is_valid()): raise ValidationError( dict(serializer.errors.items() + actions_form.errors.items())) # Only save thread object if some of the edited fields are in the thread data, not extra actions if set(update_data) - set(actions_form.fields): serializer.save() # signal to update Teams when a user edits a thread thread_edited.send(sender=None, user=request.user, post=cc_thread) api_thread = serializer.data _do_extra_actions(api_thread, cc_thread, update_data.keys(), actions_form, context, request) # always return read as True (and therefore unread_comment_count=0) as reasonably # accurate shortcut, rather than adding additional processing. api_thread['read'] = True api_thread['unread_comment_count'] = 0 return api_thread
def update_thread(request, thread_id, update_data): """ Update a thread. Arguments: request: The django request object used for build_absolute_uri and determining the requesting user. thread_id: The id for the thread to update. update_data: The data to update in the thread. Returns: The updated thread; see discussion_api.views.ThreadViewSet for more detail. """ cc_thread, context = _get_thread_and_context(request, thread_id) _check_editable_fields(cc_thread, update_data, context) serializer = ThreadSerializer(cc_thread, data=update_data, partial=True, context=context) actions_form = ThreadActionsForm(update_data) if not (serializer.is_valid() and actions_form.is_valid()): raise ValidationError(dict(serializer.errors.items() + actions_form.errors.items())) # Only save thread object if some of the edited fields are in the thread data, not extra actions if set(update_data) - set(actions_form.fields): serializer.save() # signal to update Teams when a user edits a thread thread_edited.send(sender=None, user=request.user, post=cc_thread) api_thread = serializer.data _do_extra_actions(api_thread, cc_thread, update_data.keys(), actions_form, context, request) # always return read as True (and therefore unread_comment_count=0) as reasonably # accurate shortcut, rather than adding additional processing. api_thread['read'] = True api_thread['unread_comment_count'] = 0 return api_thread
def get_thread(request, thread_id): """ Retrieve a thread. Arguments: request: The django request object used for build_absolute_uri and determining the requesting user. thread_id: The id for the thread to retrieve """ cc_thread, context = _get_thread_and_context(request, thread_id) serializer = ThreadSerializer(cc_thread, context=context) return serializer.data
def _serialize_discussion_entities(request, context, discussion_entities, requested_fields, discussion_entity_type): """ It serializes Discussion Entity (Thread or Comment) and add additional data if requested. For a given list of Thread/Comment; it serializes and add additional information to the object as per requested_fields list (i.e. profile_image). Parameters: request: The django request object context: The context appropriate for use with the thread or comment discussion_entities: List of Thread or Comment objects requested_fields: Indicates which additional fields to return for each thread. discussion_entity_type: DiscussionEntity Enum value for Thread or Comment Returns: A list of serialized discussion entities """ results = [] usernames = [] include_profile_image = _include_profile_image(requested_fields) for entity in discussion_entities: if discussion_entity_type == DiscussionEntity.thread: serialized_entity = ThreadSerializer(entity, context=context).data elif discussion_entity_type == DiscussionEntity.comment: serialized_entity = CommentSerializer(entity, context=context).data results.append(serialized_entity) if include_profile_image: if serialized_entity['author'] and serialized_entity[ 'author'] not in usernames: usernames.append(serialized_entity['author']) if ('endorsed' in serialized_entity and serialized_entity['endorsed'] and 'endorsed_by' in serialized_entity and serialized_entity['endorsed_by'] and serialized_entity['endorsed_by'] not in usernames): usernames.append(serialized_entity['endorsed_by']) results = _add_additional_response_fields(request, results, usernames, discussion_entity_type, include_profile_image) return results
def get_thread_list(request, course_key, page, page_size): """ 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 Returns: A paginated result containing a list of threads; see discussion_api.views.ThreadViewSet for more detail. """ course = _get_course_or_404(course_key, request.user) context = get_context(course, request) threads, result_page, num_pages, _ = Thread.search({ "course_id": unicode(course.id), "group_id": (None if context["is_requester_privileged"] else get_cohort_id( request.user, course.id)), "sort_key": "date", "sort_order": "desc", "page": page, "per_page": page_size, }) # 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 404 in that case if result_page != page: raise Http404 results = [ ThreadSerializer(thread, context=context).data for thread in threads ] return get_paginated_data(request, results, page, num_pages)
def get_thread_list(request, course_key, page, page_size, topic_id_list=None, text_search=None, following=False): """ 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 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: ValueError: if more than one of the mutually exclusive parameters is provided Http404: if the requesting user does not have access to the requested course or a page beyond the last is requested """ 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") course = _get_course_or_404(course_key, request.user) context = get_context(course, request) query_params = { "group_id": (None if context["is_requester_privileged"] else get_cohort_id( request.user, course.id)), "sort_key": "date", "sort_order": "desc", "page": page, "per_page": page_size, "text": text_search, } text_search_rewrite = None if following: threads, result_page, num_pages = 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 threads, result_page, num_pages, text_search_rewrite = 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 404 in that case if result_page != page: raise Http404 results = [ ThreadSerializer(thread, context=context).data for thread in threads ] ret = get_paginated_data(request, results, page, num_pages) ret["text_search_rewrite"] = text_search_rewrite return ret
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", ): """ 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 only values are "asc" or "desc". The default is "desc". 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 Http404: if the requesting user does not have access to the requested course or a page beyond the last is requested """ 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": "date", "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 not in ["asc", "desc"]: raise ValidationError({ "order_direction": ["Invalid value. '{}' must be 'asc' or 'desc'".format(order_direction)] }) course = _get_course_or_404(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_cohort_id(request.user, course.id) ), "page": page, "per_page": page_size, "text": text_search, "sort_key": cc_map.get(order_by), "sort_order": order_direction, } text_search_rewrite = None 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: threads, result_page, num_pages = 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 threads, result_page, num_pages, text_search_rewrite = 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 404 in that case if result_page != page: raise Http404 results = [ThreadSerializer(thread, context=context).data for thread in threads] ret = get_paginated_data(request, results, page, num_pages) ret["text_search_rewrite"] = text_search_rewrite return ret