It's a pretty standard task in Django REST Framework to supply additional args/kwargs to a serializer to set values of fields set not via request.data, but via the value in url parameters or cookies. For instance, I need to set user field of my Comment model equal to request.user upon POST request. Those additional arguments are called context.
Several questions (1, 2) on StackOverflow suggest that I override get_serializer_context() method of my ModelViewSet. I did and it doesn't help. I tried to understand, what's wrong, and found out that I don't understand from the source code, how this context system is supposed to work in general. (documentation on this matter is missing, too)
Can anyone explain, where serializer adds context to normal request data? I found two places, where it saves the values from context.
serializer.save(), method, which mixes kwargs with validated data, but it is usually called with no arguments (e.g. by ModelMixins).
fields.__new__(), which caches args and kwargs, but it seems that nobody ever reads them later.
Whenever you use generic views or viewsets, DRF(3.3.2) adds request object, view object and format to the serializer context. You can use serializer.context to access, lets say request.user in the serializer.
This is added when get_serializer_class() is called. Inside that, it calls get_serializer_context() method where all these parameters are added to its context.
DRF source code for reference:
class GenericAPIView(views.APIView):
"""
Base class for all other generic views.
"""
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
to set values of fields set not via request.data, but via the value in url parameters or cookies. For instance, I need to set user field of my Comment model equal to request.user upon POST request.
This is how I handle both cases in my ModelViewSet:
def perform_create(self, serializer):
# Get article id from url e.g. http://myhost/article/1/comments/
# obviously assumes urls.py is setup right etc etc
article_pk = self.kwargs['article_pk']
article = get_object_or_404(Article.objects.all(), pk=article_pk)
# Get user from request
serializer.save(author=self.request.user, article=article)
Unfortunately the nested objects is not standard for DRF but that's besides the point. :)
Related
(DRF v3.7, django-filters v1.1.0)
Hi! I have a working FilterSet that lets me filter my results via a query parameter, e.g. http://localhost:9000/mymodel?name=FooOnly
This is working just fine.
class MyNameFilter(FilterSet):
name = CharFilter(field_name='name', help_text='Filter by name')
class Meta:
model = MyModel
fields = ('name',)
class MyModel(...):
...
filter_backends = (DjangoFilterBackend,)
filter_class = MyNameFilter
But when I render the built-in auto-generated docs for my API, I am seeing this query parameter documented for all methods in my route, e.g. GET, PUT, PATCH, etc.
I only intend to filter via this query parameter for some of these HTTP verbs, as it doesn't make sense for others, e.g. PUT
Is there a good way to make my FilterSet conditional in this manner? Conditional on route method.
I tried applying this logic at both the Router level (a misguided idea). Also at the ViewSet level -- but there is no get_filter_class override method in the same way there is e.g. get_serializer_class.
Thanks for the help.
you'll get get_filter_class in DjangoFilterBackend. You need to create a new FilterBackend which overrides the filter_queryset method.
class GETFilterBackend(DjangoFilterBackend):
def filter_queryset(self, request, queryset, view):
if request.method == 'GET':
return super().filter_queryset(request, queryset, view)
return queryset
class MyModel(...):
...
filter_backends = (GETFilterBackend,)
filter_class = MyNameFilter
Figured this out, with help from Carlton G. on the django-filters Google Groups forum (thank you, Carlton).
My solution was to go up a level and intercept the CoreAPI schema that came out of the AutoSchema inspection, but before it made its way into the auto-generated docs.
At this point of interception, I override _allows_filters to apply only on my HTTP verbs of interest. (Despite being prefixed with a _ and thus intended as a private method not meant for overriding, the method's comments explicitly encourage this. Introduced in v3.7: Initially "private" (i.e. with leading underscore) to allow changes based on user experience.
My code below:
from rest_framework.schemas import AutoSchema
# see https://www.django-rest-framework.org/api-guide/schemas/#autoschema
# and https://www.django-rest-framework.org/api-guide/filtering/
class LimitedFilteringViewSchema(AutoSchema):
# Initially copied from lib/python2.7/site-packages/rest_framework/schemas/inspectors.py:352,
# then modified to restrict our filtering by query-parameters to only certain view
# actions or HTTP verbs
def _allows_filters(self, path, method):
if getattr(self.view, 'filter_backends', None) is None:
return False
if hasattr(self.view, 'action'):
return self.view.action in ["list"] # original code: ["list", "retrieve", "update", "partial_update", "destroy"]
return method.lower() in ["get"] # original code: ["get", "put", "patch", "delete"]
And then, at my APIView level:
class MyViewSchema(LimitedFilteringViewSchema):
# note to StackOverflow: this was some additional schema repair work I
# needed to do, again adding logic conditional on the HTTP verb.
# Not related to the original question posted here, but hopefully relevant
# all the same.
def get_serializer_fields(self, path, method):
fields = super(MyViewSchema, self).get_serializer_fields(path, method)
# The 'name' parameter is set in MyModelListItemSerializer as not being required.
# However, when creating an access-code-pool, it must be required -- and in DRF v3.7, there's
# no clean way of encoding this conditional logic, short of what you see here:
#
# We override the AutoSchema inspection class, so we can intercept the CoreAPI Fields it generated,
# on their way out but before they make their way into the auto-generated api docs.
#
# CoreAPI Fields are named tuples, hence the poor man's copy constructor below.
if path == u'/v1/domains/{domain_name}/access-code-pools' and method == 'POST':
# find the index of our 'name' field in our fields list
i = next((i for i, f in enumerate(fields) if (lambda f: f.name == 'name')(f)), -1)
if i >= 0:
name_field = fields[i]
fields[i] = Field(name=name_field.name, location=name_field.location,
schema=name_field.schema, description=name_field.description,
type=name_field.type, example=name_field.example,
required=True) # all this inspection, just to set this here boolean.
return fields
class MyNameFilter(FilterSet):
name = CharFilter(field_name='name', help_text='Filter returned access code pools by name')
class Meta:
model = MyModel
fields = ('name',)
class MyAPIView(...)
schema = MyViewSchema()
filter_backends = (DjangoFilterBackend,)
filter_class = MyNameFilter
I have a REST api url endpoint that represents a Song within an Album:
/api/album/(?P<album_id>)/song/(?P<id>)/
and I want to refer to it from another resource, e.g. Chart that contains Top-1000 songs ever. Here's an implementation of ChartSerializer:
class ChartSerializer(HyperlinkedModelSerializer):
songs = HyperlinkedRelatedField(
queryset=Song.objects.all(),
view_name='api:song-detail',
lookup_field='id'
)
class Meta:
model = Chart
fields = ('songs', )
Clearly, I can pass id as lookup_field, but it seems to me that I won't be able to pass album_id by any means. I'm looking into HyperlinkedModelSerializer.get_url() method:
def get_url(self, obj, view_name, request, format):
"""
Given an object, return the URL that hyperlinks to the object.
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
attributes are not configured to correctly match the URL conf.
"""
# Unsaved objects will not yet have a valid URL.
if hasattr(obj, 'pk') and obj.pk in (None, ''):
return None
lookup_value = getattr(obj, self.lookup_field)
kwargs = {self.lookup_url_kwarg: lookup_value}
return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
As you can see, it constructs kwargs for reverse url lookup from scratch and doesn't allow to pass additional parameters to it. Am I right that this is not supported?
UPDATE:
Found a reference to this problem in the issue list of DRF: https://github.com/tomchristie/django-rest-framework/issues/3204
So, the answer is YES. There is even a paragraph about this issue in the DRF documentation:
http://www.django-rest-framework.org/api-guide/relations/#custom-hyperlinked-fields
I'm using TurboGears 2.3 and working on validating forms with formencode and need some guidance
I have a form which covers 2 different objects. They are a almost the same, but with some difference
When i submit my form, I want to validate 2 things
Some basic data
Some specific data for the specific object
Here are my schemas:
class basicQuestionSchema(Schema):
questionType = validators.OneOf(['selectQuestion', 'yesNoQuestion', 'amountQuestion'])
allow_extra_fields = True
class amount_or_yes_no_question_Schema(Schema):
questionText = validators.NotEmpty()
product_id_radio = object_exist_by_id(entity=Product, not_empty=True)
allow_extra_fields = True
class selectQuestionSchema(Schema):
questionText = validators.NotEmpty()
product_ids = validators.NotEmpty()
allow_extra_fields = True
And here are my controller's methods:
#expose()
#validate(validators=basicQuestionSchema(), error_handler=questionEditError)
def saveQuestion(self,**kw):
type = kw['questionType']
if type == 'selectQuestion':
self.save_select_question(**kw)
else:
self.save_amount_or_yes_no_question(**kw)
#validate(validators=selectQuestionSchema(),error_handler=questionEditError)
def save_select_question(self,**kw):
...
Do stuff
...
#validate(validators=amount_or_yes_no_question_Schema(),error_handler=questionEditError)
def save_amount_or_yes_no_question(self,**kw):
...
Do other stuff
...
What I wanted to do was validate twice, with different schemas. This doesn't work, as only the first #validate is validated, and the other are not (maybe ignored)
So, what am i doning wrong?
Thanks for the help
#validate is part of the request flow, so when manually calling a controller it is not executed (it is not a standard python decorator, all TG2 decorators actually only register an hook using tg.hooks so they are bound to request flow).
What you are trying to achieve should be done during validation phase itself, you can then call save_select_question and save_amount_or_yes_no_question as plain object methods after validation.
See http://runnable.com/VF_2-W1dWt9_fkPr/conditional-validation-in-turbogears-for-python for a working example of conditional validation.
Part 1:
I have a call to layout(:default){|path,wish| wish !~ /rss|atom|json/} but requests to /foo/bar.json seem to think wish is html and uses the layout anyway. How can I fix this?
Part 2:
I want to route /path/to/file.ext so that it calls the method to on the controller mapped to /path and uses ext when formulating the return. Is there a better (more elegant) way to do this than passing the 'file.ext' to the to method, parsing it, and doing cases? This question would have been more succinct if I had written, how does one do REST with Ramaze? There appears to be a Google Groups answer to this one, but I can't access it for some reason.
class ToController < Controller
map '/path/to'
provide( :json, :type => "application/json") { |action, val| val.to_json }
def bar
#barInfo = {name: "Fonzie's", poison: "milk"}
end
end
This controller returns plain JSON when you request /path/to/bar.json and uses the layout+view wrapping when you request /path/to/bar (Ramaze has no default layout setting, the layout in this example comes from the Controller parent class).
I have a simple view that I'm using to experiment with AJAX.
def get_shifts_for_day(request,year,month,day):
data= dict()
data['d'] =year
data['e'] = month
data['x'] = User.objects.all()[2]
return HttpResponse(simplejson.dumps(data), mimetype='application/javascript')
This returns the following:
TypeError at /sched/shifts/2009/11/9/
<User: someguy> is not JSON serializable
If I take out the data['x'] line so that I'm not referencing any models it works and returns this:
{"e": "11", "d": "2009"}
Why can't simplejson parse my one of the default django models? I get the same behavior with any model I use.
You just need to add, in your .dumps call, a default=encode_myway argument to let simplejson know what to do when you pass it data whose types it does not know -- the answer to your "why" question is of course that you haven't told poor simplejson what to DO with one of your models' instances.
And of course you need to write encode_myway to provide JSON-encodable data, e.g.:
def encode_myway(obj):
if isinstance(obj, User):
return [obj.username,
obj.firstname,
obj.lastname,
obj.email]
# and/or whatever else
elif isinstance(obj, OtherModel):
return [] # whatever
elif ...
else:
raise TypeError(repr(obj) + " is not JSON serializable")
Basically, JSON knows about VERY elementary data types (strings, ints and floats, grouped into dicts and lists) -- it's YOUR responsibility as an application programmer to match everything else into/from such elementary data types, and in simplejson that's typically done through a function passed to default= at dump or dumps time.
Alternatively, you can use the json serializer that's part of Django, see the docs.