In the Django admin, I would really like to be able to display an editable ManyToManyField in the list display.
It doesn't necessarily need to be the full ManyToManyField control - being able to save just one value would be good enough for the purposes of the list display (though the underlying values are many-to-many in nature).
My model looks like this:
class Item(models.Model):
name = models.CharField(max_length=500)
colour = models.ManyToManyField(Colour, related_name='primary_colour')
If I try this in admin.py:
class ItemAdmin(admin.ModelAdmin):
list_display = ('name', 'colour')
list_editable = ('colour')
Then I get this error:
'ItemAdmin.list_display[6]', 'colour' is a ManyToManyField which is not supported.
Is there any way at all that I can show an editable ManyToManyField for rapid editing in the list display?
I found this related question, which explains how to make the values visible in the list display, but not editable: ManyToManyField widget in a django admin change list?
Django by default won't allow to add ManyToManyField in list_editable in ModelAdmin. So we need to override model admin methods.
On looking your models you need to follow below steps to get the ManyToManyField editable in list display page.
In apps/forms.py you need to define which ManyToMany fields you need to make editable in list display page. As below,
from django import forms
from app.models import Item
class ItemChangeListForm(forms.ModelForm):
# here we only need to define the field we want to be editable
colour = forms.ModelMultipleChoiceField(queryset=Colour.objects.all(),
required=False)
In app/admin.py you need to override methods of model admin. As below,
from django.contrib import admin
from django.contrib.admin.views.main import ChangeList
from app.models import Item
from app.forms import ItemChangeListForm
class ItemChangeList(ChangeList):
def __init__(self, request, model, list_display,
list_display_links, list_filter, date_hierarchy,
search_fields, list_select_related, list_per_page,
list_max_show_all, list_editable, model_admin):
super(ItemChangeList, self).__init__(request, model,
list_display, list_display_links, list_filter,
date_hierarchy, search_fields, list_select_related,
list_per_page, list_max_show_all, list_editable,
model_admin)
# these need to be defined here, and not in ItemAdmin
self.list_display = ['action_checkbox', 'name', 'colour']
self.list_display_links = ['name']
self.list_editable = ['colour']
class ItemAdmin(admin.ModelAdmin):
def get_changelist(self, request, **kwargs):
return ItemChangeList
def get_changelist_form(self, request, **kwargs):
return ItemChangeListForm
admin.site.register(Item, ItemAdmin)
Now you all set to check the changes, run server and check django admin for Movie model. You can edit ManyToMany field directly from list display page.
Note : If you are going to use muliptle ManyToManyFields editable in list then, you need to set DATA_UPLOAD_MAX_NUMBER_FIELDS in settings.py .
You can easily add a custom view to your admin urls and add the required html/javascript/ajax. Here's the basics:
class ItemAdmin(admin.ModelAdmin):
# regular stuff
def render_foo(self, obj):
# add this to your list_display
html = '<stuff><input/submit action></stuff>'
return mark_safe(html)
def get_urls(self):
urls = super(ItemAdmin, self).get_urls()
extra_urls = patterns('',
(r'^process_foo/$', self.admin_site.admin_view(self.process_foo)),
)
return extra_urls + urls
def process_foo(self, request):
if not request.is_ajax():
raise Http404
foo = request.GET.get("attr")
# process m2m
# return some json
Related
Is it possible to load external data into a field for filling in?
Example: A field with for product names. However we already have the names of the products in another location, we just need to list these products within the field in the default django admin. Using resquets.
Thank you very much for your attention.
I think what you're looking for is how to customize the Django Admin, right? Check out this page in the documentation for a more detailed explanation, but here's an example that might help:
from django.contrib import admin
from .models import *
class ProductInline(admin.TabularInline):
model = Product
extra = 0
class OrderAdmin(admin.ModelAdmin):
inlines = [ProductInline]
admin.site.register(Order, OrderAdmin)
admin.site.register(Product)
This will show all of the products attached to a particular order when viewing that order from Django Admin.
You can prepopulate/fill a field in Django Admin with external data source. I guess you have some options defined somewhere outside your Django app and use those options as input for a charfield/integer field.
You can handle filling choices in a seperate Django form or overriding ModelAdmin methods. By creating a seperate form:
filter_choices = depends on your logic for loading external data
class AdminForm(forms.ModelForm):
filter_text = forms.ChoiceField(choices = filter_choices , label="Filter By",
widget=forms.Select(), required=True)
class Meta:
model = YourModel
#admin.register(YourModel)
class YourModelAdmin(admin.ModelAdmin):
form = AdminForm
You can try the 'formfield_for_foreignkey' method of the default ModelAdmin class
Example:
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "car":
kwargs["queryset"] = Car.objects.filter(owner=request.user)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
This example (from the original docs) will populate the 'car' field with only specific values.
Pls note that this method suits a foreinKey. I'm not sure if it fits your requirements.
I am using createview for creating few fields of a model. I want to provide custom validation to attributes in my form. I am not sure how to do it through the CreateView. I don't want to create a Modelform for it.
Generally custom validation for the attributes is performed by clean_attr() method in forms. So, Is there any way to perform this in createview ?
my createview class
#method_decorator(never_cache, name='dispatch')
class AppCreateView(LoginRequiredMixin, CreateView):
model = models.App
fields = ['name', 'background', 'font', 'textcolor']
def get_context_data(self, *args, **kwargs):
context = super(AppCreateView, self).get_context_data(*args, **kwargs)
context['view'] = 'create'
return context
In the fields, I am excluding a fild called "date" (which has to be today). Is there any way to set the date attribute in CreateView ?
Thanks
update
My questions
How to make custom validation in AppCreateView ?
How to fill the other attributes apart of the user filled ones such as date ?
If you want to do that you can add validators on your model which will be catched by CreateView : https://docs.djangoproject.com/en/2.1/ref/validators/
For the date, juste add on your field :
my_field = models.DateTimeField(auto_now=True) #the date will be timezone.now() when you save the instance.
EDIT: I updated the code to reflect one mistake in the queryset filter, where I had user__user_type, has been replaced with the correct userprofile__user_type.
I'm using Django 1.4, and I understand there's a new feature to create custom list_filters for the admin, that replaced the FilterSpec API.
I've read countless SO posts about creating custom admin list_filters, but I'm still stuck.
My use case is for a list_filter for my User objects that's referencing a field in the UserProfile Model.
So in models.py:
class UserProfile(models.Model):
user = models.OneToOneField(User, unique=True)
user_type = models.CharField(max_length=25, choices=USER_TYPES, default='Client')
...
and in admin.py:
from django.contrib import admin
from django.contrib.admin import site, ModelAdmin, ChoicesFieldListFilter
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin
from models import UserProfile
class UserTypeFilter(ChoicesFieldListFilter):
title = 'User Type'
parameter_name = 'user_type'
def lookups(self, request, model_admin):
usertypes = set([c.user_type for c in UserProfile.objects.all()])
return [(c.id, c.user_type) for c in usertypes]
def queryset(self, request, queryset):
if self.value():
return queryset.filter(userprofile__user_type=self.value())
else:
return queryset
class UserAdmin(UserAdmin):
list_filter = ('is_staff', UserTypeFilter)
inlines = (UserProfileInline, )
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
Here's the error I get:
'UserAdmin.list_filter[1]' is 'UserTypeFilter' which is of type FieldListFilter but is not associated with a field name.
I originally tried using the SimpleListFilter, and got this error
'unicode' object has no attribute 'id'
Which makes sense, because my user_type field is a choice field, and the choices are unicode objects.
The docs show this example:
class PersonAdmin(UserAdmin):
list_filter = ('company__name',)
But this seems to imply that 'company' is a field on the User model. Do I need to redefine my User model to include a OneToOneField for UserProfile? Or am I missing some obvious way to reference a user's profile?
Thanks,
The first error you reference seems to be specific to FieldListFilter which I've never used. I guess the point of it is to automate something related to a specific field.
I see no use for it with your example as you're supplying all options.
I'd just use a SimpleListFilter for now...
Anyways your error is that usertypes is a set of c.user_type which is a string. It has no id attribute.
You need to return [(c, c) for c in usertypes] where the first value is the value passed to your QS, and the second is the display value.
The rest doesn't need to change.
I was wondering how they made it possible to display more fields in the User page of the Django admin site.
If you create a new User you only have some basic fields to fill in, but if you reopen that user (edit mode) then you see a lot more fields to fill in.
I'm trying to achieve the same, I had a look at the add_form.html template but I can't really get my head around it. I guess I'm looking for a way of specifying different fields = [] sets based on the edit status of the document.
Thanks!
The answer lies in the custom admin class registered for the User model. It overrides a couple of methods on ModelAdmin and checks to see whether the current request is creating a new User (in which case the bare-bones form class for adding accounts is used) or editing an existing one (in which case a full form is shown).
Here's my try. When I try to create a new item (Add) it shows only certain fields but then when I hit save it returns an error:
DoesNotExist
in /Library/Python/2.6/site-packages/django/db/models/fields/related.py in get, line 288
admin.py
from django.contrib import admin
from myapp.catalog.models import Model
from myapp.catalog.forms import ProductAdminForm, ProductAddForm
class ProductAdmin(admin.ModelAdmin):
form = ProductAdminForm
#...
add_form = ProductAddForm
def get_form(self, request, obj=None, **kwargs):
defaults = {}
if obj is None:
defaults.update({
'form': self.add_form,
})
defaults.update(kwargs)
return super(ProductAdmin, self).get_form(request, obj, **defaults)
forms.py
from myapp.catalog.models import Product
class ProductAdminForm(forms.ModelForm):
class Meta:
model = Product
#...
class ProductAddForm(forms.ModelForm):
class Meta:
model = Product
fields = ("model", "colour",)
I want to allow the admins of my site to filter users from a specific country on the Admin Site. So the natural thing to do would be something like this:
#admin.py
class UserAdmin(django.contrib.auth.admin.UserAdmin):
list_filter=('userprofile__country__name',)
#models.py
class UserProfile(models.Model)
...
country=models.ForeignKey('Country')
class Country(models.Model)
...
name=models.CharField(max_length=32)
But, because of the way Users and their UserProfiles are handled in django this leads to the following error:
'UserAdmin.list_filter[0]' refers to field 'userprofile__country__name' that is missing from model 'User'
How do I get around this limitation?
What you are looking for is custom admin FilterSpecs. The bad news is, the support for those might not supposed to ship soon (you can track the discussion here).
However, at the price of a dirty hack, you can workaround the limitation. Some highlights on how FilterSpecs are built before diving in the code :
When building the list of FilterSpec to display on the page, Django uses the list of fields you provided in list_filter
Those fields needs to be real fields on the model, not reverse relationship, nor custom properties.
Django maintains a list of FilterSpec classes, each associated with a test function.
For each fields in list_filter, Django will use the first FilterSpec class for which the test function returns True for the field.
Ok, now with this in mind, have a look at the following code. It is adapted from a django snippet. The organization of the code is left to your discretion, just keep in mind this should be imported by the admin app.
from myapp.models import UserProfile, Country
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin
from django.contrib.admin.filterspecs import FilterSpec, ChoicesFilterSpec
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
class ProfileCountryFilterSpec(ChoicesFilterSpec):
def __init__(self, f, request, params, model, model_admin):
ChoicesFilterSpec.__init__(self, f, request, params, model, model_admin)
# The lookup string that will be added to the queryset
# by this filter
self.lookup_kwarg = 'userprofile__country__name'
# get the current filter value from GET (we will use it to know
# which filter item is selected)
self.lookup_val = request.GET.get(self.lookup_kwarg)
# Prepare the list of unique, country name, ordered alphabetically
country_qs = Country.objects.distinct().order_by('name')
self.lookup_choices = country_qs.values_list('name', flat=True)
def choices(self, cl):
# Generator that returns all the possible item in the filter
# including an 'All' item.
yield { 'selected': self.lookup_val is None,
'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
'display': _('All') }
for val in self.lookup_choices:
yield { 'selected' : smart_unicode(val) == self.lookup_val,
'query_string': cl.get_query_string({self.lookup_kwarg: val}),
'display': val }
def title(self):
# return the title displayed above your filter
return _('user\'s country')
# Here, we insert the new FilterSpec at the first position, to be sure
# it gets picked up before any other
FilterSpec.filter_specs.insert(0,
# If the field has a `profilecountry_filter` attribute set to True
# the this FilterSpec will be used
(lambda f: getattr(f, 'profilecountry_filter', False), ProfileCountryFilterSpec)
)
# Now, how to use this filter in UserAdmin,
# We have to use one of the field of User model and
# add a profilecountry_filter attribute to it.
# This field will then activate the country filter if we
# place it in `list_filter`, but we won't be able to use
# it in its own filter anymore.
User._meta.get_field('email').profilecountry_filter = True
class MyUserAdmin(UserAdmin):
list_filter = ('email',) + UserAdmin.list_filter
# register the new UserAdmin
from django.contrib.admin import site
site.unregister(User)
site.register(User, MyUserAdmin)
It's clearly not a panacea but it will do the job, waiting for a better solution to come up.(for example, one that will subclass ChangeList and override get_filters).
Django 1.3 fixed it. You're now allowed to span relations in list_filter
https://docs.djangoproject.com/en/1.3/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter