Create a resource POST with a GenericForeignKey in TastyPie - python

TastyPie shows here how to list ContentTypes and GenericForeignKeys. I have this working, but how do you make a POST to create a resource that has Generic Foreign Key?
This is my resource: I want to now create a new campaign while also creating a new SingleVoucherReward or MultiVoucherReward at the same time then linking it. How can this be done in TastyPie?
class CampaignCreateResource(ModelResource):
"""
API Facet.
"""
user = fields.ToOneField(UserResource, 'user', full=True)
participant_reward = GenericForeignKeyField({
SingleVoucherReward: SingleVoucherResource,
MultiVoucherReward: MultiVoucherResource,
}, 'participant_reward')
class Meta:
queryset = Campaign.objects.all()
resource_name = 'campaign'
allowed_methods = ['post', 'get']
authentication = APIAuthentication().get_authentication()
authorization = UserObjectsOnlyAuthorization()
validation = FormValidation(form_class=CampaignForm)
excludes = ['id', 'participant_reward_object_id']
def hydrate(self, bundle, request=None):
"""
Tastypie uses a 'hydrate' cycle to take serializated data from the client
and turn it into something the data model can use.
"""
bundle.obj.user = get_user_model().objects.get(pk=bundle.request.user.id)
return bundle

Related

Filtering results of multiple models under one API Endpoint with django-rest-framework

I am using DRF to generate API for my django app.
I am using the ViewSet class and exposing API endpoints for most of my models at their own path.
I want to allow viewing of my Endpoint and TLSCertificate models at an /assets/ path. As they are both children of an Organisation entity, I want to allow the results to be filtered Organisation.
So far I have:
serializers.py
class AssetSerializer(serializers.Serializer):
endpoints = EndpointSerializer(many=True)
certificates = TLSCertificateSerializer(many=True)
views.py
class AssetFilterSet(filters.FilterSet):
organisation = filters.ModelChoiceFilter(
name='organisation', queryset=Organisation.objects.all())
project = filters.ModelChoiceFilter(
name='project', queryset=Project.objects.all())
class Meta:
model = Endpoint
fields = ['organisation', 'project']
# the object type to be passed into the AssetSerializer
Asset = namedtuple('Asset', ('endpoints', 'certificates'))
class AssetViewSet(CacheResponseAndETAGMixin, viewsets.ViewSet):
"""
A simple ViewSet for listing the Endpoints and Certificates in the Asset list.
Adapted from https://stackoverflow.com/questions/44978045/serialize-multiple-models-and-send-all-in-one-json-response-django-rest-framewor
"""
# TODO filtering not functional yet
filter_class = AssetFilterSet
filter_fields = ('organisation', 'project',)
queryset = Endpoint.objects.all()
def list(self, request):
assets = Asset(
endpoints=Endpoint.objects.all(),
certificates=TLSCertificate.objects.all(), )
serializer = AssetSerializer(assets, context={'request': request})
return Response(serializer.data)
This works in returning the objects but does not allow for filtering to take place.
I would appreciate any guidance in how to enable the filtering in this situation?

Adding IsAuthenticatedOrReadOnly permission in django rest framework

Suppose I have the following model -
class Person(models.Model):
name = models.CharField(max_length=200)
clubs = models.ManyToManyField(Club,related_name = 'people')
date = models.DateTimeField(default=datetime.now)
def __str__(self):
return self.name
used to create a rest api.
views.py
class PersonDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = PersonSerializer
def get_object(self):
person_id = self.kwargs.get('pk',None)
return Person.objects.get(pk=person_id)
How do I add permissions so that only authenticated user can add,update delete or retrieve objects from the person list in the api. And read-only permissions for non authorized users. I tried going through the docs but it is all very confusing. Can someone explain?
You need to add IsAuthenticatedOrReadOnly permission class to PersonDetail view.
From the DRF Docs:
The IsAuthenticatedOrReadOnly will allow authenticated users to perform any request. Requests for unauthorised users will only be permitted if the request
method is one of the "safe" methods; GET, HEAD or OPTIONS.
from rest_framework.permissions import IsAuthenticatedOrReadOnly
class PersonDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = PersonSerializer
permission_classes = (IsAuthenticatedOrReadOnly,) # specify the permission class in your view
def get_object(self):
person_id = self.kwargs.get('pk',None)
return Person.objects.get(pk=person_id)

Fetching custom rest data from Django

I am using Django as server side and angular as client side.
I want to fetch data from my django rest api backend.
I saw a lot of tutorials about fetching data from an already existent modules. But what if I want to retrieve data that is a combination of several modules?
For example, I have two modules Reservations and Clubs. I want to retrieve json object that contains data from both of these modules, for specific club id.
Modules -
class Club(models.Model):
name = models.CharField(max_length=1024)
image_path = models.CharField(max_length=1024)
class Reservation(models.Model):
club = models.ForeignKey('Club')
user = models.CharField(max_length=1024)
is_paid = models.BooleanField(default=False)
Serializers -
class ReservationSerializer(serializers.ModelSerializer):
class Meta:
model = Reservation
fields = ('club', 'user')
class ClubSerializer(serializers.ModelSerializer):
class Meta:
model = Club
fields = ('id', 'name', 'surfaces')
View sets -
class ReservationViewSet(generics.ListAPIView):
serializer_class = ReservationSerializer
queryset = Reservation.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('id', 'club')
class ClubViewSet(generics.ListAPIView):
queryset = Club.objects.all()
serializer_class = ClubSerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('id', 'name')
So, for this exmaple, I want that when I GET this url -
http://127.0.0.1:8000/api/initial_data?club=2
it will run a ViewSet that does some logic and then return a json of this format -
{club_id: 2, reservations: {1:"John", 2:"Doe", 3:"Bob"} }
And more generally speaking - How can I return my own custom json containing data from multiple modules, with given URL parameters (back to client side)?
EDIT - If I want to return a simple JSON, how should I do it with django DRF, considering the fact that each viewset is being mapped into a model/serializer?
Maybe using a simple JsonResponse..?
You can define one serializer as a field in another
class ReservationSerializer(serializers.ModelSerializer):
...
class ClubSerializer(serializers.ModelSerializer):
reservations = ReservationSerializer(many=True, read_only=True)
...
Now ClubSerializer will return Reservations inside each Club
You can use APIView and override get method.
def get(self, request)
club = request.query_params.get('club', None)
"""
Do anything, access data, prepare dictionary or list with result
"""
return Response(result_dictionary_or_list)

tastypie - disable nested objects creation

When i'm creating a new resource with a foreign relation, specified as, i.e., {"pk": 20}, i get a new unwanted FK-item created.
I have Order model class with a relations to the Language model, so when creating an Order instance, i may have to specify the order's languages. Language list should be constant, and the users must not have an ability to modify existant or to create the new languages.
Order resource:
class OrderResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user', null=True, full=True)
src_lang = fields.ForeignKey(LanguageResource, 'src_lang', null=True, full=True)
dst_lang = fields.ForeignKey(LanguageResource, 'dst_lang', null=True, full=True)
def obj_create(self, bundle, request=None, **kwargs):
return super(OrderResource, self).obj_create(bundle, request, user=request.user)
class Meta:
resource_name = 'orders'
queryset = Order.objects.all()
serializer = Serializer(['json'])
authentication = MultiAuthentication(SessionAuthentication(), ApiKeyAuthentication())
authorization = ResourceAuthorization()
And here is a Language resource:
class Language(models.Model):
name = models.CharField(max_length=100)
code = models.CharField(max_length=100)
class LanguageResource(ModelResource):
class Meta:
resource_name = 'languages'
queryset = Language.objects.all()
allowed_methods = ['get']
authorization = ReadOnlyAuthorization()
serializer = Serializer(['json'])
I'm trying to create a new Order with jQuery:
var data = JSON.stringify({
"comment": "Something random",
"src_lang": {"pk": "20"},
"dst_lang": "/api/v2/languages/72/"
});
$.ajax({
type: 'POST',
url: '/api/v2/orders/',
data: data,
dataType: "json",
contentType: "application/json"
});
Instead of setting the pk:20 to src_lang_id field, it creates a new Language with empty fields for src_lang and sets a correct value for dst_lang. But empty fields are restricted with the Language model definition. How it saves it?
Also it's enough strange because i've straightly specified readonly access for language model, and only get method for accessing the supported language list.
If i declare language fields of OrderResource class as, i.e.: src_lang = fields.ForeignKey(LanguageResource, 'src_lang', null=True, full=True, readonly=True), it creates nothing, but also does not set any values for the foreign keys.
So, i just need to specify an existant language, i don't need to create it.
UPDATE
ResourceAuthorization:
class ResourceAuthorization(Authorization):
def is_authorized(self, request, object=None):
user = getattr(request, 'user', None)
if not user:
return False
return user.is_authenticated()
def apply_limits(self, request, object_list):
if request and hasattr(request, 'user'):
if request.user.is_superuser:
return object_list
return object_list.filter(user=request.user)
return object_list.none()
UPDATE 2
I found nothing more clever making fields read only and overriding obj_create method:
class OrderResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user', null=True, full=True)
src_lang = fields.ForeignKey(LanguageResource, 'src_lang', null=True, full=True, blank=True, readonly=True)
dst_lang = fields.ForeignKey(LanguageResource, 'dst_lang', null=True, full=True, blank=True, readonly=True)
def obj_create(self, bundle, request=None, **kwargs):
src_lang_id, dst_lang_id = bundle.data.get('src_lang', None), bundle.data.get('dst_lang', None)
if not all([src_lang_id, dst_lang_id]):
raise BadRequest('You should specify both source and destination language codes')
src_lang, dst_lang = Language.objects.guess(src_lang_id), Language.objects.guess(dst_lang_id)
if not all([src_lang, dst_lang]):
raise BadRequest('You should specify both source and destination language codes')
return super(OrderResource, self).obj_create(
bundle, request, user=request.user, src_lang=src_lang, dst_lang=dst_lang
)
class Meta:
resource_name = 'orders'
queryset = Order.objects.all()
serializer = Serializer(['json'])
authentication = MultiAuthentication(SessionAuthentication(), ApiKeyAuthentication())
authorization = ResourceAuthorization()
As outlined in this answer to your question, src_lang should correspond to a resource, not to some other value. I suspect that when the POST occurs and it doesn't find a resource pk=20, it creates a new Language object and calls save without Django model validation, allowing a blank field to exist in the created Language.
One way of forcing a read-only type resource is to create a resource which doesn't allow obj_create.
class ReadOnlyLanguageResource(ModelResource):
# All the meta stuff here.
def obj_create(self):
# This should probably raise some kind of http error exception relating
# to permission denied rather than Exception.
raise Exception("Permission denied, cannot create new language resource")
This resource is then referenced from your Order resource, overriding just the src_lang field to point to your read only resource.
class OrderResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user', null=True, full=True)
src_lang = fields.ForeignKey(ReadOnlyLanguageResource, 'src_lang')
dst_lang = fields.ForeignKey(ReadOnlyLanguageResource, 'dst_lang')
Any request that references an existing resource will complete as per normal (but you'll need to reference the resource correctly, not using pk=20). Any request the references an unknown language will fail as a new Language object cannot be created.
You should specify the src_lang in the format /api/v2/languages/72/ corresponding the pk=20.
Secondly what exactly is ResourceAuthorization? The documentation lists ReadOnlyAuthorization which might be useful to you.
Also the authorization applies the resource, not the underlying model. When creating a new object for fk, it does not use the REST Api but django.db.models and its permissions. So the authorizations might not apply via the foreign key constraint.

Django Tastypie - Resource with object details only

In Django with Tastypie, is there a way to configure a resource such that it only shows object details?
I want to have a url /user which returns the details of the authenticated user, as opposed to a list containing a single user object. I don't want to have to use /users/<id> to get the details of a user.
Here's the relevant portion of my code:
from django.contrib.auth.models import User
from tastypie.resources import ModelResource
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'user'
allowed_methods = ['get', 'put']
serializer = SERIALIZER # Assume those are defined...
authentication = AUTHENTICATION # "
authorization = AUTHORIZATION # "
def apply_authorization_limits(self, request, object_list):
return object_list.filter(pk=request.user.pk)
I was able to do this by using a combination of the following resource methods
override_urls
apply_authorization_limits
Example user resource
#Django
from django.contrib.auth.models import User
from django.conf.urls import url
#Tasty
from tastypie.resources import ModelResource
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'users'
#Disallow list operations
list_allowed_methods = []
detail_allowed_methods = ['get', 'put', 'patch']
#Exclude some fields
excludes = ('first_name', 'is_active', 'is_staff', 'is_superuser', 'last_name', 'password',)
#Apply filter for the requesting user
def apply_authorization_limits(self, request, object_list):
return object_list.filter(pk=request.user.pk)
#Override urls such that GET:users/ is actually the user detail endpoint
def override_urls(self):
return [
url(r"^(?P<resource_name>%s)/$" % self._meta.resource_name, self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
]
Using something other than the primary key for getting the details of a resource is explained in more detail in the Tastypie Cookbook

Categories

Resources