Conditionally set FastAPI response model for route - python

I'm trying to return a list of objects of type Company, including only "approved" ones, and with more or less attributes depending on whether the user requesting the list is a superuser or a regular user. This is my code so far:
#router.get("/", response_model=List[schema.CompanyRegularUsers])
def get_companies(db: Session = Depends(get_db), is_superuser: bool = Depends(check_is_superuser)):
"""
If SU, also include sensitive data.
"""
if is_superuser:
return crud.get_companies_admin(db=db)
return crud.get_companies_user(db=db)
#
The function correctly returns the objects according to request (ie., only is_approved=True companies if a regular request, and both is_approved=True and is_approved=False if requested by a superuser. Problem is, both cases use schema.CompanyRegularUsers, and I'd like to use schema.CompanySuperusers when SU's make the request.
How can I achieve that feature? I.e, is there a way to conditionally set the response_model property of the decorator function?
I've tried using JSONResponse and calling Pydantic's schema.CompanySuperusers.from_orm(), but it won't work with a list of Companies...

I ended up solving the riddle by returning a custom JSONResponse. It doesn't show up in the automatic documentation, but I think I can tackle that down the road. Code is as follows, in case it helps someone else:
...
from pydantic import parse_obj_as
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
...
#router.get("/", response_model=List[schema.CompanyRegularUsers])
def get_companies(db: Session = Depends(get_db), is_superuser: bool = Depends(check_is_superuser)):
"""
If SU, also include sensitive data.
"""
if is_superuser:
companies = parse_obj_as(List[schema.CompanyAdmin], crud.get_companies_admin(db=db))
return JSONResponse(jsonable_encoder(companies))
return crud.get_companies_user(db=db)
So, in the is_admin branch, the path operation calls pydantic's parse_obj_as in order to map the list of objects returned by SQLAlchemy's query as a list of CompanyAdmin objects. Then, it uses jsonable_encoder, the encoder FastAPI uses under the hood for every default response, to serialize the list.
Edit: Typo

You can try to use Union type operator.
Your code would become
from typing import Union
#router.get("/", response_model=List[Union[schema.CompanyRegularUsers, schema.CompanySuperUser]])
this way, you specify as response model a list of either schema.CompanyRegularUsers or schema.CompanySuperUser
Let me know if it works, since I didn't test it

Related

Read String Array in Django API

I have EmberJS front-end which calls Django REST API. I am passing string array from front-end to the API as shown below:
Back-end Request
http://localhost:8000/api/v1/User/search/?active=true&city%5B%5D=newyork&city%5B%5D=chicago&city%5B%5D=boston
HTML Decoded
http://localhost:8000/api/v1/User/search/?active=true&city[]=newyork&city[]=chicago&city[]=boston
Some how I am not able to get rid of the extra %5B%5D (which is square brackets) from this request as EmberJS automatically adds that as the parameter is passed as string array. I searched over several forums but did not get a solution for it.
How to read the string array (City parameter) in Django API and get all passed values from EmberJS front-end?
You do not need to get rid of the []. The are good practice to indicate "I really do want to pass an array, i.e. multiple values for this query parameter".
The [] are presumably there because ember-data in your front end uses coalesceFindRequests. Dustin Farras (the main author of the ember-Django-adapter) has written an article about using his adapter with coalesceFindRequests: You can define a filter that covers [] and simply tell the django-rest-framework to use it:
# myapp/filters.py
from rest_framework import filters
class CoalesceFilterBackend(filters.BaseFilterBackend):
"""
Support Ember Data coalesceFindRequests.
"""
def filter_queryset(self, request, queryset, view):
id_list = request.query_params.getlist('ids[]')
if id_list:
# Disable pagination, so all records can load.
view.pagination_class = None
queryset = queryset.filter(id__in=id_list) # adapt here, see note below
return queryset
Now you just need to add this filter to filter_backends in your views, e.g.:
from myapp.filters import CoalesceFilterBackend
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
serializer = UserSerializer
filter_backends = (CoalesceFilterBackend,)
Or, configure it globally in your DRF settings:
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ('myapp.filters.CoalesceFilterBackend',) }
Note: You will need to adapt the filter for cities, of course. (When filtering for primary keys, I prefer to filter for pk__in= instead of id__in= that also covers primary keys with names other than id.)

How to pass optional keyword named group to views.py in Django?

I am working on a search filter of an ecommerce site.
Current Situation:
When user select each platform filter, the platform name will be appended to URL and display the filtered result.
My approach:
url.py
url(r'^search/(?P<product_slug>[0-9a-z-]+)$', CustomSearchView(), name='search_result_detail'),enter code here
url(r'^search/(?P<product_slug>[0-9a-z-]+)_(?P<platform_slug>[0-9a-z-]+)$', CustomSearchView(),
name='search_result_platform'),
url(r'^search/(?P<product_slug>[0-9a-z-]+)_(?P<platform_slug>[0-9a-z-]+)_(?P<platform_slug2>[0-9a-z-]+)$',
CustomSearchView(), name='search_result_platform2'),
url(r'^search/(?P<product_slug>[0-9a-z-]+)_(?P<platform_slug>[0-9a-z-]+)_'
r'(?P<platform_slug2>[0-9a-z-]+)_(?P<platform_slug3>[0-9a-z-]+)$',
CustomSearchView(), name='search_result_platform3'),
Main Question:
I didn't want to limit the filtering number. So if there are 20 platform filters, I need to create 20 URLs. Definitely it's not a smart way. Any other smart way to avoid creating a batches of URL?
views.py
def __call__(self, request, product_slug, platform_slug=None,platform_slug2=None,platform_slug3 = None
,platform_slug4 = None,platform_slug5 = None):
if platform_slug is None:
self.product_review_list = SearchResult.objects.filter(products__slug=product_slug)
else:
self.product_review_list = SearchResult.objects.filter(Q(products__slug=product_slug),
Q(platform__slug=platform_slug)|(Q(platform__slug=platform_slug2)|Q(platform__slug=platform_slug3)
|Q(platform__slug=platform_slug4)|Q(platform__slug=platform_slug5)))
As mentioned in the comments, this kind of filtering is a job for GET parameters, not paths.
The URL pattern should just be r'^search/$', and you call it via /search/?platform_slug=x&platform_slug=y&platform_slug=z. Then, in the view, you can just use __in to filter:
def custom_search_view(request):
product_review_list = SearchResult.objects.filter(products__slug__in=request.GET.getlist('platform_slug')
One other point. You must absolutely not use classes like this in your URLs and then set attributes on self. That is not threadsafe and will cause all sorts of problems. If you really need to use a class, subclass the generic View class and use it in the URLs as CustomSearchView.as_view(), then override get() or post().

Atomically Compare-Exchange a Model Field in Django

How can I atomically compare-exchange-save a value of Django Model instance Field? (Using PostgreSQL as the DB backend).
An example use case is making sure multiple posts with similar content (e.g. submits of the same form) take effect only once, without relying on insecure and only sometimes-working client-side javascript or server-side tracking of form UUIDs, which isn't secure against malicious multiple-posts.
For example:
def compare_exchange_save(model_object, field_name, comp, exch):
# How to implement?
....
from django.views.generic.edit import FormView
from django.db import transaction
from my_app.models import LicenseCode
class LicenseCodeFormView(FormView):
def post(self, request, ...):
# Get object matching code entered in form
license_code = LicenseCode.objects.get(...)
# Safely redeem the code exactly once
# No change is made in case of error
try:
with transaction.atomic()
if compare_exchange_save(license_code, 'was_redeemed', False, True):
# Deposit a license for the user with a 3rd party service. Raises an exception if it fails.
...
else:
# License code already redeemed, don't deposit another license.
pass
except:
# Handle exception
...
What you are looking for is the update function on a QuerySet object.
Depending on the value, you can do a comparison with Case, When objects - check out the docs on conditional updates NOTE that link is for 1.10 - Case/When came in in 1.8.
You might also find utility in using F which is used to reference a value in a field.
For example:
I need to update a value in my model Model:
(Model.objects
.filter(id=my_id)
.update(field_to_be_updated=Case(
When(my_field=True, then=Value(get_new_license_string()),
default=Value(''),
output_field=models.CharField())))
If you need to use an F object, just reference it on the right hand side of the equals in the update expression.
The update doesn't necessitate the use of transaction.atomic() context manager but if you need to do any other database operations you should continue to wrap that code with transaction.atomic()
Edit:
You may also like to use the queryset select_for_update method that implements row locks when the queryset is executed docs.

Using django-filter, why do unspecified or invalid lookup types return all results?

Here's a very simple Django Rest Framework/django-filter code example:
class MyModelFilter(django_filters.FilterSet):
class Meta:
model = MyModel
fields = {'my_str_field': ['exact']}
class MyModelList(generics.ListAPIView):
queryset = MyModel.objects.all()
filter_class = MyModelFilter
def get(self, request, format=None):
items = self.filter_queryset(self.queryset) # apply filters
serializer = MyModelSerializer(items, many=True)
return Response(serializer.data)
When I make this API call, the exact lookup type works as expected, returning matched objects:
/myobjects/?my_str_field=somevalue
If I use icontains, which as you see I did not specify as one of the supported lookup types, all objects are returned, as if the filter wasn't applied:
/myobjects/?my_str_field__icontains=this_can_be_anything
Furthermore, I can even use an invalid lookup type and there will be no error, with, again, all objects returned:
/myobjects/?my_str_field__this_can_be_anything=this_can_be_anything
This can obviously be misleading because a front-end developer who doesn't have access to the back-end code can happily think everything is fine and use the returned objects. I would expect, if not an error, at least an empty result set for the latter two cases. What am I doing wrong?
UPDATE: It appears that I should be using the strictness setting like so:
from django_filters.filterset import STRICTNESS
class MyModelFilter(django_filters.FilterSet):
# throw an exception on errors instead of returning empty results
strict = STRICTNESS.RAISE_VALIDATION_ERROR
class Meta:
model = MyModel
fields = {'my_str_field': ['exact']}
Unfortunately, this still doesn't result in an error, so my original question still stands.
If the server doesn't recognize a query string parameter, the typical behavior is to ignore that parameter. There's no standard or rfc that specifies how to handle unexpected query string parameters. But very many websites and web frameworks will liberally accept requests, and not perform any validation to reject requests that contain superfluous or misspelled query parameters.
In other words, this is not specific for Django Rest Framework.
This feature makes ajax cache busting possible. jQuery will add a parameter called _ with a random value to every single ajax request to make sure that the request has a unique url and is not cached anywhere. This would not work if the server returned an error on receiving an unexpected query parameter.

How to access the request.user in a Piston classmethod

I have a model which contains a ManyToMany to User to keep track of which users have 'favorited' a particular model instance.
In my API for this model, when requested by an authenticated user, I'd like to include an 'is_favorite' boolean. However, it seems that any api fields that aren't straight model attributes must be implemented as a class method, which when called in Piston does not get a reference to the request object, and therefore I have no way to know who the current user is.
From the Piston docs:
In addition to these, you may define any other methods you want. You can use these by including their names in the fields directive, and by doing so, the function will be called with a single argument: The instance of the model. It can then return anything, and the return value will be used as the value for that key.
So, if only the Piston CRUD methods get an instance of the request, how can my classmethod fields generate output which is relevant to the current authenticated user?
I am not aware of the piston API, but how about using the thread locals middleware to access the request
add this to middleware
try:
from threading import local
except ImportError:
from django.utils._threading_local import local
_thread_locals = local()
def get_request():
return getattr(_thread_locals, 'request', None)
class ThreadLocals(object):
def process_request(self, request):
_thread_locals.request = request
and update the settings with the ThreadLocals middleware
and wherever you want to access the request import get_request from middleware
if you want to just get the current user, modify the middleware to set only request.user in thread locals
From the piston wiki page it says that you may specify the contents of foreign keys and many to many fields by nesting attributes. In your case
class FriendHandler(BaseHandler):
allowed_methods = ('GET',)
model = User
fields = ('userfield_1', 'userfield_2', ('friends', ('is_friended')))
def read(self, request):
# Anything else you might want to do, but return an object of type User
# Or whatever your model happens to be called
EDIT: Another slightly hacky way to do it (if you don't want the friend to get passed at all if the is_friended is false) would be to manually create a dict object structured how you like, and then return it. piston processes the dict a works with the built in emitters (the JSON one for sure, haven't tried the others)

Categories

Resources