I have a django project with a model that looks like:
class Profile(models.Model):
#some other stuff
owner = models.OneToOneField(settings.AUTH_USER_MODEL)
last_modified = models.DateTimeField(default = timezone.now)
def __unicode__(self):
return self.owner.name
__unicode__.admin_order_field = 'owner__last_name'
My model admin looks something like:
class ProfileAdmin(admin.ModelAdmin):
ordering = ['-last_modified']
list_display = ['__unicode__', 'last_modified']
I would like for the admin to be sorted by last_modified by default (as it is now) but to be able to sort alphabetically by clicking on the top of the first column of the list display. I tried to add the __unicode__.admin_order_field line as described here, but that doesn't seem to have made any difference. Is what I want possible? If not why not?
You can only sort fields in the django admin interface if they are fields on your model or if they are fields you custom annotate in the get_queryset method of your ModelAdmin class--essentially fields created at the DB level. However, assuming you are deriving your __unicode__ or __str__ method from some fields on your model (and you are--from owner.name) you should be able to reference those fields and make the column sortable like so (though you could use this method to make the unicode field sortable on any model attribute you'd like):
class ProfileAdmin(admin.ModelAdmin):
def sortable_unicode(self, obj):
return obj.__unicode__()
sortable_unicode.short_description = 'Owner Name'
sortable_unicode.admin_order_field = 'owner__last_name'
ordering = ['-last_modified']
list_display = ['sortable_unicode', 'last_modified']
Though I do find it a bit strange that you will be displaying the owner's name but sorting on last_name. This might be a bit puzzling when you wonder why your sort order doesn't match the displayed name in the admin interface.
Related
I am building a little message system with this model:
class Mail(models.Model):
sender = models.ForeignKey(Character, related_name="+")
to = models.ForeignKey(Character, related_name="+")
sent_at = models.DateTimeField(auto_now_add=True)
subject = models.CharField(max_length=127)
body = models.TextField()
now i made a modelform from this:
class ComposeForm(forms.ModelForm):
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at"]
however this gives my "to" field a drop down list with all possible characters.
Id like to make this in a normal charfield (later with auto completer) instead of this drop down.
Any idea how i can achieve this?
I've been in a similar place and I found 2 solutions, depending on the needs. The first one is suposing you're going to use something like a select2 and get the query via ajax:
class ComposeForm(forms.ModelForm):
to_char = forms.CharField(max_length=255, required=False) # use the name you want
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at","to"]
So to_char is empty, and then you manage that field as you want, and when you do the POST, you'll get the value of to_char in the view, and assign to the model where you need it.
The other option I suggest is to use a ModelChoiceField instead of Charfield like this:
class ComposeForm(forms.ModelForm):
to_char = forms.ModelMultipleChoiceField(queryset=Character.objects.all())
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at","to"]
This will make easier to use an external select tool as select2 (without AJAX)
I choose the first one when the model has thousands of possible choices, so the select hasn't to load all the choices and the page will be fast. I use a select2 in the template over this field, in this case to_char, that loads the options in an AJAX view
I use the second one when there are hundred of choices and using the autocomplete of select2 over this field has no problems, I think if you don't have too many choices this will be the best for you, the ModelChoiceField, you can attach an autocomplete without any trouble
I might have found even a neater solution to this problem. By overriding the default form field in this way:
class ComposeForm(forms.ModelForm):
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at"]
widgets = {'to': forms.TextInput()}
1) If you do not want to display all the values, then all you need to do is to override to field queryset.
Like:
class ComposeForm(forms.ModelForm):
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at"]
def __init__(self, *args, **kwargs):
super(ComposeForm, self).__init__(*args, **kwargs)
self.fields['to'].queryset = Character.objects.none()
2) If you want to add autocomplete then you will need view that does the filtering and returns filtered options. You also need some kind of js widget. There are many of those available. On django side you only need to update field widget parameters so your js can pick up the field.
I have a Django model with a ManyToManyField.
1) When adding a new instance of this model via admin view, I would like to not see the M2M field at all.
2) When editing an existing instance I would like to be able to select multiple options for the M2M field, but display only a subset of the M2M options, depending on another field in the model. Because of the dependence on another field's actual value, I can't just use formfield_for_manytomany
I can do both of the things using a custom ModelForm, but I can't reliably tell whether that form is being used to edit an existing model instance, or if it's being used to create a new instance. Even MyModel.objects.filter(pk=self.instance.pk).exists() in the custom ModelForm doesn't cut it. How can I accomplish this, or just tell whether the form is being displayed in an "add" or an "edit" context?
EDIT: my relevant code is as follows:
models.py
class LimitedClassForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(LimitedClassForm, self).__init__(*args, **kwargs)
if not self.instance._adding:
# Edit form
clas = self.instance
sheets_in_course = Sheet.objects.filter(course__pk=clas.course.pk)
self.Meta.exclude = ['course']
widget = self.fields['active_sheets'].widget
sheet_choices = []
for sheet in sheets_in_course:
sheet_choices.append((sheet.id, sheet.name))
widget.choices = sheet_choices
else:
# Add form
self.Meta.exclude = ['active_sheets']
class Meta:
exclude = []
admin.py
class ClassAdmin(admin.ModelAdmin):
formfield_overrides = {models.ManyToManyField: {
'widget': CheckboxSelectMultiple}, }
form = LimitedClassForm
admin.site.register(Class, ClassAdmin)
models.py
class Course(models.Model):
name = models.CharField(max_length=255)
class Sheet(models.Model):
name = models.CharField(max_length=255)
course = models.ForeignKey(Course)
file = models.FileField(upload_to=getSheetLocation)
class Class(models.model):
name = models.CharField(max_length=255)
course = models.ForeignKey(Course)
active_sheets = models.ManyToManyField(Sheet)
You can see that both Sheets and Classes have course fields. You shouldn't be able to put a sheet into active_sheets if the sheet's course doesn't match the class's course.
I have the models set up like this:
class ParentModel(models.Model):
some_col = models.IntegerField()
some_other = models.CharField()
class ChildModel(models.Model)
parent = models.ForeignKey(ParentModel, related_name='children')
class ToyModel(models.Model)
child_owner = models.ForeignKey(ChildModel, related_name='toys')
Now in my admin panel when I open the changelist for ParentModel I want a new field/column in the list_display with a link to open the changelist of the ChildModel but with an applied filter to show only the children from the selected parent. For now I realized it with this method, but I think there is a cleaner way to do it, I just don't know how:
class ParentAdmin(admin.ModelAdmin)
list_display = ('id', 'some_col', 'some_other', 'list_children')
def list_children(self, obj):
url = urlresolvers.reverse('admin:appname_childmodel_changelist')
return 'List children'.format(url, obj.id)
list_children.allow_tags = True
list_children.short_description = 'Children'
admin.site.register(Parent, ParentAdmin)
So my question is, is it possible to achieve the same without this "link hacking"?
Also is it possible to indicate in a separate column in the ParentModel changelist, if any of its children has toys?
I think your approach to display the list_children column is correct. Don't worry about the 'link hacking', it's fine.
To display a column for indicate whether any of the object's children has toys, just define another method on the ParentAdmin class, and add it to list_display as before.
class ParentAdmin(admin.ModelAdmin):
list_display = ('id', 'some_col', 'some_other', 'list_children', 'children_has_toys')
...
def children_has_toys(self, obj):
"""
Returns 'yes' if any of the object's children has toys, otherwise 'no'
"""
return ToyModel.objects.filter(child_owner__parent=obj).exists()
children_has_toys.boolean = True
Setting boolean=True means Django will render the 'on' or 'off' icons as it does for boolean fields. Note that this approach requires one query per parent (i.e. O(n)). You'll have to test to see whether you get acceptable performance in production.
I want to use raw_id_fields on a ManyToMany relationship in the admin, and I want each related object to show up on its own row (as opposed to a comma-separated list in a single field, which is the default behavior). Following examples spotted in the wild, it seems like I should be able to do this:
# models.py
class Profile(models.Model):
...
follows = models.ManyToManyField(User,related_name='followees')
# admin.py
class FollowersInline(admin.TabularInline):
model = Profile
raw_id_fields = ('follows',)
extra = 1
class ProfileAdmin(admin.ModelAdmin):
search_fields = ('user__first_name','user__last_name','user__username',)
inlines = (FollowersInline,)
admin.site.register(Profile,ProfileAdmin)
But that generates the error:
<class 'bucket.models.Profile'> has no ForeignKey to <class 'bucket.models.Profile'>
I'm not clear what I'm doing wrong here. Thanks for suggestions.
Looks like you are setting the wrong model for your InlineAdmin
as the model for followers you are defining is User and not Profile.
Looking at the docs I'd say you should try:
class FollowersInline(admin.TabularInline):
model = Profile.follows.through
and
class ProfileAdmin(admin.ModelAdmin):
....
exclude = ('follows',)
inlines = (FollowersInline,)
In inline for m2m connection you should use through table and for raw_id_fields setting you should use fields from that through table - inside this table fields can be name different as you expect.
You need goes to sqlite3/psql etc. terminal to see through table schema and use propper field for raw_id_fields.
class FollowersInline(admin.TabularInline):
model = Profile.follows.through
# raw_id_fields = ("follows",) <- wrong
raw_id_fields = ("user",) # <- probably right, because your m2m relation with `User` table and django use name of that table to name field in `through` model
Django doesn't support getting foreign key values from list_display or list_filter (e.g foo__bar). I know you can create a module method as a workaround for list_display, but how would I go about to do the same for list_filter? Thanks.
Django supports list_filter with foreign key fields
# models.py:
class Foo(models.Model):
name = models.CharField(max_length=255)
def __unicode__(self):
return self.name
class Bar(models.Model):
name = models.CharField(max_length=255)
foo = models.ForeignKey(Foo)
# admin.py:
class BarAdmin(admin.ModelAdmin):
list_filter = ('foo__name')
From documentation: Field names in list_filter can also span relations using the __ lookup
Well, the docs say that you can may use ForeignKey field types in list_filter:
http://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter
An example:
# models.py:
class Foo(models.Model):
name = models.CharField(max_length=255)
def __unicode__(self):
return self.name
class Bar(models.Model):
name = models.CharField(max_length=255)
foo = models.ForeignKey(Foo)
# admin.py:
class BarAdmin(admin.ModelAdmin):
list_filter = ('foo')
If you want to filter by a field from the related model, there's a patch on the way to make this work (will probably be merged into 1.2 as it seems):
http://code.djangoproject.com/ticket/3400
solution from this page worked for me http://www.hoboes.com/Mimsy/hacks/fixing-django-124s-suspiciousoperation-filtering/
define
class SmarterModelAdmin(admin.ModelAdmin):
valid_lookups = ()
def lookup_allowed(self, lookup, *args, **kwargs):
if lookup.startswith(self.valid_lookups):
return True
return super(SmarterModelAdmin, self).lookup_allowed(lookup, *args, **kwargs)
then allow the lookup for certain foreign key field
class PageAdmin(SmarterModelAdmin):
valid_lookups = ('parent')
If you construct the URL for the changelist manually then Django has no problems following relationships. For example:
/admin/contact/contact/?participant__event=8
or
/admin/contact/contact/?participant__event__name__icontains=er
Both work fine (although the latter doesn't add 'distinct()' so might have duplicates but that won't usually be an issue for filters)
So you just need to add something to the page that creates the correct links. You can do this either with by overriding the changelist template or by writing a custom filterspec. There are several examples I found by Googling - particularly on Django Snippets
You can easily create custom filters since Django 1.4 by overriding django.contrib.admin.SimpleListFilter class.
More information :
Admin list_filter documentation ;
Django-1.4 release note.
I ran into the same problem and really needed a solution. I have a workaround that lets you create a filter on a FK related model property. You can even traverse more than one FK relationship. It creates a new FilterSpec subclass that subclasses the default RelatedFilterSpec used to give you a filter on a ForeignKey field.
See http://djangosnippets.org/snippets/2260/
The Haes answer works perfectly, but if the __ looks up to another ForeignKey field, you end up with a blank result. You must place another __ lookup, until it points to the real field.
In my case: list_filter = ('place__condo__name', )
my models.py:
class Condo(models.Model):
name = models.CharField(max_length=70)
...
class Place(models.Model):
condo = models.ForeignKey(Condo)
...
class Actions(models.Model):
place = models.ForeignKey(Place)
...