serializers.py 7.79 KB
Newer Older
1
"""Defines serializers used by the Team API."""
2
from copy import deepcopy
3 4

from django.conf import settings
5
from django.contrib.auth.models import User
6
from django.db.models import Count
7
from django_countries import countries
8
from rest_framework import serializers
9

10
from lms.djangoapps.teams.models import CourseTeam, CourseTeamMembership
11 12 13
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
14

15

16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
class CountryField(serializers.Field):
    """
    Field to serialize a country code.
    """

    COUNTRY_CODES = dict(countries).keys()

    def to_representation(self, obj):
        """
        Represent the country as a 2-character unicode identifier.
        """
        return unicode(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(
                u"{code} is not a valid country code".format(code=data)
            )
        return data


44 45 46 47 48
class UserMembershipSerializer(serializers.ModelSerializer):
    """Serializes CourseTeamMemberships with only user and date_joined

    Used for listing team members.
    """
49 50 51 52
    profile_configuration = deepcopy(settings.ACCOUNT_VISIBILITY_CONFIGURATION)
    profile_configuration['shareable_fields'].append('url')
    profile_configuration['public_fields'].append('url')

53 54 55 56 57 58 59
    user = ExpandableField(
        collapsed_serializer=CollapsedReferenceSerializer(
            model_class=User,
            id_source='username',
            view_name='accounts_api',
            read_only=True,
        ),
60
        expanded_serializer=UserReadOnlySerializer(configuration=profile_configuration),
61 62 63 64
    )

    class Meta(object):
        model = CourseTeamMembership
65 66
        fields = ("user", "date_joined", "last_activity_at")
        read_only_fields = ("date_joined", "last_activity_at")
67 68 69 70 71 72


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)
73
    country = CountryField()
74 75 76 77 78

    class Meta(object):
        model = CourseTeam
        fields = (
            "id",
79
            "discussion_topic_id",
80 81 82 83 84 85 86
            "name",
            "course_id",
            "topic_id",
            "date_created",
            "description",
            "country",
            "language",
87
            "last_activity_at",
88 89
            "membership",
        )
90
        read_only_fields = ("course_id", "date_created", "discussion_topic_id", "last_activity_at")
91 92 93 94 95


class CourseTeamCreationSerializer(serializers.ModelSerializer):
    """Deserializes a CourseTeam for creation."""

96 97
    country = CountryField(required=False)

98 99 100 101 102 103 104 105 106 107 108
    class Meta(object):
        model = CourseTeam
        fields = (
            "name",
            "course_id",
            "description",
            "topic_id",
            "country",
            "language",
        )

109 110 111 112 113 114 115 116
    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", ''),
117
        )
118 119
        team.save()
        return team
120 121


122 123 124 125 126 127 128 129 130 131 132 133
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(CourseTeamSerializerWithoutMembership, self).__init__(*args, **kwargs)
        del self.fields['membership']


134 135
class MembershipSerializer(serializers.ModelSerializer):
    """Serializes CourseTeamMemberships with information about both teams and users."""
136 137 138 139
    profile_configuration = deepcopy(settings.ACCOUNT_VISIBILITY_CONFIGURATION)
    profile_configuration['shareable_fields'].append('url')
    profile_configuration['public_fields'].append('url')

140 141 142 143 144 145 146
    user = ExpandableField(
        collapsed_serializer=CollapsedReferenceSerializer(
            model_class=User,
            id_source='username',
            view_name='accounts_api',
            read_only=True,
        ),
147
        expanded_serializer=UserReadOnlySerializer(configuration=profile_configuration)
148 149 150 151 152 153 154 155
    )
    team = ExpandableField(
        collapsed_serializer=CollapsedReferenceSerializer(
            model_class=CourseTeam,
            id_source='team_id',
            view_name='teams_detail',
            read_only=True,
        ),
156
        expanded_serializer=CourseTeamSerializerWithoutMembership(read_only=True),
157 158 159 160
    )

    class Meta(object):
        model = CourseTeamMembership
161 162
        fields = ("user", "team", "date_joined", "last_activity_at")
        read_only_fields = ("date_joined", "last_activity_at")
163 164


165 166
class BaseTopicSerializer(serializers.Serializer):
    """Serializes a topic without team_count."""
167 168 169
    description = serializers.CharField()
    name = serializers.CharField()
    id = serializers.CharField()  # pylint: disable=invalid-name
170 171 172 173


class TopicSerializer(BaseTopicSerializer):
    """
174 175 176 177
    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.
178
    """
179
    team_count = serializers.SerializerMethodField()
180 181 182

    def get_team_count(self, topic):
        """Get the number of teams associated with this topic"""
183 184 185 186 187
        # 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 CourseTeam.objects.filter(course_id=self.context['course_id'], topic_id=topic['id']).count()
188 189


190
class BulkTeamCountTopicListSerializer(serializers.ListSerializer):  # pylint: disable=abstract-method
191
    """
192
    List serializer for efficiently serializing a set of topics.
193
    """
194

195 196 197 198 199
    def to_representation(self, obj):
        """Adds team_count to each topic. """
        data = super(BulkTeamCountTopicListSerializer, self).to_representation(obj)
        add_team_count(data, self.context["course_id"])
        return data
200

201 202

class BulkTeamCountTopicSerializer(BaseTopicSerializer):  # pylint: disable=abstract-method
203
    """
204 205
    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.
206
    """
207
    class Meta(object):
208
        list_serializer_class = BulkTeamCountTopicListSerializer
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224


def add_team_count(topics, course_id):
    """
    Helper method to add team_count for a list of topics.
    This allows for a more efficient single query.
    """
    topic_ids = [topic['id'] for topic in topics]
    teams_per_topic = CourseTeam.objects.filter(
        course_id=course_id,
        topic_id__in=topic_ids
    ).values('topic_id').annotate(team_count=Count('topic_id'))

    topics_to_team_count = {d['topic_id']: d['team_count'] for d in teams_per_topic}
    for topic in topics:
        topic['team_count'] = topics_to_team_count.get(topic['id'], 0)