dabapps / django-rest-framework-serialization-spec

DEPRECATED, see https://github.com/dabapps/django-readers instead
MIT License
11 stars 0 forks source link

serialization_spec is not expanded recursively #38

Open pmg103 opened 4 years ago

pmg103 commented 4 years ago

expand_nested_specs() only expands specs nested one level.

This means that if you use serialization_spec = in a SerializationSpecPlugin to specify the prefetching to be done, it will not be executed if you use this plugin below the top level.

pmg103 commented 4 years ago

Example for reference:

Here, we want to return the calculated list of available products on an activity's organisation:

class ActivitySummary(SerializationSpecMixin, generics.RetrieveAPIView):
    queryset = Activity.objects.all()

    serialization_spec = [
        'id',
        'name',
        {'organisation': [
            'id',
            'name',
            {'available_products': OrganisationAvailableProducts()},
        ]},

We try to implement that as a plugin:

class OrganisationAvailableProducts(SerializationSpecPlugin):
    """
    An organisations available products are a union of:
    1. All associated products which don't require accreditation
    2. All associated products which require accreditation, and at least 1 admin is accredited in
    """
    ADMIN_ROLES = [
        User.ROLE_CHOICES.ORGADMIN,
        User.ROLE_CHOICES.ACCOUNT_SUPERUSER,
        User.ROLE_CHOICES.ACCOUNT_ORG_ADMIN,
        User.ROLE_CHOICES.ACCOUNT_PROFESSIONAL_PRACTITIONER,
    ]

    serialization_spec = [
        {'_admins': Filtered('users', Q(groups__name__in=ADMIN_ROLES), [
            'id',
            {'accredited_products': [
                'id'
            ]}
        ])},
        {'_products': [
            'id',
            'requires_accreditation',
        ]}
    ]

    def get_value(self, instance):
        accredited_products = set(sum([[product.id for product in admin.accredited_products.all()] for admin in instance._admins], []))
        return [
            product.id
            for product in instance._products
            if not product.requires_accreditation or product.id in accredited_products
        ]

But sadly this does not work as this serialization_spec is two levels down and is not expanded so cannot be accessed inside our get_value(). You are forced to do the prefetch yourself which is error-prone:

class OrganisationAvailableProducts(SerializationSpecPlugin):
    """
    An organisations available products are a union of:
    1. All associated products which don't require accreditation
    2. All associated products which require accreditation, and at least 1 admin is accredited in
    """
    ADMIN_ROLES = [
        User.ROLE_CHOICES.ORGADMIN,
        User.ROLE_CHOICES.ACCOUNT_SUPERUSER,
        User.ROLE_CHOICES.ACCOUNT_ORG_ADMIN,
        User.ROLE_CHOICES.ACCOUNT_PROFESSIONAL_PRACTITIONER,
    ]

    def modify_queryset(self, queryset):
        return queryset.prefetch_related(
            Prefetch(
                'users',
                queryset=User.objects.filter(
                    groups__name__in=self.ADMIN_ROLES
                ).prefetch_related(
                    Prefetch('accredited_products', queryset=Product.objects.only('id'))
                ).only(
                    'accredited_products', 'organisation'
                ),
                to_attr='_admins'
            ),
            Prefetch(
                'products',
                queryset=Product.objects.only('id', 'requires_accreditation'),
                to_attr='_products'
            ),
        )

    def get_value(self, instance):
        accredited_products = set(sum([[product.id for product in admin.accredited_products.all()] for admin in instance._admins], []))
        return [
            product.id
            for product in instance._products
            if not product.requires_accreditation or product.id in accredited_products
        ]