serializers.py 7.79 KB
Newer Older
1
"""Defines serializers used by the Team API."""
2
from copy import deepcopy
3
from django.contrib.auth.models import User
4
from django.db.models import Count
5
from django.conf import settings
6

7
from django_countries import countries
8
from rest_framework import serializers
9

10
from openedx.core.lib.api.serializers import CollapsedReferenceSerializer
11
from openedx.core.lib.api.fields import ExpandableField
12
from openedx.core.djangoapps.user_api.accounts.serializers import UserReadOnlySerializer
13

14
from lms.djangoapps.teams.models import CourseTeam, CourseTeamMembership
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 44
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


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

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

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

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


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

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


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

97 98
    country = CountryField(required=False)

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

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


123 124 125 126 127 128 129 130 131 132 133 134
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']


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

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

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


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


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

    def get_team_count(self, topic):
        """Get the number of teams associated with this topic"""
184 185 186 187 188
        # 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()
189 190


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

196 197 198 199 200
    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
201

202 203

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


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)