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.
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 displaying a modelformset and I would like the forms to be ordered by the contents of one of its fields. So I want to use the equivalent of SomeModel.objects.filter(whatever).order_by('somefield') for a (model)formset in a template.
How can I do this?
Note that can_order does not do what I want (it must be automatic, not user specified). I have also tried other things, like the dictsort filter, but that produces unpredictable output (i.e. not ordered by the specified field).
I even tried {% regroup formset by somefield as sorted_formset %}, but the resulting sorted_formset cannot be used (iterated) as a normal formset.
To complete the answers. There are two ways how you can control the order of forms in the formset: formsets respect order of given queryset (this is shown in other replies). Another way (if you want to have the order of the forms fully under control) is to define custom formset class and override __iter__ and __getitem__ methods:
from django.forms import BaseModelFormSet
class MyModelBaseFormset(BaseModelFormSet):
def __iter__(self):
"""Yields the forms in the order they should be rendered"""
return ...
def __getitem__(self, index):
"""Returns the form at the given index, based on the rendering order"""
return ...
MyModelFormset = modelformset_factory(model=MyModel, formset=MyModelBaseFormset, queryset=...)
This approach is described in the Django documentation:
Iterating over the formset will render the forms in the order they
were created. You can change this order by providing an alternate
implementation for the __iter__() method.
Formsets can also be indexed into, which returns the corresponding
form. If you override __iter__, you will need to also override
__getitem__ to have matching behavior.
https://docs.djangoproject.com/en/dev/topics/forms/formsets/#django.forms.formsets.BaseFormSet
The example of implementing these methods is for example in this SO thread: modelformset __iter__ overloading problem.
if you didn't defined Formset, this is the "inline code" version:
FS=inlineformset_factory(ParentClass,ChildClass)
formset=FS(instance=parentobject,
queryset=parentobject.childobject_set.order_by("-time_begin")
)
Thanks to #john-peters answer above, for pointing me in the right direction. But here is a better way:
MyFormset(inlineformset_factory(...)):
def get_queryset(self):
return super(MyFormset, self).get_queryset().order_by('myfieldname')
This way you do not have to copy or mess with django's code and potentially cause breakage down the road.. just take the queryset that django gives you and override the ordering. I have used this in my own code, it works.
EDIT. After working on this a bit I realize that although it appears to work fine, it somehow messes up the logic in BaseInlineFormset.get_queryset(), resulting in duplicate database queries. However, in the hopes that someone will comment on this and correct it, I will leave it here. Meanwhile, I have another solution which DOES WORK and does not result in redundant queries.. as follows:
MyFormset(inlineformset_factory(...)):
def __init__(self, *args, **kwargs):
super(MyFormset, self).__init__(*args, **kwargs)
self.queryset = self.queryset.order_by('myfieldname')
This modifies the queryset at a safe time, before anything is done with it. In my code I am also doing .select_related() here, which greatly speeds up my large modelformsets!
Thanks to rantanplan's comment I found a solution. I could not use the method as described in the link above because I don't know what the queryset is going to be (this is a complex form with nested formsets).
Anyway, I found a solution by overriding the get_queryset method from Django's BaseInlineFormSet Class.
I copied it below, including my mod, in case googlers find it helpful.
def get_queryset(self):
'''
Copied this method from Django code and modified the ordering statement
'''
if not hasattr(self, '_queryset'):
if self.queryset is not None:
qs = self.queryset
else:
qs = self.model._default_manager.get_query_set()
# If the queryset isn't already ordered we need to add an
# artificial ordering here to make sure that all formsets
# constructed from this queryset have the same form order.
if not qs.ordered:
# MY MOD IS HERE:
# qs = qs.order_by(self.model._meta.pk.name)
qs = qs.order_by('order_index')
#/MOD
# Removed queryset limiting here. As per discussion re: #13023
# on django-dev, max_num should not prevent existing
# related objects/inlines from being displayed.
self._queryset = qs
return self._queryset
A more simpler way but it might not apply in every situation but if you can afford it just define ordering in models Meta class:
class ExampleModel(models.Model):
...
class Meta:
ordering = ("name", )
Formset will respect the queryset's ordering which will respect the Meta class variable's value.
I set up my function like this
#view_config(
route_name = 'route_name',
permissions = 'permissions',
renderer = 'r.mako'
)
def r( request ):
# stuff goes here
now, I want to add functionality such that I check certain conditions (using ajax) i would use one template, otherwise use another. is there a way to do this in pyramid? thanks
Well you can add the view multiple times with different renderers if you can determine what you want to do via predicates. For example
#view_config(route_name='route', xhr=True, renderer='json')
#view_config(route_name='route', renderer='r.mako')
#view_config(route_name='route', request_param='fmt=json', renderer='json')
def r(request):
# ...
Or you can just override the renderer manually via request.override_renderer = 'b.mako':
http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/narr/renderers.html#overriding-a-renderer-at-runtime
Or you can just explicitly render the response via the render and render_to_response methods from within the view, as the renderer argument is ignored if you return a Response object from the view.
Note that the xhr predicate in the first example should be sufficient to check for an ajax request. Note also that you don't have to use the same view for both if you don't want to, just depends.
I am developing a site right now that, once you are logged in, a search bar will always be present on the top of the page. I am wondering what the best way design this paradigm in Django. Currently, I have a separate file called forms.py that sits at settings.py level in my folder hierarchy. In almost every view, I have to add:
from forms.py import SearchForm
and then in every single render call, I have to pass:
form = SearchForm()
return render('somepage.html',{"search_form" : form},c=RequestContext())
I have looked around for a better way of doing this, but I am having trouble finding anything useful. I have a feeling that the current design I am using is not ideal, since I am required to import/pass as parameter in almost every view.
The form is defined in a base.html, so I am using template inheritance, but I still need to pass the form object to every render as far as I can tell.
Use a context processor
Add your search form to the context of all views using RequestContext, which the new render you're using does automatically.
def FormContextProcessor(request):
if request.user.is_authenticated():
return {'form': SearchForm() }
return {}
You said it's used in nearly all views, and this is hardly an expensive operation instantiating a form, so I'd use this solution.
with django < 1.3 you can have a decorator, which can take care of rendering a html:
def search_render(function):
# return a decorated function which will take template from the args
# take output of the inner function (this should be a dictionary e.g. data = ..
# instantiate SearchForm
# add SearchForm instance to the data dictionary
# and return render(template, data, RequestContext(request))
#search_render(tamplate='somepage.html')
def my_other_view(request):
return {'data':'value'}
With django >= 1.3 you can use a class based views, with similiar approach.
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!