Files
edx-platform/lms/djangoapps/discussion/rest_api/serializers.py
Régis Behmo 065adf398e feat: reapply forum v2 changes (#36002)
* feat: Reapply "Integrate Forum V2 into edx-platform"

This reverts commit 818aa343a2.

* feat: make it possible to globally disable forum v2 with setting

We introduce a setting that allows us to bypass any course waffle flag
check. The advantage of such a setting is that we don't need to find the
course ID: in some cases, we might not have access to the course ID, and
we need to look for it... in forum v2.

See discussion here: https://github.com/openedx/forum/issues/137

* chore: bump openedx-forum to 0.1.5

This should fix an issue with index creation on edX.org.
2024-12-12 12:18:33 +05:00

907 lines
33 KiB
Python

"""
Discussion API serializers
"""
from typing import Dict
from urllib.parse import urlencode, urlunparse
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db.models import TextChoices
from django.urls import reverse
from django.utils.html import strip_tags
from rest_framework import serializers
from common.djangoapps.student.models import get_user_by_username_or_email
from common.djangoapps.student.roles import GlobalStaff
from lms.djangoapps.discussion.django_comment_client.base.views import track_thread_lock_unlock_event, \
track_thread_edited_event, track_comment_edited_event, track_forum_response_mark_event
from lms.djangoapps.discussion.django_comment_client.utils import (
course_discussion_division_enabled,
get_group_id_for_user,
get_group_name,
is_comment_too_deep,
)
from lms.djangoapps.discussion.rest_api.permissions import (
NON_UPDATABLE_COMMENT_FIELDS,
NON_UPDATABLE_THREAD_FIELDS,
can_delete,
get_editable_fields,
)
from lms.djangoapps.discussion.rest_api.render import render_body
from lms.djangoapps.discussion.rest_api.utils import (
get_course_staff_users_list,
get_moderator_users_list,
get_course_ta_users_list,
)
from openedx.core.djangoapps.discussions.models import DiscussionTopicLink
from openedx.core.djangoapps.discussions.utils import get_group_names_by_id
from openedx.core.djangoapps.django_comment_common.comment_client.comment import Comment
from openedx.core.djangoapps.django_comment_common.comment_client.thread import Thread
from openedx.core.djangoapps.django_comment_common.comment_client.user import User as CommentClientUser
from openedx.core.djangoapps.django_comment_common.comment_client.utils import CommentClientRequestError
from openedx.core.djangoapps.django_comment_common.models import CourseDiscussionSettings
from openedx.core.djangoapps.user_api.accounts.api import get_profile_images
from openedx.core.lib.api.serializers import CourseKeyField
User = get_user_model()
CLOSE_REASON_CODES = getattr(settings, "DISCUSSION_MODERATION_CLOSE_REASON_CODES", {})
EDIT_REASON_CODES = getattr(settings, "DISCUSSION_MODERATION_EDIT_REASON_CODES", {})
class TopicOrdering(TextChoices):
"""
Enum for the available options for ordering topics.
"""
COURSE_STRUCTURE = "course_structure", "Course Structure"
ACTIVITY = "activity", "Activity"
NAME = "name", "Name"
def get_context(course, request, thread=None):
"""
Returns a context appropriate for use with ThreadSerializer or
(if thread is provided) CommentSerializer.
"""
course_staff_user_ids = get_course_staff_users_list(course.id)
moderator_user_ids = get_moderator_users_list(course.id)
ta_user_ids = get_course_ta_users_list(course.id)
requester = request.user
cc_requester = CommentClientUser.from_django_user(requester).retrieve(course_id=course.id)
cc_requester["course_id"] = course.id
course_discussion_settings = CourseDiscussionSettings.get(course.id)
is_global_staff = GlobalStaff().has_user(requester)
has_moderation_privilege = requester.id in moderator_user_ids or requester.id in ta_user_ids or is_global_staff
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),
"moderator_user_ids": moderator_user_ids,
"course_staff_user_ids": course_staff_user_ids,
"ta_user_ids": ta_user_ids,
"cc_requester": cc_requester,
"has_moderation_privilege": has_moderation_privilege,
"is_global_staff": is_global_staff,
"is_staff_or_admin": requester.id in course_staff_user_ids,
}
def validate_not_blank(value):
"""
Validate that a value is not an empty string or whitespace.
Raises: ValidationError
"""
if not value.strip():
raise ValidationError("This field may not be blank.")
def validate_edit_reason_code(value):
"""
Validate that the value is a valid edit reason code.
Raises: ValidationError
"""
if value not in EDIT_REASON_CODES:
raise ValidationError("Invalid edit reason code")
def validate_close_reason_code(value):
"""
Validate that the value is a valid close reason code.
Raises: ValidationError
"""
if value not in CLOSE_REASON_CODES:
raise ValidationError("Invalid close reason code")
def _validate_privileged_access(context: Dict) -> bool:
"""
Return the field specified by ``field_name`` if requesting user is privileged.
Checks that the course exists in the context, and that the user has privileged
access.
Args:
context (Dict): The serializer context.
Returns:
bool: Course exists and the user has privileged access.
"""
course = context.get('course', None)
is_requester_privileged = context.get('has_moderation_privilege')
return course and is_requester_privileged
class _ContentSerializer(serializers.Serializer):
# pylint: disable=abstract-method
"""
A base class for thread and comment serializers.
"""
id = serializers.CharField(read_only=True) # pylint: disable=invalid-name
author = serializers.SerializerMethodField()
author_label = serializers.SerializerMethodField()
created_at = serializers.CharField(read_only=True)
updated_at = serializers.CharField(read_only=True)
raw_body = serializers.CharField(source="body", validators=[validate_not_blank])
rendered_body = serializers.SerializerMethodField()
abuse_flagged = serializers.SerializerMethodField()
voted = serializers.SerializerMethodField()
vote_count = serializers.SerializerMethodField()
editable_fields = serializers.SerializerMethodField()
can_delete = serializers.SerializerMethodField()
anonymous = serializers.BooleanField(default=False)
anonymous_to_peers = serializers.BooleanField(default=False)
last_edit = serializers.SerializerMethodField(required=False)
edit_reason_code = serializers.CharField(required=False, validators=[validate_edit_reason_code])
edit_by_label = serializers.SerializerMethodField(required=False)
non_updatable_fields = set()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._rendered_body = None
for field in self.non_updatable_fields:
setattr(self, f"validate_{field}", self._validate_non_updatable)
def _validate_non_updatable(self, value):
"""Ensure that a field is not edited in an update operation."""
if self.instance:
raise ValidationError("This field is not allowed in an update.")
return value
def _is_user_privileged(self, user_id):
"""
Returns a boolean indicating whether the given user_id identifies a
privileged user.
"""
return user_id in self.context["moderator_user_ids"] or user_id in self.context["ta_user_ids"]
def _is_anonymous(self, obj):
"""
Returns a boolean indicating whether the content should be anonymous to
the requester.
"""
user_id = self.context["request"].user.id
is_user_staff = user_id in self.context["moderator_user_ids"] or user_id in self.context["ta_user_ids"]
return (
obj["anonymous"] or
obj["anonymous_to_peers"] and not is_user_staff
)
def get_author(self, obj):
"""
Returns the author's username, or None if the content is anonymous.
"""
return None if self._is_anonymous(obj) else obj["username"]
def _get_user_label(self, user_id):
"""
Returns the role label (i.e. "Staff", "Moderator" or "Community TA") for the user
with the given id.
"""
is_staff = user_id in self.context["course_staff_user_ids"]
is_moderator = user_id in self.context["moderator_user_ids"]
is_ta = user_id in self.context["ta_user_ids"]
return (
"Staff" if is_staff else
"Moderator" if is_moderator else
"Community TA" if is_ta else
None
)
def _get_user_label_from_username(self, username):
"""
Returns role label of user from username
Possible Role Labels: Staff, Moderator, Community TA or None
"""
try:
user = User.objects.get(username=username)
return self._get_user_label(user.id)
except ObjectDoesNotExist:
return None
def get_author_label(self, obj):
"""
Returns the role label for the content author.
"""
if self._is_anonymous(obj) or obj["user_id"] is None:
return None
else:
user_id = int(obj["user_id"])
return self._get_user_label(user_id)
def get_rendered_body(self, obj):
"""
Returns the rendered body content.
"""
if self._rendered_body is None:
self._rendered_body = render_body(obj["body"])
return self._rendered_body
def get_abuse_flagged(self, obj):
"""
Returns a boolean indicating whether the requester has flagged the
content as abusive.
"""
total_abuse_flaggers = len(obj.get("abuse_flaggers", []))
return (
self.context["has_moderation_privilege"] and total_abuse_flaggers > 0 or
self.context["cc_requester"]["id"] in obj.get("abuse_flaggers", [])
)
def get_voted(self, obj):
"""
Returns a boolean indicating whether the requester has voted for the
content.
"""
return obj["id"] in self.context["cc_requester"]["upvoted_ids"]
def get_vote_count(self, obj):
"""
Returns the number of votes for the content.
"""
return obj.get("votes", {}).get("up_count", 0)
def get_editable_fields(self, obj):
"""
Return the list of the fields the requester can edit
"""
return sorted(get_editable_fields(obj, self.context))
def get_can_delete(self, obj):
"""
Returns if the current user can delete this thread/comment.
"""
return can_delete(obj, self.context)
def get_last_edit(self, obj):
"""
Returns information about the last edit for this content for
privileged users.
"""
is_user_author = str(obj['user_id']) == str(self.context['request'].user.id)
if not (_validate_privileged_access(self.context) or is_user_author):
return None
edit_history = obj.get("edit_history")
if not edit_history:
return None
last_edit = edit_history[-1]
reason_code = last_edit.get("reason_code")
if reason_code:
last_edit["reason"] = EDIT_REASON_CODES.get(reason_code)
return last_edit
def get_edit_by_label(self, obj):
"""
Returns the role label for the last edit user.
"""
is_user_author = str(obj['user_id']) == str(self.context['request'].user.id)
is_user_privileged = _validate_privileged_access(self.context)
edit_history = obj.get("edit_history")
if (is_user_author or is_user_privileged) and edit_history:
last_edit = edit_history[-1]
return self._get_user_label_from_username(last_edit.get('editor_username'))
class ThreadSerializer(_ContentSerializer):
"""
A serializer for thread data.
N.B. This should not be used with a comment_client Thread object that has
not had retrieve() called, because of the interaction between DRF's attempts
at introspection and Thread's __getattr__.
"""
course_id = serializers.CharField()
topic_id = serializers.CharField(source="commentable_id", validators=[validate_not_blank])
group_id = serializers.IntegerField(required=False, allow_null=True)
group_name = serializers.SerializerMethodField()
type = serializers.ChoiceField(
source="thread_type",
choices=[(val, val) for val in ["discussion", "question"]]
)
preview_body = serializers.SerializerMethodField()
abuse_flagged_count = serializers.SerializerMethodField(required=False)
title = serializers.CharField(validators=[validate_not_blank])
pinned = serializers.SerializerMethodField()
closed = serializers.BooleanField(required=False)
following = serializers.SerializerMethodField()
comment_count = serializers.SerializerMethodField(read_only=True)
unread_comment_count = serializers.SerializerMethodField(read_only=True)
comment_list_url = serializers.SerializerMethodField()
endorsed_comment_list_url = serializers.SerializerMethodField()
non_endorsed_comment_list_url = serializers.SerializerMethodField()
read = serializers.BooleanField(required=False)
has_endorsed = serializers.BooleanField(source="endorsed", read_only=True)
response_count = serializers.IntegerField(source="resp_total", read_only=True, required=False)
close_reason_code = serializers.CharField(required=False, validators=[validate_close_reason_code])
close_reason = serializers.SerializerMethodField()
closed_by = serializers.SerializerMethodField()
closed_by_label = serializers.SerializerMethodField(required=False)
non_updatable_fields = NON_UPDATABLE_THREAD_FIELDS
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Compensate for the fact that some threads in the comments service do
# not have the pinned field set
if self.instance and self.instance.get("pinned") is None:
self.instance["pinned"] = False
def get_abuse_flagged_count(self, obj):
"""
Returns the number of users that flagged content as abusive only if user has staff permissions
"""
if _validate_privileged_access(self.context):
return obj.get("abuse_flagged_count")
def get_pinned(self, obj):
"""
Compensate for the fact that some threads in the comments service do
not have the pinned field set.
"""
return bool(obj["pinned"])
def get_group_name(self, obj):
"""
Returns the name of the group identified by the thread's group_id.
"""
return self.context["group_ids_to_names"].get(obj["group_id"])
def get_following(self, obj):
"""
Returns a boolean indicating whether the requester is following the
thread.
"""
return obj["id"] in self.context["cc_requester"]["subscribed_thread_ids"]
def get_comment_list_url(self, obj, endorsed=None):
"""
Returns the URL to retrieve the thread's comments, optionally including
the endorsed query parameter.
"""
if (
(obj["thread_type"] == "question" and endorsed is None) or
(obj["thread_type"] == "discussion" and endorsed is not None)
):
return None
path = reverse("comment-list")
query_dict = {"thread_id": obj["id"]}
if endorsed is not None:
query_dict["endorsed"] = endorsed
return self.context["request"].build_absolute_uri(
urlunparse(("", "", path, "", urlencode(query_dict), ""))
)
def get_endorsed_comment_list_url(self, obj):
"""
Returns the URL to retrieve the thread's endorsed comments.
"""
return self.get_comment_list_url(obj, endorsed=True)
def get_non_endorsed_comment_list_url(self, obj):
"""
Returns the URL to retrieve the thread's non-endorsed comments.
"""
return self.get_comment_list_url(obj, endorsed=False)
def get_comment_count(self, obj):
"""
Increments comment count to include post and returns total count of
contributions (i.e. post + responses + comments) for the thread
"""
return obj["comments_count"] + 1
def get_unread_comment_count(self, obj):
"""
Returns the number of unread comments. If the thread has never been read,
this additionally includes 1 for the post itself, in addition to its responses and
comments.
"""
if not obj["read"] and obj["comments_count"] == obj["unread_comments_count"]:
return obj["unread_comments_count"] + 1
return obj["unread_comments_count"]
def get_preview_body(self, obj):
"""
Returns a cleaned version of the thread's body to display in a preview capacity.
"""
return strip_tags(self.get_rendered_body(obj)).replace('\n', ' ').replace(' ', ' ')
def get_close_reason(self, obj):
"""
Returns the reason for which the thread was closed.
"""
is_user_author = str(obj['user_id']) == str(self.context['request'].user.id)
if not (_validate_privileged_access(self.context) or is_user_author):
return None
reason_code = obj.get("close_reason_code")
return CLOSE_REASON_CODES.get(reason_code)
def get_closed_by(self, obj):
"""
Returns the username of the moderator who closed this thread,
only to other privileged users and author.
"""
is_user_author = str(obj['user_id']) == str(self.context['request'].user.id)
if _validate_privileged_access(self.context) or is_user_author:
return obj.get("closed_by")
def get_closed_by_label(self, obj):
"""
Returns the role label for the user who closed the post.
"""
is_user_author = str(obj['user_id']) == str(self.context['request'].user.id)
if is_user_author or _validate_privileged_access(self.context):
return self._get_user_label_from_username(obj.get("closed_by"))
def create(self, validated_data):
thread = Thread(user_id=self.context["cc_requester"]["id"], **validated_data)
thread.save()
return thread
def update(self, instance, validated_data):
for key, val in validated_data.items():
instance[key] = val
requesting_user_id = self.context["cc_requester"]["id"]
if key == "closed" and val:
instance["closing_user_id"] = requesting_user_id
track_thread_lock_unlock_event(self.context['request'], self.context['course'],
instance, validated_data.get('close_reason_code'))
if key == "closed" and not val:
instance["closing_user_id"] = requesting_user_id
track_thread_lock_unlock_event(self.context['request'], self.context['course'],
instance, validated_data.get('close_reason_code'), locked=False)
if key == "body" and val:
instance["editing_user_id"] = requesting_user_id
track_thread_edited_event(self.context['request'], self.context['course'],
instance, validated_data.get('edit_reason_code'))
instance.save()
return instance
class CommentSerializer(_ContentSerializer):
"""
A serializer for comment data.
N.B. This should not be used with a comment_client Comment object that has
not had retrieve() called, because of the interaction between DRF's attempts
at introspection and Comment's __getattr__.
"""
thread_id = serializers.CharField()
parent_id = serializers.CharField(required=False, allow_null=True)
endorsed = serializers.BooleanField(required=False)
endorsed_by = serializers.SerializerMethodField()
endorsed_by_label = serializers.SerializerMethodField()
endorsed_at = serializers.SerializerMethodField()
child_count = serializers.IntegerField(read_only=True)
children = serializers.SerializerMethodField(required=False)
abuse_flagged_any_user = serializers.SerializerMethodField(required=False)
profile_image = serializers.SerializerMethodField(read_only=True)
non_updatable_fields = NON_UPDATABLE_COMMENT_FIELDS
def __init__(self, *args, **kwargs):
remove_fields = kwargs.pop('remove_fields', None)
super().__init__(*args, **kwargs)
if remove_fields:
# for multiple fields in a list
for field_name in remove_fields:
self.fields.pop(field_name)
def get_endorsed_by(self, obj):
"""
Returns the username of the endorsing user, if the information is
available and would not identify the author of an anonymous thread.
This information is unavailable outside the thread context.
"""
if not self.context.get("thread"):
return None
endorsement = obj.get("endorsement")
if endorsement:
endorser_id = int(endorsement["user_id"])
# Avoid revealing the identity of an anonymous non-staff question
# author who has endorsed a comment in the thread
if not (
self._is_anonymous(self.context["thread"]) and
not self._is_user_privileged(endorser_id)
):
return User.objects.get(id=endorser_id).username
return None
def get_endorsed_by_label(self, obj):
"""
Returns the role label (i.e. "Staff" or "Community TA") for the
endorsing user.
This information is unavailable outside the thread context.
"""
if not self.context.get("thread"):
return None
endorsement = obj.get("endorsement")
if endorsement:
return self._get_user_label(int(endorsement["user_id"]))
else:
return None
def get_endorsed_at(self, obj):
"""
Returns the timestamp for the endorsement, if available.
This information is unavailable outside the thread context.
"""
if not self.context.get("thread"):
return None
endorsement = obj.get("endorsement")
return endorsement["time"] if endorsement else None
def get_children(self, obj):
return [
CommentSerializer(child, context=self.context).data
for child in obj.get("children", [])
]
def to_representation(self, data):
# pylint: disable=arguments-differ
data = super().to_representation(data)
# Django Rest Framework v3 no longer includes None values
# in the representation. To maintain the previous behavior,
# we do this manually instead.
if 'parent_id' not in data:
data["parent_id"] = None
return data
def get_abuse_flagged_any_user(self, obj):
"""
Returns a boolean indicating whether any user has flagged the
content as abusive.
"""
if _validate_privileged_access(self.context):
return len(obj.get("abuse_flaggers", [])) > 0
def get_profile_image(self, obj):
request = self.context["request"]
return get_profile_images(request.user.profile, request.user, request)
def validate(self, attrs):
"""
Ensure that parent_id identifies a comment that is actually in the
thread identified by thread_id and does not violate the configured
maximum depth.
"""
parent = None
parent_id = attrs.get("parent_id")
if parent_id:
try:
parent = Comment(id=parent_id).retrieve()
except CommentClientRequestError:
pass
if not (parent and parent["thread_id"] == attrs["thread_id"]):
raise ValidationError(
"parent_id does not identify a comment in the thread identified by thread_id."
)
if is_comment_too_deep(parent):
raise ValidationError("Comment level is too deep.")
return attrs
def create(self, validated_data):
comment = Comment(
course_id=self.context["thread"]["course_id"],
user_id=self.context["cc_requester"]["id"],
**validated_data
)
comment.save()
return comment
def update(self, instance, validated_data):
for key, val in validated_data.items():
instance[key] = val
# TODO: The comments service doesn't populate the endorsement
# field on comment creation, so we only provide
# endorsement_user_id on update
requesting_user_id = self.context["cc_requester"]["id"]
if key == "endorsed":
track_forum_response_mark_event(self.context['request'], self.context['course'], instance, val)
instance["endorsement_user_id"] = requesting_user_id
if key == "body" and val:
instance["editing_user_id"] = requesting_user_id
track_comment_edited_event(self.context['request'], self.context['course'],
instance, validated_data.get('edit_reason_code'))
instance.save()
return instance
class DiscussionTopicSerializer(serializers.Serializer):
"""
Serializer for DiscussionTopic
"""
id = serializers.CharField(read_only=True) # pylint: disable=invalid-name
name = serializers.CharField(read_only=True)
thread_list_url = serializers.CharField(read_only=True)
children = serializers.SerializerMethodField()
thread_counts = serializers.DictField(read_only=True)
def get_children(self, obj):
"""
Returns a list of children of DiscussionTopicSerializer type
"""
if not obj.children:
return []
return [DiscussionTopicSerializer(child).data for child in obj.children]
def create(self, validated_data):
"""
Overriden create abstract method
"""
def update(self, instance, validated_data):
"""
Overriden update abstract method
"""
class DiscussionTopicSerializerV2(serializers.Serializer):
"""
Serializer for new style topics.
"""
id = serializers.CharField( # pylint: disable=invalid-name
read_only=True,
source="external_id",
help_text="Provider-specific unique id for the topic"
)
usage_key = serializers.CharField(
read_only=True,
help_text="Usage context for the topic",
)
name = serializers.CharField(
read_only=True,
source="title",
help_text="Topic name",
)
thread_counts = serializers.SerializerMethodField(
read_only=True,
help_text="Mapping of thread counts by type of thread",
)
enabled_in_context = serializers.BooleanField(
read_only=True,
help_text="Whether this topic is enabled in its context",
)
def get_thread_counts(self, obj: DiscussionTopicLink) -> Dict[str, int]:
"""
Get thread counts from provided context
"""
return self.context['thread_counts'].get(obj.external_id, {
"discussion": 0,
"question": 0,
})
class DiscussionRolesSerializer(serializers.Serializer):
"""
Serializer for course discussion roles.
"""
ACTION_CHOICES = (
('allow', 'allow'),
('revoke', 'revoke')
)
action = serializers.ChoiceField(ACTION_CHOICES)
user_id = serializers.CharField()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = None
def validate_user_id(self, user_id):
"""
Validate user id
Args:
user_id (str): username or email
Returns:
str: user id if valid
"""
try:
self.user = get_user_by_username_or_email(user_id)
return user_id
except User.DoesNotExist as err:
raise ValidationError(f"'{user_id}' is not a valid student identifier") from err
def validate(self, attrs):
"""Validate the data at an object level."""
# Store the user object to avoid fetching it again.
if hasattr(self, 'user'):
attrs['user'] = self.user
return attrs
def create(self, validated_data):
"""
Overriden create abstract method
"""
def update(self, instance, validated_data):
"""
Overriden update abstract method
"""
class DiscussionRolesMemberSerializer(serializers.Serializer):
"""
Serializer for course discussion roles member data.
"""
username = serializers.CharField()
email = serializers.EmailField()
first_name = serializers.CharField()
last_name = serializers.CharField()
group_name = serializers.SerializerMethodField()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.course_discussion_settings = self.context['course_discussion_settings']
def get_group_name(self, instance):
"""Return the group name of the user."""
group_id = get_group_id_for_user(instance, self.course_discussion_settings)
group_name = get_group_name(group_id, self.course_discussion_settings)
return group_name
def create(self, validated_data):
"""
Overriden create abstract method
"""
def update(self, instance, validated_data):
"""
Overriden update abstract method
"""
class DiscussionRolesListSerializer(serializers.Serializer):
"""
Serializer for course discussion roles member list.
"""
course_id = serializers.CharField()
results = serializers.SerializerMethodField()
division_scheme = serializers.SerializerMethodField()
def get_results(self, obj):
"""Return the nested serializer data representing a list of member users."""
context = {
'course_id': obj['course_id'],
'course_discussion_settings': self.context['course_discussion_settings']
}
serializer = DiscussionRolesMemberSerializer(obj['users'], context=context, many=True)
return serializer.data
def get_division_scheme(self, obj): # pylint: disable=unused-argument
"""Return the division scheme for the course."""
return self.context['course_discussion_settings'].division_scheme
def create(self, validated_data):
"""
Overridden create abstract method
"""
def update(self, instance, validated_data):
"""
Overridden update abstract method
"""
class UserStatsSerializer(serializers.Serializer):
"""
Serializer for course user stats.
"""
threads = serializers.IntegerField()
replies = serializers.IntegerField()
responses = serializers.IntegerField()
active_flags = serializers.IntegerField()
inactive_flags = serializers.IntegerField()
username = serializers.CharField()
def to_representation(self, instance):
"""Remove flag counts if user is not privileged."""
data = super().to_representation(instance)
if not self.context.get("is_privileged", False):
data["active_flags"] = None
data["inactive_flags"] = None
return data
class BlackoutDateSerializer(serializers.Serializer):
"""
Serializer for blackout dates.
"""
start = serializers.DateTimeField(help_text="The ISO 8601 timestamp for the start of the blackout period")
end = serializers.DateTimeField(help_text="The ISO 8601 timestamp for the end of the blackout period")
class ReasonCodeSeralizer(serializers.Serializer):
"""
Serializer for reason codes.
"""
code = serializers.CharField(help_text="A code for the an edit or close reason")
label = serializers.CharField(help_text="A user-friendly name text for the close or edit reason")
class CourseMetadataSerailizer(serializers.Serializer):
"""
Serializer for course metadata.
"""
id = CourseKeyField(help_text="The identifier of the course")
blackouts = serializers.ListField(
child=BlackoutDateSerializer(),
help_text="A list of objects representing blackout periods "
"(during which discussions are read-only except for privileged users)."
)
thread_list_url = serializers.URLField(
help_text="The URL of the list of all threads in the course.",
)
following_thread_list_url = serializers.URLField(
help_text="thread_list_url with parameter following=True",
)
topics_url = serializers.URLField(help_text="The URL of the topic listing for the course.")
allow_anonymous = serializers.BooleanField(
help_text="A boolean indicating whether anonymous posts are allowed or not.",
)
allow_anonymous_to_peers = serializers.BooleanField(
help_text="A boolean indicating whether posts anonymous to peers are allowed or not.",
)
user_roles = serializers.ListField(
child=serializers.CharField(),
help_text="A list of all the roles the requesting user has for this course.",
)
user_is_privileged = serializers.BooleanField(
help_text="A boolean indicating if the current user has a privileged role",
)
provider = serializers.CharField(
help_text="The discussion provider used by this course",
)
enable_in_context = serializers.BooleanField(
help_text="A boolean indicating whether in-context discussion is enabled for the course",
)
group_at_subsection = serializers.BooleanField(
help_text="A boolean indicating whether discussions should be grouped at subsection",
)
post_close_reasons = serializers.ListField(
child=ReasonCodeSeralizer(),
help_text="A list of reasons that can be specified by moderators for closing a post",
)
edit_reasons = serializers.ListField(
child=ReasonCodeSeralizer(),
help_text="A list of reasons that can be specified by moderators for editing a post, response, or comment",
)