Why does full=true breaks resources behaviour in Django Tastypie? - python

Using full=trueon related fields is very convenient feature to prevent client from doing too many requests to get necessary information.
Nevertheless, I really don't understand why the "instructions" in Meta Class are not followed as soon as you use a related field.
Here is a simple example:
class UserResource(ModelResource):
class meta():
queryset = User.objects.all()
resource_name = 'users'
authorization = NothingAuthorization() # Returns None or [] on GET requests
class ClientUserResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user', full=True)
class meta():
queryset = ClientUser.objects.all()
resource_name = 'client_users'
# Some other required fields
Then,
a GET request on /users/1 returns a 401 response (just as expected, perfect!)
a GET request on /client_users/1 (client_user 1 is related to user 1) returns the data of client_user 1 AND the data of user 1 (I didn't expect to get user 1 information since it's unauthorised.)
Note that I got the same behaviour with allowed_methods, validation, etc.
This behaviour is a burden to me since it forces me to remove full=true everywhere to avoid security holes.
What am I doing wrong?

What do you expect Tastypie to do in that case? I suppose you would like to see a 401 Unauthorized to your ClientUserResource GET, but in my opinion this would be rather inconsistent. You are specifying that ClientUserResource has no explicit Authorization defined (and thus free GET access) but following your logic you would like to see the response according to another resource's authorization.
If you want to avoid security holes, it would be best indeed to not use full=True. You could overwrite the hydrate method to return the expanded object or not according to the rules you want, or use a similar Authentication class for ClientUserResource that takes into consideration the rules of UserResource (in this case full=True would be ok as you are controlling the full resource access).
EDIT: My two proposed solutions with more detail.
1. Same authorization
With this I mean that, if you want to protect your related resource while using full=True, you should use the same authorization in both resources (or a more strict authorization in ClientUserResource so your UserResource never gets leaked). In fact, I can't think of a case in which you would like to have two complete distinct authorization logics and then include one resource inside of another.
In your example, you should add authorization = NothingAuthorization(). If you really want a different authentication behavior for the second class let's see another option.
2. Overwrite dehydrate method or obj_create
In this case, you should delete the full=True and provide that behavior yourself, by for instance overwriting the dehydrate method. In this method you have access to your request and data so you can perform some logic.
class ClientUserResource(ModelResource):
class meta():
queryset = ClientUser.objects.all()
resource_name = 'client_users'
def dehydrate(self, bundle):
# Do whatever you want, for instance, authorize only one user, and then add
# to the result your desired info as full=True would do
if bundle.request.user.username == "admin":
bundle.data['user'] = {'username': "admin",
'email': "admin#gmail.com",
...}}
return bundle
This can look kind of "ugly", as you are not using any Authorization class in your resource that provides the authorization logic. I suppose you could build in the dehydrate method a UserResource along with the request and test there the authorization, but I think it gets more and more complicated for the strange behavior we are expecting here (different authentication models for two resources which are very coupled).

Related

How to modify Page model view in wagtail admin?

Background: I would like to enhance a page instance during an admin page view with some admin request related information (some pre-population in general). Basically I would need some function like "get_queryset", but not for list view, just for edit view.
In my older question related to a similar problem: Wagtail - how to preopulate fields in admin form? I was provided with instructions to use something called
CreatePageView
However, I cannot import it. Furthermore, I cannot even found any mention about that in google if I search:
Wagtail +CreatePageView
The closest thing I found is https://docs.wagtail.io/en/v2.1.1/reference/contrib/modeladmin/create_edit_delete_views.html but the page also states:
NOTE: modeladmin only provides ‘create’, ‘edit’ and ‘delete’
functionality for non page type models (i.e. models that do not extend
wagtailcore.models.Page). If your model is a ‘page type’ model,
customising any of the following will not have any effect
I am quite confused. What should I do if I need to customize the admin view for Page model extension?
I studied the wagtail source codes for Model.admin and Page and I have not found any way. Any ideas?
The related code simplified:
wagtail hooks:
class ItemAdmin(ModelAdmin):
pass
# some function override here maybe?
models:
class ItemPage(Page):
pass
# override for a function that gives data to the admin view maybe here?
Edit
As suggested in comments, it is possible to modify the admin page form during creation:
from wagtail.admin.forms import WagtailAdminPageForm
class ItemPageForm(WagtailAdminPageForm):
def __init__(self, data=None, files=None, parent_page=None, *args, **kwargs):
super().__init__(data, files, *args, **kwargs)
class ItemPage(Page):
base_form_class = ItemPageForm
however, acquiring the "request" in the WagtailAdminPageForm constructor does not seem possible.
This question is a bit ambiguous, so it is not super clear exactly what you need.
Interpreted question: When crediting (or editing) a page, I need access to the request to modify the initial values of some fields in the page form.
Potential Approach
Note: This may not be best practice and could be fragile depending on future changes to Wagtail.
First, we need a custom EditHandler, these are the way Wagtail builds up forms and even Panels within the editing interface. An EditHandler's job is to manage the form to return based on the model and even the current request.
As a first step, it would be good to get your page create form showing correctly by following the instructions on using a custom tabbed interface. From here, you can replace the TabbedInterface with your custom class (e.g. CustomTabbedInterface) and add some functionality to this which will allow for a dynamic form_class to be returned.
get_form_class should return the form_class, however, we can modify it to return a function that, when called, will instantiate the class with custom information based on the request.
There may be some issues with this approach below in edit views or scenarios not considered by this example so validate this fully before using.
Example Code
from wagtail.admin.edit_handlers import TabbedInterface, ObjectList
from wagtail.core.models import Page
class CustomTabbedInterface(TabbedInterface):
def get_form_class(self):
form_class = super().get_form_class()
request = self.request
if request and request.method != 'POST':
# check request is available to ensure this instance has been bound to it
user = self.request.user
def initiate_class(**kwargs):
# instead of returning the class, return a function that returns the instantiated class
# here we can inject a kwarg `initial` into the generated form
# important: this gets called for edit view also and initial will override the instance data
# kwarg['instance'] will be the `Page` instance and can be inspected as needed
kwargs['initial'] = {'introduction': user.first_name}
return form_class(**kwargs)
return initiate_class
return form_class
class StandardPage(Page):
# ... field etc
edit_handler = CustomTabbedInterface([
ObjectList(content_panels, heading='Content'),
ObjectList(Page.promote_panels, heading='Promote'),
ObjectList(Page.settings_panels, heading='Settings', classname="settings"),
])
Explanation
wagtail/admin/views/pages.py contains the create view, which will use the edit_handler, bind it to the model and the request and then call its get_form_class.
The form_class is used for the response here form = form_class(instance=page, parent_page=parent_page)
It gets called with the instance and the parent_page kwargs
Our custom get_form_class response takes those kwargs and injects an additional initial kwarg.
initial is used by Django forms to add any initial data - https://docs.djangoproject.com/en/3.0/ref/forms/api/#dynamic-initial-values
Finally, the Django form will merge the instance field values with the intial kwarg to generate the final pre-filled data for the form. You can see how this works in Django's BaseModelForm.
Be careful to consider what will happen on an update view, you likely do not want to override existing values with your initial values when a user has already entered something in the field.

Create/Update operations with nested serializers

I, as a newbie Django developer, am trying to build a RESTful API for a mobile app. I've took over an existing project and previous developers have used Django REST Framework. Super cool package, easy to work with so far. Except for one thing...
There is this problem when I want to create new resources, which happen to have nested serializers. I'm not great on explaining software issues with words, so here is the simplified version of my case:
class UserSerializer(serializers.ModelSerializer):
company = CompanySerializer()
# other props and functions are unrelated
class CompanySerializer(serializers.ModelSerializer):
# props and functions are unrelated
Now with this structure, GET /users and GET /users/{id} endpoints work great and I get the results I expect. But with POST /users and PATCH /users/{id} I get a response that says I need to provide an object for company prop and it should resemble a Company object with all the required props, so that it can create the company too. And I'm sure it tries to create a new company because I've tried sending { company: { id: 1 } } and it simply ignores the ID and requires a name to create a new one. This is obviously not what I want because I just want to create a user (who may or may not belong to a company), not both a user and a company.
I've tried switching that CompanySerializer to a serializers.PrimaryKeyRelatedField and it seems like it works on create endpoint but now I don't get the Company object on list and detail endpoints.
What am I missing here? I'm 99% sure that they did not intend this framework to work this way.
You need to override create() and update() methods on nested serializers to make them writable. Otherwise DRF is not sure what to do with nested objects. The simplest override would go something like this:
class UserSerializer(serializers.ModelSerializer):
company = CompanySerializer()
...
def create(self, validated_data):
return User.objects.create(**validated_data)
def update(self, instance, validated_data):
user = instance
user.__dict__.update(validated_data)
user.save()
return user
Note: haven't tested this variant of update() might need adjustments.
The trick is to use a different serializer class for retrieving vs updating - one with a PrimaryKeyRelatedField and one with a nested serializer. You can override get_serializer_class to do this. Assuming you are using a viewset:
class BaseUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (...)
class WriteUserSerializer(BaseUserSerializer):
company = CompanySerializer()
class ReadUserSerializer(BaseUserSerializer):
company = PrimaryKeyRelatedField()
class UserViewSet(viewsets.ViewSet):
def get_serializer_class(self):
if self.action in ["create", "update", "partial_update", "destroy"]:
return WriteUserSerializer
else:
return ReadUserSerializer

Using django-filter, why do unspecified or invalid lookup types return all results?

Here's a very simple Django Rest Framework/django-filter code example:
class MyModelFilter(django_filters.FilterSet):
class Meta:
model = MyModel
fields = {'my_str_field': ['exact']}
class MyModelList(generics.ListAPIView):
queryset = MyModel.objects.all()
filter_class = MyModelFilter
def get(self, request, format=None):
items = self.filter_queryset(self.queryset) # apply filters
serializer = MyModelSerializer(items, many=True)
return Response(serializer.data)
When I make this API call, the exact lookup type works as expected, returning matched objects:
/myobjects/?my_str_field=somevalue
If I use icontains, which as you see I did not specify as one of the supported lookup types, all objects are returned, as if the filter wasn't applied:
/myobjects/?my_str_field__icontains=this_can_be_anything
Furthermore, I can even use an invalid lookup type and there will be no error, with, again, all objects returned:
/myobjects/?my_str_field__this_can_be_anything=this_can_be_anything
This can obviously be misleading because a front-end developer who doesn't have access to the back-end code can happily think everything is fine and use the returned objects. I would expect, if not an error, at least an empty result set for the latter two cases. What am I doing wrong?
UPDATE: It appears that I should be using the strictness setting like so:
from django_filters.filterset import STRICTNESS
class MyModelFilter(django_filters.FilterSet):
# throw an exception on errors instead of returning empty results
strict = STRICTNESS.RAISE_VALIDATION_ERROR
class Meta:
model = MyModel
fields = {'my_str_field': ['exact']}
Unfortunately, this still doesn't result in an error, so my original question still stands.
If the server doesn't recognize a query string parameter, the typical behavior is to ignore that parameter. There's no standard or rfc that specifies how to handle unexpected query string parameters. But very many websites and web frameworks will liberally accept requests, and not perform any validation to reject requests that contain superfluous or misspelled query parameters.
In other words, this is not specific for Django Rest Framework.
This feature makes ajax cache busting possible. jQuery will add a parameter called _ with a random value to every single ajax request to make sure that the request has a unique url and is not cached anywhere. This would not work if the server returned an error on receiving an unexpected query parameter.

Django Admin: when displaying an object, display a URL that contains one of the fields

Here is an abstract base class for many of my "Treatment" models (TreatmentA, TreatmentB, etc):
class TreatmentBase(models.Model):
URL_PREFIX = '' # child classes define this string
code = shared.models.common.RandomCharField(length=6)
class Meta:
abstract = True
Each Treatment instance has a URL, that when visited by a user, takes them to a page specific to that treatment. I want to be able to create a Treatment in Django Admin, and immediately get this URL so I can send it to users. This URL can be created with the following method on TreatmentBase:
def get_url(self):
return '{}/{}/'.format(self.URL_PREFIX, self.code)
However, I am stuck with how to get this URL to display in Django Admin. I can think of the following solutions:
(1) Customize the display of the code field so that it becomes a clickable URL. Problem: I don't know how to do this.
(2) Add the get_url method to ModelAdmin.list_display. Problem: This means I would have to define a separate list_display for each of the child models of BaseTreatment, and I would have to explicitly list all the fields of the model, meaning I have to update it every time I modify a model, violating DRY.
(3) Add an extra field like this:
url = models.URLField(default = get_url)
Problem: get_url is an instance method (since it needs to refer to the self.code field), and from my reading of the docs about the default argument, it just has to be a simple callable without arguments.
Any way to do this seemingly simple task?
You could go with option 2 (adding to the admin display) but add it to the
readonly_fields which may alleviate your DRY concerns when models changes.
Option 3 (the extra field) could also work if you override the save method setting the URL property. You'd either want to set the field as readonly in the admin or only set the value in the save method if it's currently None.

How to access the request.user in a Piston classmethod

I have a model which contains a ManyToMany to User to keep track of which users have 'favorited' a particular model instance.
In my API for this model, when requested by an authenticated user, I'd like to include an 'is_favorite' boolean. However, it seems that any api fields that aren't straight model attributes must be implemented as a class method, which when called in Piston does not get a reference to the request object, and therefore I have no way to know who the current user is.
From the Piston docs:
In addition to these, you may define any other methods you want. You can use these by including their names in the fields directive, and by doing so, the function will be called with a single argument: The instance of the model. It can then return anything, and the return value will be used as the value for that key.
So, if only the Piston CRUD methods get an instance of the request, how can my classmethod fields generate output which is relevant to the current authenticated user?
I am not aware of the piston API, but how about using the thread locals middleware to access the request
add this to middleware
try:
from threading import local
except ImportError:
from django.utils._threading_local import local
_thread_locals = local()
def get_request():
return getattr(_thread_locals, 'request', None)
class ThreadLocals(object):
def process_request(self, request):
_thread_locals.request = request
and update the settings with the ThreadLocals middleware
and wherever you want to access the request import get_request from middleware
if you want to just get the current user, modify the middleware to set only request.user in thread locals
From the piston wiki page it says that you may specify the contents of foreign keys and many to many fields by nesting attributes. In your case
class FriendHandler(BaseHandler):
allowed_methods = ('GET',)
model = User
fields = ('userfield_1', 'userfield_2', ('friends', ('is_friended')))
def read(self, request):
# Anything else you might want to do, but return an object of type User
# Or whatever your model happens to be called
EDIT: Another slightly hacky way to do it (if you don't want the friend to get passed at all if the is_friended is false) would be to manually create a dict object structured how you like, and then return it. piston processes the dict a works with the built in emitters (the JSON one for sure, haven't tried the others)

Categories

Resources