I want to build a REST API where user can do operations on objects, based on their permissions. Consider a record that represents a car - it contains the license number, the type of the car and extra information. Also consider the following user system:
Owners - Who own the car object. Can modify it and delete it.
Editors - Who can only modify the object properties.
Viewers - Can only view the object properties.
Each record can contain multi owners/editors/viewers (The user who created the object should be automatically the owner). Also, owners can add or remove editors/viewers. In my head, I see it as a list of owners/editors/viewers.
So in case of a GET request, I want to be able to return all objects that the user has permissions for, separated into those three categories.
So under my api app, I have the following code:
The models.py file contains:
class CarRecord(models.Model):
type = models.CharField(max_length=50)
license = models.CharField(max_length=50)
The serializers.py file contains:
class CarRecordSerializer(serializers.ModelSerializer):
type = models.CharField(max_length=50)
license = models.CharField(max_length=50)
class Meta:
model = CarRecord
fields = ('__all__')
In view.py I have:
class CarRecordViews(APIView):
def get(self, request):
if not request.user.is_authenticated:
user = authenticate(username=request.data.username, password=request.data.password)
if user is not None:
return Response(data={"error": "invalid username/password"}, status=status.HTTP_401_UNAUTHORIZED)
# return all records of cars that user some type of permission for
Now, I want to get all the records of user that he has permissions to query (along with their permission type). I thought of adding a three extra fields under CarRecord - each one is a list of users that contains that permission type. But I'm not sure if it's the "Django way". So wanted to consult first with SO.
EDIT: I tried to add the following field to my CarRecord class:
owners = models.ManyToManyField(User, related_name='car_owners', verbose_name=('owners'), default=[])
Also I added:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['username']
lass CarRecordSerializer(serializers.ModelSerializer):
type = models.CharField(max_length=50)
license = models.CharField(max_length=50)
owners = UserSerializer(many=True)
class Meta:
model = CarRecord
fields = ('__all__')
And the way I create the CarRecordSerializer instance is:
serializer = CarRecordSerializer(data=request.data)
But I get:
{
"error": {
"owners": [
"This field is required."
]
}
}
How to make it work? I guess is my problem is how to serialize a ManyToMany object?
EDIT2: My second attempt is:
class CarRecord(models.Model):
date_created = models.DateTimeField()
type = models.CharField(max_length=50)
license = models.CharField(max_length=50)
owners = models.ManyToManyField(User, related_name='car_owners', verbose_name=('owners'), default=[]))
creator = models.ForeignKey(User,on_delete=models.DO_NOTHING)
# ...
class CarRecordSerializer(serializers.ModelSerializer):
date_created = serializers.DateTimeField(default=datetime.now(timezone.utc))
type = models.CharField(max_length=50)
license = models.CharField(max_length=50)
creator = serializers.StringRelatedField()
owners = serializers.PrimaryKeyRelatedField(many=True,read_only=True)
class Meta:
model = CarRecord
fields = '__all__'
def create(self, validated_data):
self.owners = [self.context['creator']]
record = CarRecord(**validated_data, creator=self.context['creator'])
record.save()
return record
# ...
# In post method:
serializer = CarRecordSerializer(data=request.data, context={ 'creator': user })
But now, in GET method, I filter the owners list with the user and it can't find the objects:
> CarRecord.objects.filter(owners=user)
<QuerySet []>
Also, in the Admin section I see that all of the objects automatically have all the users in the owners/editors/viewers lists. Why is that? Owners should contain only the user that created the record and editors and viewers should be empty lists. In another query, owner can add additional owners/editors/viewers.
Here is the solution I might think is the right one
class CarRecord(models.Model):
date_created = models.DateTimeField(auto_now_add=True)
type = models.CharField(max_length=50)
license = models.CharField(max_length=50)
owners = models.ManyToManyField(User, related_name='car_owners')
creator = models.ForeignKey(User,on_delete=models.DO_NOTHING)
class CarRecordSerializer(serializers.ModelSerializer):
creator = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), required=False)
owners_details = UserSerializer(source='owners', many=True, read_only=True)
class Meta:
model = CarRecord
fields = '__all__'
def create(self, validated_data):
try:
new_owners = validated_data.pop('owners')
except:
new_owners = None
car_record = super().create(validated_data)
if new_owners:
for new_owner in new_owners:
car_record.owners.add(new_owner)
return car_record
In views.py
from rest_frameword import generics
from rest_framework import permissions
class CustomCarRecordPermissions(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method == 'GET':
return True
elif request.method == 'PUT' or request.method == 'PATCH':
return request.user == obj.creator or request.user in obj.owners.all()
elif request.method == 'DELETE':
return request.user == obj.creator
return False
class CarRecordListCreate(generics.ListCreateAPIView):
permission_classes = (IsAuthenticated, )
serializer_class = CarRecordSerializer
queryset = CarRecord.objects.all()
def post(self, request, *args, **kwargs):
request.data['creator'] = request.user.id
return super().create(request, *args, **kwargs)
class CarRecordDetailView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (CustomCarRecordPermissions, )
serializer_class = CarRecordSerializer
lookup_field = 'pk'
queryset = CarRecord.objects.all()
models is self explanatory;
In CarRecord serializers we set creator as required False and primary key related field so that we can supply request user id before create as shown in views.py post method.
In Detail view we set our custom permission; If the request is GET we allow permissions. But if the request is PUT or PATCH the owners and the creator are allowed. But if it is a delete request only creator is allowed.
I think the django-rest-framework-guardian package fits here. This package is based on django-guardian.
django-guardian is an implementation of object permissions for Django providing an extra authentication backend.
There is no change on your models.py
You should change serializers.py and views.py.
For example, your serializer should look like this
from rest_framework_guardian.serializers import ObjectPermissionsAssignmentMixin
class CarRecordSerializer(ObjectPermissionsAssignmentMixin, serializers.ModelSerializer):
type = models.CharField(max_length=50)
license = models.CharField(max_length=50)
class Meta:
model = CarRecord
fields = ('__all__')
def get_permissions_map(self, created):
current_user = self.context['request'].user
readers = Group.objects.get(name='readers')
editors = Group.objects.get(name='editors')
owners = Group.objects.get(name='owners')
return {
'view_car_record': [current_user, readers, owners],
'change_car_record': [current_user, editors],
'delete_car_record': [current_user, owners]
}
and your views should look like this:
from rest_framework_guardian import filters
class CarRecordModelViewSet(ModelViewSet):
queryset = CarRecord.objects.all()
serializer_class = CarRecordSerializer
filter_backends = [filters.ObjectPermissionsFilter]
Edit settings.py like this:
INSTALLED_APPS = [
'rest_framework',
'guardian',
]
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"guardian.backends.ObjectPermissionBackend",
]
You can define filter backends globally in your settings, too:
REST_FRAMEWORK = {
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework_guardian.filters.ObjectPermissionsFilter",
],
}
Don't forget! If you define the ObjectPermissionsFilter in the settings.py, your all views are affected by this filter.
If you want to restrict post request per user, you shoul implement custom permission class, like this:
from rest_framework import permissions
class CustomObjectPermissions(permissions.DjangoObjectPermissions):
"""
Similar to `DjangoObjectPermissions`, but adding 'view' permissions.
"""
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
'HEAD': ['%(app_label)s.view_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
Check this link to get the detailed information for the CustomObjectPermissions
You can write permission class car owner user.
Your model.
class CarRecord(models.Model):
date_created = models.DateTimeField()
type = models.CharField(max_length=50)
license = models.CharField(max_length=50)
owners = models.ManyToManyField(User, related_name='car_owners', verbose_name=('owners'), default=[]))
creator = models.ForeignKey(User,on_delete=models.DO_NOTHING)
Permission class permission.py
from rest_framework.permissions import BasePermission,
from cars.models import CarRecord
class isCarAccess(BaseCommand):
def has_permission(self, request, view):
if request.method == 'OPTIONS':
return True
check_user = CarRecord.objects.filter(owners__in=[request.user])
return request.user is not None and request.user.is_authenticated and check_user
this permission class will check that does user exists, user is authenticated and as well the user belongs to the card record or not.
And you can pass this permission in your view.
from .permission import isCarAccess
from .models import CarRecord
class CarRecordViews(APIView):
permission_classes = [isCarAccess]
def get(self, request):
car_record = CarRecord.objects.filter(owners__in=[request.user])
# return all records of cars that user some type of permission for
and your settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"oauth2_provider.contrib.rest_framework.OAuth2Authentication",
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
),
}
Related
I am trying to get current logged in user through my model so that I can only see the current user in my order page dropdown:
I have gone through a lot of documents which state that it is not that easy or feasible to get current logged in user in model.
I have tried other method like getting AUTH_USER_MODEL but it is returning admin level users as well so not solving the problem.
I am also sending the current logged in user from my views file but dont know how to access it inside form class, able to access it in init but dont know how it can be accessed in class.
models.py :
from django.db import models
from django.contrib.auth.models import User
from django.conf import settings
from django.contrib.auth import get_user_model
from django.http import HttpResponse,HttpRequest
class Customer(models.Model):
name = models.CharField(max_length=200,null=True)
user = models.OneToOneField(User, null=True, on_delete=models.CASCADE)
date_created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Order(models.Model):
product = models.ForeignKey(Product,null=True, on_delete=models.SET_NULL)
#customer = models.ForeignKey(settings.AUTH_USER_MODEL,null=True, on_delete=models.SET_NULL)
customer = models.ForeignKey(Customer,null=True, on_delete=models.SET_NULL)
date_ordered = models.DateTimeField(auto_now_add=True)
status = models.CharField(max_length=20,default= 'PENDING')
def __str__(self):
return str(self.customer.id)
forms.py :
class createorderform(ModelForm):
def __init__(self,*args,**kwargs):
self._instance=kwargs.pop('instance',None)
super().__init__(*args,**kwargs)
#def __init__(self,instance):
# self.user = instance
# super().__init__(instance)
class Meta:
model=Order
fields="__all__"
exclude=['status']
Views.py
def placeorder(request,i):
try:
products = Product.objects.all()
customer = Customer.objects.get(id=i)
print("Customer:",customer)
#form=createorderform(prod=products,cust=customer)
form=createorderform(instance=customer)
#form=createorderform()
if(request.method=='POST'):
form=createorderform(request.POST,instance=customer)
if(form.is_valid()):
form.save()
return redirect('/')
context={'form':form}
return render(request,'app_name/placeorder.html',context)
except:
print("Error occurred : {exec}".format(exec=traceback.format_exc()))
What I am getting is all the users:
What I want is to only show the current user in drop down.
Please help or guide me in the right direction.
Thanks in advance!!
Something like
from django.forms import ModelForm
from django.views.generic import CreateView
class CreateOrderForm(ModelForm):
class Meta:
model = Order
exclude = ['status', 'customer']
def __init__(self, **kwargs):
self.customer = kwargs.pop('customer')
super().__init__(**kwargs)
def save(self, commit=True):
self.instance.customer = self.customer
return super().save(commit=commit)
class PlaceOrderView(CreateView):
model = Order
form_class = CreateOrderForm
template_name = 'app_name/placeorder.html'
def get_form_kwargs(self):
return {
**super().get_form_kwargs(),
'customer': Customer.objects.get(user=self.request.user),
}
should be enough.
So I have kind of tried one solution and this one work, although I have tried it earlier but in between 100's of documents and solutions the easy one got lost :
In my forms.py :
class createorderform(ModelForm):
def __init__(self,*args,**kwargs):
self._instance=kwargs.pop('instance',None)
super().__init__(*args,**kwargs)
#def __init__(self,instance):
# self.user = instance
# super().__init__(instance)
class Meta:
model=Order
fields="__all__"
exclude=['customer','status']
I excluded the customer field and instead populated it in my views. py:
def placeorder(request,i):
try:
products = Product.objects.all()
customer = Customer.objects.get(id=i)
print("Customer:",customer)
#form=createorderform(prod=products,cust=customer)
form=createorderform(instance=customer)
#form=createorderform()
if(request.method=='POST'):
form=createorderform(request.POST,instance=customer)
if(form.is_valid()):
curr_user = form.save(commit=False)
curr_user.customer = customer
curr_user.save()
return redirect('/')
context={'form':form}
return render(request,'app_name/placeorder.html',context)
except:
print("Error occurred : {exec}".format(exec=traceback.format_exc()))
So I am overriding the save() and building my current logged in user in the view itself while saving it. It's storing the same user in db as well.
EDIT:
def placeorder(request,i):
try:
products = Product.objects.all()
customer = Customer.objects.get(id=i)
print("Customer:",customer)
#form=createorderform(prod=products,cust=customer)
form=createorderform()
#form=createorderform(instance=customer)
if(request.method=='POST'):
form=createorderform(request.POST)
#form=createorderform(request.POST,instance=customer)
if(form.is_valid()):
curr_user = form.save(commit=False)
curr_user.customer = customer
curr_user.save()
return redirect('/')
context={'form':form}
return render(request,'app_name/placeorder.html',context)
except:
print("Error occurred : {exec}".format(exec=traceback.format_exc()))
So I have change my form object and I am not sending my current logged in user to forms any more so just creating the object without "instance=customer" which got my current logged in user.
Now we don't need to handle the instance in init method of createorderform form :
class createorderform(ModelForm):
#def __init__(self,*args,**kwargs):
# self._instance=kwargs.pop('instance',None)
# super().__init__(*args,**kwargs)
#def __init__(self,instance):
# self.user = instance
# super().__init__(instance)
class Meta:
model=Order
fields="__all__"
exclude=['customer','status']
I am trying to update my data in 'VoterList' model by using PUT api, but i don't know which function should i use in my 'views.py' file to handle the coming PUT request because in PUT api, we use parameters from URL to pick the relevent entry from model for updation and then update it by using data received from PUT api.
model.py
class VoterList(models.Model):
# id = models.IntegerField(auto_created= True, primary_key=True)
name = models.CharField( max_length=20)
email = models.EmailField()
mobile = models.IntegerField()
city = models.CharField( max_length=20)
type = models.CharField(max_length=20)
def __str__(self):
return self.name
serializers.py
class FillVoterListSerializers(serializers.HyperlinkedModelSerializer):
class Meta:
model = VoterList
fields = ('id','name', 'email', 'mobile', 'city', 'type')
def update(self, instance, validated_data):
instance.name = validated_data.pop("name", instance.name)
instance.email = validated_data.pop("email", instance.email)
instance.save()
return instance
I will manage the code for PUT in serializers by myself.
views.py
class UpdateVoter(APIView):
serializer_class = FillVoterListSerializers
permission_classes = (AllowAny,)
def post(self, request,*args,**kwargs):
isDataExist = VoterList.objects.get(id=request.data.get('id'))
if not isDataExist:
return Response({"message":"No Voter exist with this id."})
else:
isDataUpdated = self.serializer_class(isDataExist, request.data, partial=True)
if isDataUpdated.is_valid():
isDataUpdated.save()
return Response({"message": "Voter updated."})
else:
return Response({"message": "All fields are Mandatory."})
urls.py
urlpatterns = [
url('api/updateVoter/(?P<id>[0-9]+)/$', UpdateVoter.as_view(), name= "updateVoter")]
So what code should i write in my view.py to handle the PUT request.
Note: I want to tell you that i am preparing api for mobile applications, so please respond accordingly.
Any help is appreciated.
You can use the put() function in your view similar to the post() which you've used
def put(self, request, pk, format=None):
# Your code here
Refer the DRF docs : https://www.django-rest-framework.org/tutorial/3-class-based-views/
Let's say I have three models as:
class User(AppModel):
name = models.CharField(max_length=255)
class Business(AppModel):
owner = models.ForeignKey("User", related_name="businesses", on_delete=models.CASCADE)
legal_name = models.CharField(max_length=255)
class Invoice(AppModel):
business = models.ForeignKey("Business", related_name="invoices", on_delete=models.CASCADE)
amount = models.integerField()
As you can see, a user can have multiple businesses and a business can have multiple invoices.
My serializers.py:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields= ('name')
class BusinessSerializer(serializers.ModelSerializer):
owner = UserSerializer(many=False)
class Meta:
model = Business
fields= ('owner','legal_name')
class InvoiceSerializer(serializers.ModelSerializer):
business= BusinessSerializer(many=False)
class Meta:
model = Invoice
fields= ('business','amount')
views.py:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
class BusinessViewSet(viewsets.ModelViewSet):
queryset = Business.objects.all()
serializer_class = BusinessSerializer
class InvoiceViewSet(viewsets.ModelViewSet):
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer
urls.py:
router = DefaultRouter()
router.register('user', UserViewSet, base_name='users')
router.register('business', BusinessViewSet, base_name='businesses')
router.register('invoice', InvoiceViewSet, base_name='invoices')
urlpatterns = router.urls
http://example.com/api/user returns all users. Not a problem.
But the functionality I'm looking for is:
http://example.com/api/business/ returns
[
{
"legal_name": "1business",
"owner": 1,
},
{
"legal_name": "2business",
"owner": 1,
},]
http://example.com/api/business/1/ returns
{
"legal_name": "1business",
"owner": 1,
}
The above is ok. But I also need:
http://example.com/api/business/1/invoices/ should return
[
{
"business": 1,
"amount": 100,
},
{
"business": 1,
"amount": 999,
},]
As well I should be able to create update delete those invoices there.
Any Help? I'm new to django rest framework. The above classes are just a sample. Ignore errors.
You should use django decorators which are #list_route and #detail_route for your viewset. But be careful with your DRF version. Because those decorators merged together as #action in DRF 3.8+. Here is the announcement.
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
class BusinessViewSet(viewsets.ModelViewSet):
queryset = Business.objects.all()
serializer_class = BusinessSerializer
#action(detail=True, methods=["GET"], url_path="invoices")
def invoices(self, request, pk=None):
"""
Your codes comes here to return related result.
pk variable contains the param value from url.
if you do not specify the url_path properties then action will accept the function's name as url path.
"""
entity = Invoice.objects.filter(business=pk)
serializer = self.get_serializer(entity, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
Then, you will be able to call this endpoints from;
http://example.com/api/business/{{PK}}/invoices/
http://example.com/api/business/1/invoices/
http://example.com/api/business/3/invoices/
http://example.com/api/business/23/invoices/
Here you can find more details about #actions from documentation.
PS: Don't forget to control empty entity results in your codes. You should return correct response with correct status codes.
I am trying to have a logged in user add Media to their media field in the users profile in a DRF generic create view. Has anyone tried this? Here's my view and model:
class MediaCreate(generics.CreateAPIView):
"""
To create a media object, send a post request to:
/profiles/media/create/
In the format:
Audio: "audio file upload"
Title: "char field"
"""
queryset = Media.objects.all()
serializer_class = MediaSerializer
class Musician(ProfileModel):
summary = models.TextField(blank=True)
company = models.CharField(max_length=60, blank=True)
media = models.ManyToManyField('Media', blank=True)
timestamp = models.DateTimeField(auto_now_add=True, blank=True)
def __str__(self):
return '{}'.format(self.user.username)
So I decided to do this with a function api view instead so I can add the model to the profile using the request and not requiring any get request
I know the check if method == Post is redundant but it doesn't harm anything:
#api_view(['POST'])
def MediaCreate(request):
context = {}
logged_on = False
if request.user.is_authenticated():
logged_on = True
visitor = request.user.musician
serializer = MediaSerializer(data=request.data)
if request.method == "POST":
if serializer.is_valid():
serializer.save()
try:
x = serializer.instance
visitor.media.add(x)
context['upload'] = True
except:
error = "Media Not Added to Profile"
context['logged_on',
'error',
'upload'] = logged_on, error, False
return JsonResponse(
data=context,
status=status.HTTP_400_BAD_REQUEST)
context['logged_on'] = logged_on
return JsonResponse(data=context, status=status.HTTP_200_OK)
all you need to do define in your serializers.py the two classes:
class MediaSerializer(serializers.ModelSerializer):
class Meta:
model = Media
class MusicianSerializer(serializers.ModelSerializer):
media = MediaSerializer(many=True, read_only=true)
class Meta:
model = Musician
In order to update the media field for specific musician you send only the media_id and not the whole object as show the following example:
data = {
"summary": "text summary here"
"company": "Company A"
"media_id": 3
}
in your views.py you should define the following view and override get_object to fit your needs:
class MusicianUpdatingApiView(generics.UpdateAPIView):
"""
To add a media to a user, send a post request to:
/profiles/:id/media
"""
serializer_class = MusicianSerializer
def get_object(self):
summary = self.request.data.get("summary")
company = self.request.data.get("company")
media_id = self.request.data.get("media_id")
musician_id = self.kwargs['id'] # cause it send in the url
data = {
"summary": summary
"company": company
"media_id": media_id
}
updated_musician, created = Musician.objects.update_or_create(
id=musician_id,
defaults=data
)
return updated_musician
in urls.py
url(r'^profiles/(?P<id>\d+)/media$', MusicianUpdatingApiView.as_view()),
I have 2 databases. One containing the AUTH that's also extended in the following models.py -
from django.contrib.auth.models import User
class FileIndex(models.Model):
filename = models.CharField(max_length=256)
filetype = models.CharField(max_length=16)
vendorid = models.IntegerField()
vendorname = models.CharField(max_length=256)
tablename = models.CharField(max_length=256)
class Meta:
db_table = 'file_index'
verbose_name = 'File/Vendor Index'
verbose_name_plural = 'File/Vendor Indicies'
def __str__(self):
return self.filename
class UserFile(models.Model):
userid = models.ForeignKey(User)
fileid = models.ForeignKey(FileIndex)
grant_date = models.DateTimeField()
revoke_date = models.DateTimeField(blank=True)
class Meta:
db_table = 'auth_files'
verbose_name = 'User File Matrix'
verbose_name_plural = 'User File Matricies'
the 'tablename' field in FileIndex references a Table Name in another database referenced in a separate App. My current test view I'm using is follows in my views.py
class File_List(generics.ListAPIView):
model = cdx_composites_csv
serializer_class = cdx_compositesSerializer
def get_queryset(self):
"""
This view should return a list of all the purchases for
the user as determined by the username portion of the URL.
"""
filename = self.request.GET.get('filename')
model = get_model('markit', filename)
filedate = self.request.GET.get('filedate')
queryset = model.objects.using('markitdb').filter(Date__contains=filedate)
return queryset
If I'm not logged in it works fine and gives "not authorized" but regardless of if I've set view permissions on the table or not my user can still execute the view.
Model is listed before the function or else it will complain about the model not being there. I want to figure that out later. First I'm trying to understand why my view is still executing even if the user does not have group permission to view the Model.
I've attacked this by doing the following in my view -
class ExampleView(APIView):
model = cdx_composites_csv
serializer_class = cdx_compositesSerializer
def get(self, request, format=None):
if UserFile.objects.filter(fileid_id=1, userid_id=2).exists():
content = {
'status': 'Request Successful.'
}
return Response(content)
else:
content = {
'status': 'Request Failed.'
}
return Response(content)
Essentially after authentication it's doing a query against the UserFile to validate the User and the file exists and if it does then I can write it to do the queryset or not.