How to modify Page model view in wagtail admin? - python

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.

Related

Using a FormSet in a ClassBasedView in django

I'm trying to set up a django form consisting solely of a formset. In forms.py I have:
class StudentEnrolmentForm(forms.ModelForm):
school_class = forms.ModelChoiceField(SchoolClass.objects.currently_enrolling())
class Meta:
model = StudentApplication
fields = []
StudentEnrolmentFormSet = modelformset_factory(StudentApplication, StudentEnrolmentForm, extra=0)
but I'm unclear how to incorporate the FormSet into a CBV (In this case I've chosen a FormView). In this case I'm basically displaying a table of students, and allowing the operator to assign each student to a class. I only want a single 'submit' button at the end of the page.
If you will take a look on sources of Django views and check how FormView is working, you find that it just overrides default get and post methods of base View class and adds some additional methods for the form handling.
So you can:
try to assign your formset to the form_class field of your view and
play around. Probably you will have to override some additional
methods;
take a look on https://github.com/AndrewIngram/django-extra-views;
if options #1 and #2 causes too much pain - use default View

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)

Setting object owner with generic create_object view in django

Is it possible to use create_object view to create a new object and automatically assign request.user as foreign key?
P.E:
class Post(models.Model):
text = models.TextField()
author = models.ForeignKey(User)
What I want is to use create_object and fill author with request.user.
In many ways, all the solutions to this will be more trouble than they are worth. This one qualifies as a hack. It is possible for a django update to leave you high and dry if they change the way create_update is implemented. For simplicity sake, I'll assume that you are trying to set a default user, not silently force the user to be the logged in user.
Write a context processor:
from django.views.generic.create_update import get_model_and_form_class
def form_user_default(request):
if request.method == 'GET':
model, custom_form = get_model_and_form_class(Post,None)
custom_form.author = request.user
return {'form':custom_form}
else: return {}
What this will do is override the form object that create_update passes to the template. What it's technically doing is re-creating the form after the default view has done it.
Then in your url conf:
url(r'pattern_to_match', 'django.views.generic.create_update.create_object', kwargs={'context_processors':form_user_default})
Again, I had to delve into the source code to figure out how to do this. It might really be best to try writing your own view (but incorporate as many Django custom objects as possible). There's no "simple default" way to do this, because in the django paradigm forms are more closely tied to the model layer than to views, and only views have knowledge of the request object.
You may want to consider a closure.
from django.forms import ModelForm
from django.views.generic.create_update import create_object, update_object
def make_foo_form(request):
class FooForm(ModelForm):
class Meta:
model = Foo
fields = ['foo', 'bar']
def save(self, commit=True):
f = super(FooForm, self).save(commit=False)
if not f.pk: f.user = request.user
if commit: f.save()
return f
return FooForm
def create_foo(request):
FooForm = make_foo_form(request)
return create_object(form_class=FooForm)
There is some inefficiency here, since you need to create the ModelForm object on each request, but it does allow you to inject functionality into the generic view.
You need to decide whether the added complexity for the form creation is worth maintaining simplicity on the view side.
A benefit here, though, is that this also works with the update case with practically no extra effort:
def update_foo(request, object_id):
FooForm = make_foo_form(request)
return update_object(form_class=FooForm, object_id=object_id)
Obviously, you can use this approach for more complex cases as well.
If a user is authenticated, their user object is the request.user object.
I'm not familiar with create_object... I'm still a beginner to django and have only just started my first real project with it.
Note that you should check to make sure a user is logged in before using this. This can be done with request.user.is_authenticated().
There is no good way to hook into the saving of an object when using the current Django generic views. Once they are rewritten as classes, you'll be able to subclass the view and hook in at the proper place without having to rewrite the whole view.
I already use my own class-based generic views for this reason.
I would suggest to make a wrapper for the create_object, as this author suggest
http://www.b-list.org/weblog/2006/nov/16/django-tips-get-most-out-generic-views/
in the view you'll have access to the user info.
Afterwards, you will need to use the extra_context to pass the user to the template. Finally at the template you can add a hidden field with the user info. I haven't tried it, but I have been thinking of it for quite some time. Hope this solution suits you!
;) cheers!

Keeping filters in Django Admin

What I would like to achive is:
I go to admin site, apply some filters to the list of objects
I click and object edit, edit, edit, hit 'Save'
Site takes me to the list of objects... unfiltered. I'd like to have the filter from step 1 remembered and applied.
Is there an easy way to do it?
Unfortunately there's no easy way to do this. The filtering does not seem to be saved in any session variable.
Clicking back twice is the normal method, but it can be unweildy and annoying if you've just changed an object so that it should no longer be shown using your filter.
If it's just a one-off, click back twice or go through the filtering again, it's the easiest way.
If you're going to be filtering more often, or you just want to learn about hacking the admin (which is pretty open and easy), you'll want to write a FilterSpec.
Have a look here and here for examples of people writing their own.
A really, really terrible way to do this would be to edit the admin interface so that after you click "Save", you are redirected to you filtered URL. I wouldn't recommend this at all, but it's an option.
Another fairly simple way to do this would be to write a generic view to show your filtered objects, then use Django forms to edit the items from there. I'd have a look at this, you'll be stunned just how little code you have to write to get a simple view/edit page going.
Click 2 times "Back"?
There's a simple hack to do this, but it's not a general solution and requires modifying every ModelAdmin which you want to support this. Maybe there is a general way to do this, but I've not spent the time to solve it on a general level.
The first step is to write a custom FilterSpec for the filter (see Harley's post for links that will help) which saves the chosen filter value in the session (and deletes it when no longer wanted).
# in cust_admin/filterspecs.py
from django.contrib.admin.filterspecs import FilterSpec, ChoicesFilterSpec
class MyFilterSpec(ChoicesFilterSpec):
def __init__(self, f, request, params, model, model_admin):
super(MyFilterSpec, self).__init__(f, request, params, model,
model_admin)
if self.lookup_val is not None:
request.session[self.lookup_kwarg] = self.lookup_val
elif self.lookup_kwarg in request.session:
del(request.session[self.lookup_kwarg])
# Register the filter with a test function which will apply it to any field
# with a my_filter attribute equal to True
FilterSpec.filter_specs.insert(0, (lambda f: getattr(f, 'my_filter', False),
MyFilterSpec))
You must import the module this is in somewhere, for example your urls.py:
# in urls.py
from cust_admin import filterspecs
Set a property on the field you want to apply the filter to:
# in models.py
class MyModel(models.Model):
my_field = Models.IntegerField(choices=MY_CHOICES)
my_field.my_filter = True
In a custom ModelAdmin class, override the change_view method, so that after the user clicks save, they are returned to the list view with their filter field value added to the URL.
class MyModelAdmin(admin.ModelAdmin):
def change_view(self, request, object_id, extra_context=None):
result = super(MyModelAdmin, self).change_view(request, object_id,
extra_context)
if '_save' in request.POST:
if 'my_field__exact' in request.session:
result['Location'] = '/admin/myapp/mymodel/?my_field__exact=%s' \
% request.session['my_field__exact']
return result
Another way to do this is to embed the filter in the queryset.
You can dynamically create a proxy model with a manager that filters the way you want, then call admin.site.register() to create a new model admin. All the links would then be relative to this view.
In my opinion its better to override methods from ModelAdmin changelist_view and change_view:
Like so:
class FakturaAdmin(admin.ModelAdmin):
[...]
def changelist_view(self, request, extra_context=None):
result = super(FakturaAdmin, self).changelist_view(request, extra_context=None)
request.session['qdict'] = request.GET
return result
def change_view(self, request, object_id, extra_context=None):
result = super(FakturaAdmin, self).change_view(request, object_id, extra_context)
try:
result['location'] = result['location']+"?"+request.session['qdict'].urlencode()
except:
pass
return result
As you wish, after save object you go back to list of objects with active filters.
There is a change request at the Django project asking for exactly this functionality.
All it's waiting for to be checked in is some tests and documentation. You could write those, and help the whole project, or you could just take the proposed patch (near the bottom of the page) and try it out.
https://code.djangoproject.com/ticket/6903
This feature has been added to Django as part of the 1.6 release and is enabled now by default. It is described in the release notes:
ModelAdmin now preserves filters on the list view after creating,
editing or deleting an object. It’s possible to restore the previous
behavior of clearing filters by setting the preserve_filters attribute
to False.

Categories

Resources