night-crawler
night-crawler

Reputation: 1469

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()

Upvotes: 1

Views: 693

Answers (2)

Austin Phillips
Austin Phillips

Reputation: 15776

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.

Upvotes: 2

Pratik Mandrekar
Pratik Mandrekar

Reputation: 9568

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.

Upvotes: 1

Related Questions