Django DRF: Schema for bulk creation api - django

I'm using django-rest-framework to build my API in which supports bulk create/update.
In these cases, the api will accept a list of object like
[
{"foo":"bar"},
{"foo":"bar"}
]
The code I'm using to allow bulk apis is just a small modification to add option many=True for serializer if the data is a list. It's like:
class FooViewSet(views.ModelViewSet):
def create(self, request, *args, **kwargs):
many = isinstance(request.data, list)
if many:
serializer = self.get_serializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
self.perform_bulk_create(serializer)
else:
................
I'm using drf_yasg for api doc generation.
But the problem is the schema generated keep detecting my request body just the single model only. Is there any config to make DRF schema generator knows that it will accept a list type?
Here is the schema which DRF generated
{
"post": {
"operationId": "foos_create",
"description": "",
"parameters": [
{
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Foo"
}
}
],
"responses": {
"201": {
"description": "",
"schema": {
"$ref": "#/definitions/Foo"
}
}
},
"tags": [
"foos"
]
}
}
My expectation is the schema would be the array type of Foo definition
Any help will be appreciated. Thanks for your time.

I know it very old post but I was facing a similar issue, and as a noob in DRF and python took a while to figure this stuff out.
I just had to add a simple decorator.
FYI I have used https://github.com/miki725/django-rest-framework-bulk for the bulk update.
#method_decorator(name='perform_bulk_create', decorator=swagger_auto_schema(
request_body=ContactSerializer(many=True),
operation_description="post list of contacts"
))

Related

Confused by Django Rest Framework Permissions

I have a complex set of permissions that I want to apply to my views, but I'm having a hard time understanding where to put them. Here are my basic permissions.
List all projects:
If the user is the owner and if their account is active
Or if they are a superuser
And the object is not private to a different user
Or they are a member of the assigned group
Problem is that when I add this logic to the permissions classes of the ModelApiViewset, it returns every project. How do I get it to show only the ones that meet the above criteria?
To further illustrate the example, I have 2 accounts right now: my superuser, and a test_user. test_user is not an admin or staff account, and is not part of the manager group. The test_user account is an owner of project 2, but not project 1. But if I use the test_user credentials, I see both project 1 and 2:
[
{
"id": 1,
"name": "Test Private Project",
"slug": "test-private-project",
"description": "Just testing the super private project",
"group": {
"name": "manager",
"id": 1
},
"created_date": "2020-04-20T18:04:20.666564Z",
"modified_date": "2020-04-20T18:04:20.666594Z",
"owner": {
"username": "admin",
"full_name_display": "Administrator",
"photo": null,
"is_active": true,
"id": 1
},
"is_private": true
},
{
"id": 2,
"name": "Test User Project",
"slug": "test-user-project",
"description": "Test project for users",
"group": {
"name": "Users",
"id": 2
},
"created_date": "2020-04-20T20:10:02.068390Z",
"modified_date": "2020-04-20T20:10:02.068429Z",
"owner": {
"username": "test_user",
"full_name_display": "Test User",
"photo": null,
"is_active": true,
"id": 2
},
"is_private": false
}
]
The test_user shouldn't see project 1 because they are not the owner, not an admin, and the project is private to the admin.
view:
class ProjectListViewSet(viewsets.ModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
authentication_classes = [TokenAuthentication, ]
permission_classes = [IsOwner, IsActive, IsPrivatelyOwned]
permissions:
class IsOwner(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.owner == request.user
class IsActive(BasePermission):
def has_object_permission(self, request, view, obj):
return request.user.is_active
class IsPrivatelyOwned(BasePermission):
def has_object_permission(self, request, view, obj):
if obj.is_private:
if obj.owner == request.user:
return True
return False
return True
I know I'm missing something pretty simple here, but I can't seem to figure it out. Any help would be greatly appreciated. Thanks in advance.
I think you misunderstand what permissions are. Permissions and filters are 2 different things. Permissions control the right to access objects in your database.
From the doc:
Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you require object-level filtering of list views, you'll need to filter the queryset separately. See the filtering documentation for more details.

Django RestFramework Elastic Search: Timeline API

I'm using django restframework along with elastic search to develop a backend application for a mobile app. I need to develop a timeline API that will load a timeline of posts from other users the user is following. Along with other related poststhat the people they're following may comment.
What is the best implementation method for this problem?
like this
#some_vf
def someview(req, **kw):
query = {
"_source": ["field1", "field2", "field3"],
"query": {
"must": [
{"term": {"username": "zhangsan"}}
],
"should": [
{"term": {"userid": 1}},
{"term": {"followed": "something"}} # other filter with zhangsan
]
},
"sort": [{"timestamp": {"order": "asc"}}],
"size": size,
"from": from_
}
es = Elasticsearch()
data = elasticsearch.helpers.scan(es, ["user_docs_index", "related_posts_index", "followed_some_index"], "_doc")
# or do your serializers
return Response(data)

Django 1.11 - make the whole site read-only for users from a given group (preferably with a fixture)

My question is not the same with View permissions in Django because that one explains how to deal with the issue in Django 2.1 and higher which has a concept of "view" permission while I am working with Django 1.1. which does not.
Django 1.11
I have a group users who should have only read-only access to everything on the site. No restrictions on fields, models, and actial data, only what they can do with it (read-only). I know about possible implementations that suggest doing it "field-by-field" (or "make all fields read-only") and "model-by-model" solution. I am curios if there is a way to do it cleaner, on user group level, or at least on user level.
My views.py so far is default:
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
#login_required
def index(request):
"""View function for home page of site."""
# Render the HTML template index.html with the data in the context variable
return render(request, 'home.html')
Ideally, I'd like to be able to do this with a fixture.
Currently in the fixture, I have my groups defined like this:
{
"model": "auth.group",
"fields": {
"name": "some_group",
"permissions": [
[
"add_somemodel",
"myproject",
"somemodel"
],
[
"change_somemodel",
"myproject",
"somemodel"
],
[
"delete_somemodel",
"myproject",
"somemodel"
]
]
}
}
In Django 2.2 I can do
{
"model": "auth.group",
"fields": {
"name": "some_group",
"permissions": [
[
"view_somemodel",
"myproject",
"somemodel"
]
]
}
}
but in Django 1.11 I have only "add", "delete" and "change" - no "view" option (according to the docs enter link description here). So, is there a way to create a fixture that creates a group that has only read permissions for everything?
In your view you need something like this(Notice that this is an example of how to view a post if you belong to the proper access group):
def post_detail(request, slug=None):
if not request.user.is_staff or not request.user.is_superuser:
raise Http404
instance = get_object_or_404(Post, slug=slug)
share_string = quote_plus(instance.content)
context = {
"title": instance.title,
"instance": instance,
"share_string": share_string,
}
return render(request, "post_detail.html", context)
Pay attention to:
if not request.user.is_staff or not request.user.is_superuser:
raise Http404
Here are some link to the docs that will help you:
How to authenticate users
All attributes to django.contrib.auth
Edit:
I saw your code now, so what you want to achieve can be done like this
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
#login_required
def index(request):
"""View function for home page of site."""
# With that way although a user might be logged in
# but the user might not have access to see the page
if not request.user.is_staff or not request.user.is_superuser:
raise Http404
# Render the HTML template index.html with the data in the context variable
return render(request, 'home.html')
That way a user may be logged in, but if it's not staff member or superuser it want have access to the page.
Thank you everybody for responding. I did not figure out the way to do it with only user/group/permissions config in the db with Django 1.11, maybe it does not exist. Here is what I ended up with (very similar to the first suggestion I hit on SO when I started the research 4 hours ago, with minimal code changes)
Create a fixture for my new group that contains only "change_somemodel" permission and created a user as a member of that group, i.e. no "add_somemodel" and no "delete_somemodel" entries and load it into DB:
[
{
"model": "auth.group",
"fields": {
"name": "<my_new_group>",
"permissions": [
[
"change_<somemodel1>",
"<myproject>",
"<somemodel1>"
],
[
"change_<somemodel2>",
"<myproject>",
"<somemodel2>"
]
]
}
,
{
"model": "auth.user",
"fields": {
"password": "<my_password>",
"last_login": null,
"is_superuser": false,
"username": "<my_username>",
"first_name": "",
"last_name": "",
"email": "",
"is_staff": true,
"is_active": true,
"date_joined": "2019-04-01T14:40:30.249Z",
"groups": [
[
"<my_new_group>"
]
],
"user_permissions": []
}
}
],
This took care of the first part: now when I login as this user I do not have "Add new.." or "Delete" buttons anywhere for any model for my user.
Now, when I load a view for a given instance of some model, I still have the fields editable and I still see "Save", "Save and Add Another" and "Save and Continue" buttons. To take care of this, in admin.py in the superclass from which all my models are subclassed, in its custom def changeform_view I added:
if request.user.groups.filter(name='<my_new_group>'):
extra_context['show_save_and_add_another'] = False
extra_context['show_save_and_continue'] = False
extra_context['show_save'] = False
This made those 3 "Save" buttons disappear for all the models and made all fields read-only. Now this new user can not add, delete or edit anything for any model yet they can see everything, just as I wanted.
Looks like in the newer Django, starting from 2.1, this can be done even better.

Show Filters and Ordering in Django Rest Framework Options Request

I'm using the Django Rest Framework I noticed on the web browseable part of the API there is a button called 'options' when clicked it shows the following...
HTTP 200 OK Vary: Accept Content-Type: text/html Allow: HEAD, GET, OPTIONS
{
"parses": [
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data"
],
"renders": [
"application/json",
"text/html"
],
"name": "Products",
"description": "API endpoint."
}
my question is, is there anyway I could list out here all the filter options an other stuff for this url?
You can make OPTIONS return whatever you want, by overriding the .metadata() method on the view.
See here: https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/views.py#L340
Update as of 2015: We now have a customizable metadata API that makes this easier: http://www.django-rest-framework.org/api-guide/metadata/
You can totally do this. Here's a custom metadata class that I've been keeping up to date here on StackOverflow. This simply lists all the available filters, their types, and their choices. It also lists the ordering fields that are available on a class:
class SimpleMetadataWithFilters(SimpleMetadata):
def determine_metadata(self, request, view):
metadata = super(SimpleMetadataWithFilters, self).determine_metadata(request, view)
filters = OrderedDict()
if not hasattr(view, 'filter_class'):
# This is the API Root, which is not filtered.
return metadata
for filter_name, filter_type in view.filter_class.base_filters.items():
filter_parts = filter_name.split('__')
filter_name = filter_parts[0]
attrs = OrderedDict()
# Type
attrs['type'] = filter_type.__class__.__name__
# Lookup fields
if len(filter_parts) > 1:
# Has a lookup type (__gt, __lt, etc.)
lookup_type = filter_parts[1]
if filters.get(filter_name) is not None:
# We've done a filter with this name previously, just
# append the value.
attrs['lookup_types'] = filters[filter_name]['lookup_types']
attrs['lookup_types'].append(lookup_type)
else:
attrs['lookup_types'] = [lookup_type]
else:
# Exact match or RelatedFilter
if isinstance(filter_type, RelatedFilter):
model_name = (filter_type.filterset.Meta.model.
_meta.verbose_name_plural.title())
attrs['lookup_types'] = "See available filters for '%s'" % \
model_name
else:
attrs['lookup_types'] = ['exact']
# Do choices
choices = filter_type.extra.get('choices', False)
if choices:
attrs['choices'] = [
{
'value': choice_value,
'display_name': force_text(choice_name, strings_only=True)
}
for choice_value, choice_name in choices
]
# Wrap up.
filters[filter_name] = attrs
metadata['filters'] = filters
if hasattr(view, 'ordering_fields'):
metadata['ordering'] = view.ordering_fields
return metadata
Put that somewhere in your project, then set your DEFAULT_METADATA_CLASS, and you should be all set, with a new key on your OPTIONS requests like so:
"filters": {
"sub_opinions": {
"type": "RelatedFilter"
},
"source": {
"type": "MultipleChoiceFilter",
"choices": [
{
"display_name": "court website",
"value": "C"
},
]
}
...more...
}
This will also display choices, mirroring the way it's handled elsewhere in DRF.

Right way to pass Django objects to ExtJS

Django has a built in serialization functionality which allows you to serialize any query result set into JSON:
json_serializer = serializers.get_serializer("json")()
json_serializer.serialize(queryset, ensure_ascii=False)
This produces output such as:
[
{
"pk": 1,
"model": "app_name.model_name",
"fields": {
"field_name": "value",
(...)
}
}
]
If you want to pass this JSON object over to an ExtJS driven application you run into a problem, because ExtJS expects its JSON to be formatted differently:
{
"total": 100,
"success": true,
"objects": [
{
"id": 1,
"field_name": "value",
(...)
}
]
}
There are 2 main differences: the additional meta-data (success, total) and the IDs of the objects which are provided together with other fields in Ext, but not in Django.
There are many possible ways to make one or the other format conform with the second, but what do you consider to be the best way to make this work? Is it a special serializer on the Django side, or maybe a special reader on the ExtJS side...
What do you think is the best way to solve this problem?
Better idea: use a custom serialiser.
settings.py
SERIALIZATION_MODULES = {
'extjson': 'extjs.serialiser.json'
}
extjs\serialiser\json.py
from django.core.serialisers.json import Serialiser
class ExtJSONSerialiser(Serializer)
"""
Serializes a QuerySet to basic Python objects.
"""
def end_object(self, obj):
self._current.update({"id": smart_unicode(obj._get_pk_val(), strings_only=True),})
self.objects.append(self._current)
self._current = None
def getvalue(self):
return {
'total': len(self.objects),
'success': True,
'objects': self.objects,
}
yourcode.py
json_serializer = serializers.get_serializer("extjson")()
json_serializer.serialize(queryset, ensure_ascii=False)
I found a solution which is able to serialize objects, which contain QuerySet as attributes. It's comes from traddicts.org blog, but you can now find it on GitHub:
https://github.com/datamafia/django-query-json-serializer/blob/master/JSONSerializer.py
I further modified the code to work recursively, so in effect I can do the following:
users = User.objects.all()
response = {}
response['success'] = True
response['users'] = users
json_serialize(response)
json_serialize(response, serialize_related=True)
json_serialize(response, serialize_related=True, ignored=['users.groups'])
json_serialize(response, serialize_related=True, ignored=['users.groups.permissions'])
I like your answer Thomas, but I needed something which would be even more flexible.