228 lines
7.9 KiB
Python
228 lines
7.9 KiB
Python
"""
|
|
Defines serializers used by the Team API.
|
|
"""
|
|
|
|
|
|
from copy import deepcopy
|
|
|
|
from django.conf import settings
|
|
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
|
from django_countries import countries
|
|
from rest_framework import serializers
|
|
|
|
from lms.djangoapps.teams.api import add_team_count, get_teams_accessible_by_user
|
|
from lms.djangoapps.teams.models import CourseTeam, CourseTeamMembership
|
|
from openedx.core.djangoapps.user_api.accounts.serializers import UserReadOnlySerializer
|
|
from openedx.core.lib.api.fields import ExpandableField
|
|
from openedx.core.lib.api.serializers import CollapsedReferenceSerializer
|
|
|
|
|
|
class CountryField(serializers.Field):
|
|
"""
|
|
Field to serialize a country code.
|
|
"""
|
|
|
|
COUNTRY_CODES = list(dict(countries).keys())
|
|
|
|
def to_representation(self, obj): # pylint: disable=arguments-differ
|
|
"""
|
|
Represent the country as a 2-character unicode identifier.
|
|
"""
|
|
return str(obj)
|
|
|
|
def to_internal_value(self, data):
|
|
"""
|
|
Check that the code is a valid country code.
|
|
|
|
We leave the data in its original format so that the Django model's
|
|
CountryField can convert it to the internal representation used
|
|
by the django-countries library.
|
|
"""
|
|
if data and data not in self.COUNTRY_CODES:
|
|
raise serializers.ValidationError(
|
|
f"{data} is not a valid country code"
|
|
)
|
|
return data
|
|
|
|
|
|
class UserMembershipSerializer(serializers.ModelSerializer):
|
|
"""Serializes CourseTeamMemberships with only user and date_joined
|
|
|
|
Used for listing team members.
|
|
"""
|
|
profile_configuration = deepcopy(settings.ACCOUNT_VISIBILITY_CONFIGURATION)
|
|
profile_configuration['bulk_shareable_fields'].append('url')
|
|
profile_configuration['public_fields'].append('url')
|
|
|
|
user = ExpandableField(
|
|
collapsed_serializer=CollapsedReferenceSerializer(
|
|
model_class=User,
|
|
id_source='username',
|
|
view_name='accounts_api',
|
|
read_only=True,
|
|
),
|
|
expanded_serializer=UserReadOnlySerializer(configuration=profile_configuration),
|
|
)
|
|
|
|
class Meta:
|
|
model = CourseTeamMembership
|
|
fields = ("user", "date_joined", "last_activity_at")
|
|
read_only_fields = ("date_joined", "last_activity_at")
|
|
|
|
|
|
class CourseTeamSerializer(serializers.ModelSerializer):
|
|
"""Serializes a CourseTeam with membership information."""
|
|
id = serializers.CharField(source='team_id', read_only=True) # pylint: disable=invalid-name
|
|
membership = UserMembershipSerializer(many=True, read_only=True)
|
|
country = CountryField()
|
|
|
|
class Meta:
|
|
model = CourseTeam
|
|
fields = (
|
|
"id",
|
|
"discussion_topic_id",
|
|
"name",
|
|
"course_id",
|
|
"topic_id",
|
|
"date_created",
|
|
"description",
|
|
"country",
|
|
"language",
|
|
"last_activity_at",
|
|
"membership",
|
|
"organization_protected",
|
|
)
|
|
read_only_fields = ("course_id", "date_created", "discussion_topic_id", "last_activity_at")
|
|
|
|
|
|
class CourseTeamCreationSerializer(serializers.ModelSerializer):
|
|
"""Deserializes a CourseTeam for creation."""
|
|
|
|
country = CountryField(required=False)
|
|
|
|
class Meta:
|
|
model = CourseTeam
|
|
fields = (
|
|
"name",
|
|
"course_id",
|
|
"description",
|
|
"topic_id",
|
|
"country",
|
|
"language",
|
|
"organization_protected",
|
|
)
|
|
|
|
def create(self, validated_data):
|
|
team = CourseTeam.create(
|
|
name=validated_data.get("name", ''),
|
|
course_id=validated_data.get("course_id"),
|
|
description=validated_data.get("description", ''),
|
|
topic_id=validated_data.get("topic_id", ''),
|
|
country=validated_data.get("country", ''),
|
|
language=validated_data.get("language", ''),
|
|
organization_protected=validated_data.get("organization_protected", False)
|
|
)
|
|
team.save()
|
|
return team
|
|
|
|
|
|
class CourseTeamSerializerWithoutMembership(CourseTeamSerializer):
|
|
"""The same as the `CourseTeamSerializer`, but elides the membership field.
|
|
|
|
Intended to be used as a sub-serializer for serializing team
|
|
memberships, since the membership field is redundant in that case.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
del self.fields['membership']
|
|
|
|
|
|
class MembershipSerializer(serializers.ModelSerializer):
|
|
"""Serializes CourseTeamMemberships with information about both teams and users."""
|
|
profile_configuration = deepcopy(settings.ACCOUNT_VISIBILITY_CONFIGURATION)
|
|
profile_configuration['bulk_shareable_fields'].append('url')
|
|
profile_configuration['public_fields'].append('url')
|
|
|
|
user = ExpandableField(
|
|
collapsed_serializer=CollapsedReferenceSerializer(
|
|
model_class=User,
|
|
id_source='username',
|
|
view_name='accounts_api',
|
|
read_only=True,
|
|
),
|
|
expanded_serializer=UserReadOnlySerializer(configuration=profile_configuration)
|
|
)
|
|
team = ExpandableField(
|
|
collapsed_serializer=CollapsedReferenceSerializer(
|
|
model_class=CourseTeam,
|
|
id_source='team_id',
|
|
view_name='teams_detail',
|
|
read_only=True,
|
|
),
|
|
expanded_serializer=CourseTeamSerializerWithoutMembership(read_only=True),
|
|
)
|
|
|
|
class Meta:
|
|
model = CourseTeamMembership
|
|
fields = ("user", "team", "date_joined", "last_activity_at")
|
|
read_only_fields = ("date_joined", "last_activity_at")
|
|
|
|
|
|
class BaseTopicSerializer(serializers.Serializer): # pylint: disable=abstract-method
|
|
"""Serializes a topic without team_count."""
|
|
description = serializers.CharField()
|
|
name = serializers.CharField()
|
|
id = serializers.CharField() # pylint: disable=invalid-name
|
|
type = serializers.CharField()
|
|
max_team_size = serializers.IntegerField()
|
|
|
|
|
|
class TopicSerializer(BaseTopicSerializer): # pylint: disable=abstract-method
|
|
"""
|
|
Adds team_count to the basic topic serializer, checking if team_count
|
|
is already present in the topic data, and if not, querying the CourseTeam
|
|
model to get the count. Requires that `context` is provided with a valid course_id
|
|
in order to filter teams within the course.
|
|
"""
|
|
team_count = serializers.SerializerMethodField()
|
|
|
|
def get_team_count(self, topic):
|
|
"""Get the number of teams associated with this topic"""
|
|
# If team_count is already present (possible if topic data was pre-processed for sorting), return it.
|
|
if 'team_count' in topic:
|
|
return topic['team_count']
|
|
else:
|
|
return get_teams_accessible_by_user(
|
|
self.context.get('user'),
|
|
[topic['id']],
|
|
self.context['course_id'],
|
|
self.context.get('organization_protection_status')
|
|
).count()
|
|
|
|
|
|
class BulkTeamCountTopicListSerializer(serializers.ListSerializer): # pylint: disable=abstract-method
|
|
"""
|
|
List serializer for efficiently serializing a set of topics.
|
|
"""
|
|
|
|
def to_representation(self, obj): # pylint: disable=arguments-differ
|
|
"""Adds team_count to each topic. """
|
|
data = super().to_representation(obj)
|
|
add_team_count(
|
|
self.context['request'].user,
|
|
data,
|
|
self.context['course_id'],
|
|
self.context.get('organization_protection_status')
|
|
)
|
|
return data
|
|
|
|
|
|
class BulkTeamCountTopicSerializer(BaseTopicSerializer): # pylint: disable=abstract-method
|
|
"""
|
|
Serializes a set of topics, adding the team_count field to each topic as a bulk operation.
|
|
Requires that `context` is provided with a valid course_id in order to filter teams within the course.
|
|
"""
|
|
class Meta:
|
|
list_serializer_class = BulkTeamCountTopicListSerializer
|