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)
Related
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.
I am using Django's dumpdata to save data and loaddata to reload it. I am also using natural keys. My model looks similar to this:
class LinkManager(models.Manager):
def get_by_natural_key(self, url):
return self.get(url=url)
class Link(models.Model):
objects = LinkManager()
title = models.CharField(max_length=200)
url = models.URLField()
def natural_key(self):
return (self.url, )
If I export and reimport the data, Django recognizes that the objects already exist and doesn't create duplicates. If I change the title, it correctly updates the objects. However, if I change the URL, it correctly treats it as a new object - although I forgot to mark url unique! How does it guess my intent?
How does django know that my url field is the natural key? There is no get_natural_fields function. Django could call natural_key on the class instead of an instance to get the fields, but that seems really brittle:
>>> [f.field_name for f in Link.natural_key(Link)]
['url']
The reason I want to know this is that I am writing my own special importer (to replace my use of loaddata), and I would like to take advantage of natural keys without hardcoding the natural key (or the "identifying" fields) for each model. Currently, I "identify" an object by it's unique fields - I do:
obj, created = Model.objects.update_or_create(**identifying, defaults=other)
but Django seems to be choosing it's "identifying" fields differently.
I think I've found it out. Django does not just call get_by_natural_key, it first calls natural_key. How does it do that, if it doesn't have an instance of the model?
It simply creates an instance, not backed by the database, from the constructor (d'oh!): Model(**data). See build_instance in django.core.serializers.base. Then it calls natural_key on the newly created object, and immediately get_by_natural_key to retrive the pk that belongs to the object, if present in the database. This way, Django does not need to know what fields the natural key depends on, it just needs to know how to get it from data. You can just call save() on the retrieved instance, if it is in the database it will have a pk and will update, if not it will create a new row.
Source of the build_instance function (Django 1.11.2):
def build_instance(Model, data, db):
"""
Build a model instance.
If the model instance doesn't have a primary key and the model supports
natural keys, try to retrieve it from the database.
"""
obj = Model(**data)
if (obj.pk is None and hasattr(Model, 'natural_key') and
hasattr(Model._default_manager, 'get_by_natural_key')):
natural_key = obj.natural_key()
try:
obj.pk = Model._default_manager.db_manager(db).get_by_natural_key(*natural_key).pk
except Model.DoesNotExist:
pass
return obj
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).
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.
I'm curious how this code actually works. In particular what does request.__class__.user = LazyUser() do as opposed to setattr(request, user, LazyUser()). Sorry if this is an ignorant question, I want to know because I'm going to implement a similar piece of middleware that adds an attribute to the request.
Here's the code, and of course when you call request.user you're returned some sort of user object.
class LazyUser(object):
def __get__(self, request, obj_type=None):
if not hasattr(request, '_cached_user'):
from django.contrib.auth import get_user
request._cached_user = get_user(request)
return request._cached_user
class AuthenticationMiddleware(object):
def process_request(self, request):
assert hasattr(request, 'session'), "The Django authentication middleware requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'."
request.__class__.user = LazyUser()
return None
Update: Doing a little reading, I understand how the __get__ bit works, so my question boils down to why do this vs. using setattr() as I mentioned above. Why is adding user as a property of the object vs the instance a good idea?
In this case, since LazyUser() is a class that figures out if the request object already has checked to make sure the user has been queried for, this is the real magic, it only needs to query the db for the user once per session. Being a property, it is a universal action that will be applied to all instances of the request. Properties are the same across all instances of said object. This simply makes the user property an instance of LazyUser allowing only one instance of it in memory (iirc this part, someone else should be able to clarify this point).
Why is adding user as a property of the object vs the instance a good idea?
It is necessary, so the __get__ method is called via the descriptor protocol. See here.