python - queryset - partial update django rest framework
Límite dinámico queryset del campo relacionado (8)
Utilizando Django REST Framework, quiero limitar qué valores se pueden usar en un campo relacionado en una creación.
Por ejemplo, considere este ejemplo (basado en el ejemplo de filtrado en http://django-rest-framework.org/api-guide/filtering.html , pero cambiado a ListCreateAPIView):
class PurchaseList(generics.ListCreateAPIView)
model = Purchase
serializer_class = PurchaseSerializer
def get_queryset(self):
user = self.request.user
return Purchase.objects.filter(purchaser=user)
En este ejemplo, ¿cómo me aseguro de que al momento de la creación, el comprador solo sea igual a self.request.user, y que este sea el único valor poblado en el menú desplegable del formulario en el renderizador API navegable?
Primero, asegúrese de que solo permita "self.request.user" cuando tenga un HTTP POST / PUT entrante (esto supone que la propiedad de su serializador y modelo se llama literalmente "usuario")
def validate_user(self, attrs, source):
posted_user = attrs.get(source, None)
if posted_user:
raise serializers.ValidationError("invalid post data")
else:
user = self.context[''request'']._request.user
if not user:
raise serializers.ValidationError("invalid post data")
attrs[source] = user
return attrs
Al agregar lo anterior a su modelo de serializador, se asegura de que SÓLO se inserte request.user en su base de datos.
2) -sobre su filtro anterior (filtro comprador = usuario), en realidad recomendaría usar un filtro global personalizado (para asegurar que esto se filtra globalmente). Hago algo para un software como una aplicación de servicio y ayuda a asegurar que cada solicitud http se filtre (incluso un http 404 cuando alguien intenta buscar un "objeto" al que no tiene acceso para verlo en primer lugar )
Hace poco parcheé esto en la rama maestra, así que tanto la lista como las vistas singulares filtrarán esto
https://github.com/tomchristie/django-rest-framework/commit/1a8f07def8094a1e34a656d83fc7bdba0efff184
3) - sobre el procesador de aplicaciones - ¿está haciendo que sus clientes lo usen directamente? si no, yo diría evitarlo. Si necesita esto, podría ser posible agregar un serializador personalizado que ayudaría a limitar la entrada en el front-end
Terminé haciendo algo similar a lo que Khamaileon sugirió aquí . Básicamente modifiqué mi serializador para echar un vistazo a la solicitud, que huele mal, pero hace el trabajo ... Así es como se ve (ejemplificado con el ejemplo de compra):
class PurchaseSerializer(serializers.HyperlinkedModelSerializer):
def get_fields(self, *args, **kwargs):
fields = super(PurchaseSerializer, self).get_fields(*args, **kwargs)
fields[''purchaser''].queryset = permitted_objects(self.context[''view''].request.user, fields[''purchaser''].queryset)
return fields
class Meta:
model = Purchase
allowed_objects es una función que toma un usuario y una consulta, y devuelve una consulta filtrada que solo contiene objetos que el usuario tiene permiso para vincular. Esto parece funcionar tanto para la validación como para los campos desplegables de la API navegable.
Escribí una clase personalizada CustomQueryHyperlinkedRelatedField para generalizar este comportamiento:
class CustomQueryHyperlinkedRelatedField(serializers.HyperlinkedRelatedField):
def __init__(self, view_name=None, **kwargs):
self.custom_query = kwargs.pop(''custom_query'', None)
super(CustomQueryHyperlinkedRelatedField, self).__init__(view_name, **kwargs)
def get_queryset(self):
if self.custom_query and callable(self.custom_query):
qry = self.custom_query()(self)
else:
qry = super(CustomQueryHyperlinkedRelatedField, self).get_queryset()
return qry
@property
def choices(self):
qry = self.get_queryset()
return OrderedDict([
(
six.text_type(self.to_representation(item)),
six.text_type(item)
)
for item in qry
])
Uso:
class MySerializer(serializers.HyperlinkedModelSerializer):
....
somefield = CustomQueryHyperlinkedRelatedField(view_name=''someview-detail'',
queryset=SomeModel.objects.none(),
custom_query=lambda: MySerializer.some_custom_query)
@staticmethod
def some_custom_query(field):
return SomeModel.objects.filter(somefield=field.context[''request''].user.email)
...
Hice lo siguiente:
class MyModelSerializer(serializers.ModelSerializer):
myForeignKeyFieldName = MyForeignModel.objects.all()
def get_fields(self, *args, **kwargs):
fields = super(MyModelSerializer, self).get_fields()
qs = MyModel.objects.filter(room=self.instance.id)
fields[''myForeignKeyFieldName''].queryset = qs
return fields
A petición @ gabn88, como ya sabrá, con DRF 3.0 y superior, no hay una solución fácil. Incluso si logra resolver una solución, no será bonita y probablemente fallará en las versiones posteriores de DRF, ya que anulará una fuente de DRF que habrá cambiado para entonces.
Olvidé la implementación exacta que utilicé, pero la idea es crear 2 campos en el serializador, uno en tu campo de serializador normal (digamos PrimaryKeyRelatedField, etc.) y otro campo en un campo de método de serialización, que los resultados se intercambiarán en ciertos casos (como basados en la solicitud, el usuario de la solicitud o lo que sea). Esto se haría en el constructor de serializadores (es decir: init )
Su campo de método de serializador devolverá una consulta personalizada que desee. Aparecerá y / o intercambiará estos campos, de modo que los resultados de su campo de método de serializador se asignarán al campo de serializador normal / predeterminado (PrimaryKeyRelatedField, etc.) en consecuencia. De esa manera siempre maneja esa única clave (su campo predeterminado) mientras que la otra clave permanece transparente dentro de su aplicación.
Junto con esta información, todo lo que realmente necesita es modificar esto: http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields
No me gustó el estilo de tener que anular el método init para cada lugar donde necesito tener acceso a datos de usuario o la instancia en tiempo de ejecución para limitar el conjunto de consultas. Así que opté por esta solución .
Aquí está el código en línea.
from rest_framework import serializers
class LimitQuerySetSerializerFieldMixin:
"""
Serializer mixin with a special `get_queryset()` method that lets you pass
a callable for the queryset kwarg. This enables you to limit the queryset
based on data or context available on the serializer at runtime.
"""
def get_queryset(self):
"""
Return the queryset for a related field. If the queryset is a callable,
it will be called with one argument which is the field instance, and
should return a queryset or model manager.
"""
# noinspection PyUnresolvedReferences
queryset = self.queryset
if hasattr(queryset, ''__call__''):
queryset = queryset(self)
if isinstance(queryset, (QuerySet, Manager)):
# Ensure queryset is re-evaluated whenever used.
# Note that actually a `Manager` class may also be used as the
# queryset argument. This occurs on ModelSerializer fields,
# as it allows us to generate a more expressive ''repr'' output
# for the field.
# Eg: ''MyRelationship(queryset=ExampleModel.objects.all())''
queryset = queryset.all()
return queryset
class DynamicQuersetPrimaryKeyRelatedField(LimitQuerySetSerializerFieldMixin, serializers.PrimaryKeyRelatedField):
"""Evaluates callable queryset at runtime."""
pass
class MyModelSerializer(serializers.ModelSerializer):
"""
MyModel serializer with a primary key related field to ''MyRelatedModel''.
"""
def get_my_limited_queryset(self):
root = self.root
if root.instance is None:
return MyRelatedModel.objects.none()
return root.instance.related_set.all()
my_related_model = DynamicQuersetPrimaryKeyRelatedField(queryset=get_my_limited_queryset)
class Meta:
model = MyModel
El único inconveniente con esto es que necesitaría establecer explícitamente el campo de serializador relacionado en lugar de utilizar el descubrimiento automático de campo proporcionado por ModelSerializer
. Sin embargo, esperaría que algo como esto esté en rest_framework por defecto.
Así es como lo hago:
class PurchaseList(viewsets.ModelViewSet):
...
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
context = self.get_serializer_context()
return serializer_class(*args, request_user=self.request.user, context=context, **kwargs)
class PurchaseSerializer(serializers.ModelSerializer):
...
def __init__(self, *args, request_user=None, **kwargs):
super(PurchaseSerializer, self).__init__(*args, **kwargs)
self.fields[''user''].queryset = User._default_manager.filter(pk=request_user.pk)
En django 3.0, se eliminó el método get_fields. Pero de manera similar, puede hacer esto en la función init del serializador:
class PurchaseSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Purchase
def __init__(self, *args, **kwargs):
super(PurchaseSerializer, self).__init__(*args, **kwargs)
if ''request'' in self.context:
self.fields[''purchaser''].queryset = permitted_objects(self.context[''view''].request.user, fields[''purchaser''].queryset)
Agregué el control if, ya que si usa PurchaseSerializer como campo en otro serializador en los métodos get, la solicitud no se pasará al contexto.