We have 2 approach ideas that we are trying to consider here for the Django REST api.
At the moment, we have implemented 1 filter option:
class APINewsViewSet(viewsets.ModelViewSet):
serializer_class = APINewsSerialiser
def get_queryset(self):
queryset = NewsBackup.objects.all()
source = self.request.query_params.get('source', None)
if source is not None:
queryset = queryset.filter(news_source = source)
return queryset
This achieves: 127.0.0.1/news?source=xxx
Something additional that we would like to do is have other filter options that the user can type in such as 127.0.0.1/news?source=xxx&sourcename=xxx which would then return them the JSON object with only data that has a source_id of xx and source_name of xx.
Is this possible with the Django REST framework? We have tried to add other options within the method:
query_params.get()
You can place the filters in a dictionary and then use the dictionary as the filter. Even better you can create a valid filters that safeguards invalid filters. Something like below:
# {'queryparam': 'db_field'}
valid_filters = {
'source': 'news_source',
'source_name': 'source_name',
}
filters = {valid_filters[key]: value for key, value in self.request.query_params.items() if key in valid_filters.keys()}
# So if the query_params is like this: source=xxx&source_name=xxx
# filters value will look something like below:
{
'news_source': 'xxx',
'source_name': 'xxx',
}
# **filters transforms the filters to: (news_source='xxx', source_name='xxx')
queryset = queryset.filter(**filters)
Or if you don't want to customize it heavily, you can use third party packages like Django Filter
UPDATE
If you want to get the multiple values for example: id=1&id=2, you can use getlist
ids = self.request.query_params.getlist('id')
# ids will have the list of id
# ids = [1, 2]
# you can then query the id
queryset = queryset.filter(id__in=ids)
UPDATE 2
If you want to support multiple filters, you can create a dictionary again:
keys = [
'id',
'source_name',
]
filters = {}
for key in keys:
filter = query_params.getlist(key)
if filter:
filters['{}__in'.format(key)] = filter
queryset = queryset.filter(**filters)
You can add the validation if you want.
Related
I have a normal Django view that returns the API for a query set. It takes query params from the URL and filters the database based on the parameters. It also outputs a maximum length of 3 "Part" objects.
I would like to add something so that it returns information on whether the queryset is clipped by the maximum length of 3. The idea is that since the inputs the query parameters, if the parameters are too vague, then there will be too much data being queried from the database. So it is clipped but then the user needs to know that it was clipped.
The current code looks like this
class PartList(generics.ListAPIView):
serializer_class = PartSerializer
def get_queryset(self):
"""
Optionally restricts the returned purchases to a given user,
by filtering against a `username` query parameter in the URL.
"""
queryset = Part.objects.all()
querydict = self.request.query_params
for (k, value) in querydict.items():
search_type = 'contains'
filter = k + '__' + search_type
queryset = queryset.filter(**{filter: value})
query_max_limit = 3
return queryset[:min(len(queryset), query_max_limit)]
You can try to fetch four elements, and in case it returns four, you display the first three, and specify that the data is clipped, like:
def get_queryset(self):
"""
Optionally restricts the returned purchases to a given user,
by filtering against a `username` query parameter in the URL.
"""
queryset = Part.objects.all()
querydict = self.request.query_params
for (k, value) in querydict.items():
search_type = 'contains'
filter = k + '__' + search_type
queryset = queryset.filter(**{filter: value})
query_max_limit = 3
qs = queryset[:query_max_limit+1]
self.clipped = clipped = len(qs) > query_max_limit
if clipped:
return list(qs)[:query_max_limit]
else:
return qs
So here the get_queryset will return a collection (not per se a QuerySet), containing at most three elements, and it will set an attribute self.clipped that specifies if the data was clipped.
Or a more elegant approach would be to first count, and then slice:
def get_queryset(self):
"""
Optionally restricts the returned purchases to a given user,
by filtering against a `username` query parameter in the URL.
"""
queryset = Part.objects.all()
querydict = self.request.query_params
for (k, value) in querydict.items():
search_type = 'contains'
filter = k + '__' + search_type
queryset = queryset.filter(**{filter: value})
query_max_limit = 3
qs = queryset[:query_max_limit+1]
self.clipped = clipped = qs.count() > query_max_limit
if clipped:
return queryset[:query_max_limit]
else:
return qs
It might be better to move this "clipping" logic to a dedicated function, and return if it is clipped, instead of setting an attribute.
It's perfectly fine to pass metadata along with your results, like so:
{
"is_clipped": true,
"results": [
…
]
}
Willem's answer is a good way to set is_clipped.
But I think you are interested in pagination, which is a standard way to communicate to clients that the results are clipped. It's possible combine your queryset filering with pagination. By the way, I suggest you use django-filter instead of rolling your own filtering.
As a Django newbie, I am trying to return JSON Objects from two models with each object containing the username, id, ticketID. Right now the code is simply putting the lists together without indexing. I should point out that there is a relationship between user and ticket so that can be traversed also.
{"username":"Paul","id":2}, {"username":"Paul","id":2}, {"username":"Ron","id":19}, {"id":"1c6f039c"}, {"id":"6480e439"},
{"id":"a97cf1s"}
class UsersforEvent(APIView):
def post(self, request):
body_unicode = request.body.decode('utf-8')
body = json.loads(body_unicode)
value = body['event']
queryset = Ticket.objects.filter(event = value)
referenced_users = User.objects.filter(ticket_owner__in=queryset.values('id'))
result_list = list(itertools.chain(referenced_users.values('username', 'id'), queryset.values('id')))
return Response((result_list))
You should do a single query to get the ticket with the related user for each one, then create the list of dicts from there. Assuming your Ticket model has an "owner" field which is a FK to User:
queryset = Ticket.objects.filter(event=value).select_related('owner')
result_list = [{'ticket_id': ticket.id, 'username': ticket.owner.username, 'user_id': ticket.owner.id}
for ticket in queryset]
(Note, this shouldn't be the action of a POST though; that is for changing data in the db, not querying.)
I have set up the column where one of the table is called Establishment_Type
Now, I am trying to filter according to Establishment_Type.
Here is my view.py code
class ShopDetailAPIView(ListAPIView):
serializer_class = ShopDetailSerializer
def get_queryset(self):
queryset = Shop.objects.all()
type = self.request.query_params.get('type', None)
type2 = self.request.query_params.get('type2', None)
if type is not None and type2 is None:
queryset = queryset.filter(Establishment_Type = type)
elif type is not None and type2 is not None:
queryset = queryset.filter(Q(Establishment_Type = type) | Q(Establishment_Type = type2))
return queryset
In the url, I query by typing:
http://127.0.0.1:8000/shop/search/?type=Restaurant&type2=Petrol%20Station
Which only filter Establishment_Type = Restaurant but not include Establishment_Type = Petrol Station
Here is my urls.py within my app called shop:
urlpatterns = [
url(r'^$', ShopListAPIView.as_view(), name = 'list' ),
#####
url(r'^create/$', ShopCreateAPIView.as_view(), name = 'create' ),
url(r'^search/$', ShopDetailAPIView.as_view(), name = 'detail'),
]
Was my url for filtering 2 Establishment type wrong?
Do I need to change something in my code in order to filter 2 values in column Establishment_Type?
Thanks #Thyrst' for advising me to use Establishment_Type__in=types
I have modify my code this way for my filter to work
class ShopDetailAPIView(ListAPIView):
serializer_class = ShopDetailSerializer
def get_queryset(self):
queryset = Shop.objects.all()
type = self.request.query_params.get('type', None)
type = type.split(',')
if type is not None:
queryset = queryset.filter(Establishment_Type__in = type)
return queryset
so type is now list, therefore when entered the url:
http://127.0.0.1:8000/shop/search/?type=Restaurant,Petrol%20Station
it filters according to Restaurant and Petrol Station.
it also works when entering just 1 value or more.
This is good for now but I feel like there might be a better way to implement this.
There is an even simpler way to achieve this using the django-filter package. Deep within the django-filter documentation, it mentions that you can use "a dictionary of field names mapped to a list of lookups".
Your code would be updated like so:
# views.py
from django_filters.rest_framework import DjangoFilterBackend
class ShopDetailAPIView(ListAPIView):
queryset = Shop.objects.all()
serializer_class = ShopDetailSerializer
filter_backends = [DjangoFilterBackend]
filter_fields = {
'type': ["in", "exact"]
}
Now in the URL you would add __in to the filter before supplying your list of parameters and it would work as you expect:
http://127.0.0.1:8000/shop/search/?type__in=Restaurant,Petrol%20Station
The django-filter documentation on what lookup filters are available is quite poor, but the in lookup filter is mentioned in the Django documentation itself.
I am trying to iterate over form results and I can't help but think that I am re-inventing the wheel here.
filterlist = []
if request.POST:
form = FilterForm(request.POST)
if form.is_valid():
for key, value in form.cleaned_data.iteritems():
filterlist.append(key)
filterlist.append(value)
This works, but seems very awkward and creates lots of other problems. For example the values come back with u' so I have to use value.encode("utf8") but then if a value is None it throws in error. So now I have to check if it is None, if not then encode. There has to be a better way.
EDIT: What I am trying to do.
I am trying to filter what is shown on a page. The problem I am running into is that if a value is empty (the user don't fill the box because they only want to filter against one object) then I get no results. For example a user wants to search for all books by the author name "Smith" but doesn't want to search against a genre.
results = Books.objects.filter(author=author, genre=genre)
The user would get no results because this is an AND search. But, if a user put in "Smith" for the author and "mystery" for the genre then it works exactly like I want it to, only giving results where both are true.
So, I am trying to eliminate the empty stuff by iterating over the form results. Like I said I am probably re-inventing the wheel here.
In Python 3 use:
for key, value in form.cleaned_data.items():
If the field names are the same in the model and the form, try this:
filter = {}
if request.method == 'POST':
form = FilterForm(request.POST)
if form.is_valid():
for key, value in form.cleaned_data.iteritems():
if value:
filter[key] = value
results = Books.objects.filter(**filter)
Python is one of the few languages having named parameters. You can assemble a dict with the non-empty form fields and pass it to the filter method using the kwargs unpacking operator **.
For example:
kwargs = {"author": "Freud"}
results = Books.objects.filter(**kwargs)
Is the same as:
results = Books.objects.filter(author="Freud")
I think the problem is that by default the Model form is not valid if a form field does not have a value entered by the user, if you don`t require the field every time from the user you need to set the required field to false in the ModelForm class in forms.py as shown in the code below. Remember that the field is set false only in the model form not in the model itself
class myForm(forms.ModelForm):
myfield_id = forms.CharField(required=False)
myfield_foo = forms.CharField(required=False)
myfield_bar = forms.CharField(required=False)
myfield_name = forms.CharField(required=False)
class Meta:
model = myModel
exclude = ('myfield_ex','myfield_file')
fields = ['myfield_id','myfield_foo','myfield_bar','myfield_name',]
After you have the form entered by the user what you need is use the Q object which can be used to create complex queries as described in the manula page here
https://docs.djangoproject.com/en/1.7/topics/db/queries/#complex-lookups-with-q
A simple example code would look like
if form.is_valid():
qgroup = []
for key,value in form.cleaned_data.iteritems():
if value:
q_name = Q(**{"%s"%format(filterKey[key]) : value})
qgroup.append(q_name)
q = None
# can use the reduce as shown here qgroup = reduce(operator.or_, (Q(**{"{0}".format(filterKey[key]): value}) for (key,value) in form.cleaned_data.iteritems()))
for key,value in form.cleaned_data.iteritems():
if value:
q_name = Q(**{"%s"%format(filterKey[key]) : value})
qgroup.append(q_name)
for x in qgroup:
q &= x ### Or use the OR operator or
if q:
resultL = myModel.objects.filter(q).select_related()
The filterKey can look something on the lines of
filterKey = {'myfield_id' : "myfield_id",
'myfield_foo' : "myfield_foo__icontains",
'myfield_bar' : "myfield_bar__relative_field__icontains",
}
My view.py is like this
def searchnews(request):
name = request.GET.get('name', 'default')
categry = request.GET.get('category', 'default')
NewTable.objects.filter(headline__contains=name).filter(category=categry)
What I want is that when the value of any element in querystring is empty
like
if category="" I want to get all the category result
You can make use of an ability to chain filters in Django.
Set the default to an empty string in case it is not found in request.GET. Check for not emptiness before adding an additional filter():
category = request.GET.get('category', '')
results = NewTable.objects.filter(headline__contains=name)
if category:
results = results.filter(category=category)
Another option is to build the filter as a kwargs dictionary
kwargs = {'headline__contains': name}
if 'category' in request.GET and request.GET['cateogry'] is not '':
kwargs['category': request.GET['category']
queryset = NewTable.objects.filter(**kwargs)
You can just make a condition to apply the filter only if a category is provided:
categry = request.GET.get('category', '')
queryset = NewTable.objects.filter(headline__contains=name)
if categry:
queryset = queryset.filter(category=categry)