I've read, extensively, how to change the admin site of Django. I have it mostly figured out -- I think. However there are still a few things that elude me in my understanding. I am using the default registered admin urls; so they are not customized, only what is exposed automatically.
The easiest way to explain this is through imagery...
Here's what I have:
Here's what I want:
I'm fairly certain the changes should be fairly simple. But I don't know exactly which model to alter and template to adjust to get it to look how I want. The [number] -- [name] are fields in my model.
I have extended other pieces of the admin interface to get customized forms for editing particular elements -- by registering my model and customizing the field for it.
#admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
form = CourseAdminForm
fieldsets = (
('Course Info:', {'fields': ('course_number', 'name', 'description', 'units')}),
('Load Info:', {'fields': ('lecture_hours', 'lab_hours', 'discussion_hours', 'work_hours')})
)
in my app/admin.py file.
I'm a bit confused because there technically isn't a model to register here. So I'm not 100% sure how to do this. Do I wrap each one of my modifications inside the CourseAdmin class as different classes/methods with registered URLs or is there some other way I need to be doing this?
You need edit your Course model class:
# models.py
class Course(models.Model):
# fields here
name = ...
# ...
# add a unicode method
# __str__ method if you are using python 3.x
def unicode(self):
return '%s - %s' % (self.pk, self.name)
Related
I would like to validate a Many-to-many field in Django admin by overriding the clean method.
This thread gives a way to do that by creating a ModelForm and then doing the clean there. However, my problem is the many-to-many field is an inline i.e. instead of the widget where you have to select multiple elements, I have a tabular inline.
I would like to find out if anyone knows how to add the inlines in the ModelForm so that I can do the clean and validation. I've seen people talk about inlineformset_factory but it's always been as it relates to views.py and not the admin (and I can't figure out how I'd even go about overriding the clean method of that).
I've added some of my code below:
class ProductVariantForm(ModelForm):
class Meta:
model = ProductVariant
fields = [ 'name',
'price',
]
# I then want to be able to add something like
# inlines = [OptionValueInline,]
# for the inline many-to-many field.
def clean(self):
# Check if list of option_values is ok.
class ProductVariantAdmin(admin.ModelAdmin):
form = ProductVariantForm
Adding an inline is a feature of the Admin itself. See this doc for more info about inlines. Afaik, you can't add an inline to just a plain form (or a ModelForm).
To check the validity of the data in an inline, you could use the form property of the InlineModelAdmin class. This way you can access the clean method of the inline form directly.
To elaborate, it is split this way because the inlines are a separate form in Django's terms, concerning different data and running separate queries. They are all submitted in one HTTP request, but that is all they have in common. So it doesn't really make sense to use the main ModelForm for the inline data.
My solution to the problem is based on this post.
class ProductVariantOptionValueInlineFormSet(BaseInlineFormSet):
def clean(self):
super().clean()
data = self.cleaned_data
# do whatever validation on data here
class ProductVariantOptionValueInline(admin.TabularInline):
model = ProductVariant.option_values.through
formset = ProductVariantOptionValueInlineFormSet
class ProductVariantAdmin(admin.ModelAdmin):
inlines = [
ProductVariantOptionValueInline,
]
exclude = ('option_values',)
I have a model called Course:
class Course(models.Model):
number_of_semesters = models.PositiveIntegerField()
field = models.CharField(max_length=30)
qualification = models.ForeignKey(Qualification, on_delete=models.CASCADE)
I am trying to get a form in which you can input as many courses as the user wants from the webpage. How will I do this?
I know this is an old one, but I would recommend you to use Django Rest Framework. Although it is kind of tricky at first, you can use the ViewSets and Serializers to get multiple objects and save them in your database at once. (BTW, even though it is used for API's you can easily substitute the normal Django views with the ViewSets and use them as a standard).
I know this is not actually what you asked, but I have been developing in Django for a while now and I haven't been able to use the formsets in a clean way to save N objects without knowing N at first.
If you decide to go with my proposal, I would recommend you to read the following:
Viewsets
Serializers (they are basically the same as the DjangoForms)
Nested Serializers (for rendering/creating/linking your ForeignKey instance)
# SERIALIZER
from rest_framework import serializers
class QualificationSerializer(serializers.ModelSerializer):
class Meta:
model = Qualification
fields = (
# Insert the fields here, just like a form
)
class CourseSerializer(serializers.ModelSerializer):
qualification = QualificationSerializer() # Nested serializer
class Meta:
model = Course
fields = (
'number_of_semesters', 'field', 'qualification',
)
One way you could do this is to not use formsets but to get creative with the prefix that you use to load a form with. For example the + button loads an empty form with a prefix based on a counter posted to the view (including a tag, something like "course-4", so you get the form with SomeForm(request.POST, prefix="course-4")). When it's time to validate/save the view you simply parse the prefixes (with a regex) from the POST and save a form for every one of them.
It seems like a bit more work than to simply use formsets but every time I've tried to use them I had to abandon them at some point because they didn't provide enough flexibility.
This is how my models look:
class QuestionTagM2M(models.Model):
tag = models.ForeignKey('Tag')
question = models.ForeignKey('Question')
date_added = models.DateTimeField(auto_now_add=True)
class Tag(models.Model):
description = models.CharField(max_length=100, unique=True)
class Question(models.Model):
tags = models.ManyToManyField(Tag, through=QuestionTagM2M, related_name='questions')
All I really wanted to do was add a timestamp when a given manytomany relationship was created. It makes sense, but it also adds a bit of complexity. Apart from removing the .add() functionality [despite the fact that the only field I'm really adding is auto-created so it technically shouldn't interfere with this anymore]. But I can live with that, as I don't mind doing the extra QuestionTagM2M.objects.create(question=,tag=) instead if it means gaining the additional timestamp functionality.
My issue is I really would love to be able to preserve my filter_horizontal javascript widget in the admin. I know the docs say I can use an inline instead, but this is just too unwieldy because there are no additional fields that would actually be in the inline apart from the foreign key to the Tag anyway.
Also, in the larger scheme of my database schema, my Question objects are already displayed as an inline on my admin page, and since Django doesn't support nested inlines in the admin [yet], I have no way of selecting tags for a given question.
Is there any way to override formfield_for_manytomany(self, db_field, request=None, **kwargs) or something similar to allow for my usage of the nifty filter_horizontal widget and the auto creation of the date_added column to the database?
This seems like something that django should be able to do natively as long as you specify that all columns in the intermediate are automatically created (other than the foreign keys) perhaps with auto_created=True? or something of the like
There are ways to do this
As provided by #obsoleter in the comment below : set QuestionTagM2M._meta.auto_created = True and deal w/ syncdb matters.
Dynamically add date_added field to the M2M model of Question model in models.py
class Question(models.Model):
# use auto-created M2M model
tags = models.ManyToMany(Tag, related_name='questions')
# add date_added field to the M2M model
models.DateTimeField(auto_now_add=True).contribute_to_class(
Question.tags.through, 'date_added')
Then you could use it in admin as normal ManyToManyField.
In Python shell, use Question.tags.through to refer the M2M model.
Note, If you don't use South, then syncdb is enough; If you do, South does not like
this way and will not freeze date_added field, you need to manually write migration to add/remove the corresponding column.
Customize ModelAdmin:
Don't define fields inside customized ModelAdmin, only define filter_horizontal. This will bypass the field validation mentioned in Irfan's answer.
Customize formfield_for_dbfield() or formfield_for_manytomany() to make Django admin to use widgets.FilteredSelectMultiple for the tags field.
Customize save_related() method inside your ModelAdmin class, like
def save_related(self, request, form, *args, **kwargs):
tags = form.cleaned_data.pop('tags', ())
question = form.instance
for tag in tags:
QuestionTagM2M.objects.create(tag=tag, question=question)
super(QuestionAdmin, self).save_related(request, form, *args, **kwargs)
Also, you could patch __set__() of the ReverseManyRelatedObjectsDescriptor field descriptor of ManyToManyField for date_added to save M2M instance w/o raise exception.
From https://docs.djangoproject.com/en/dev/ref/contrib/admin/#working-with-many-to-many-intermediary-models
When you specify an intermediary model using the through argument to a ManyToManyField, the admin will not display a widget by default. This is because each instance of that intermediary model requires more information than could be displayed in a single widget, and the layout required for multiple widgets will vary depending on the intermediate model.
However, you can try including the tags field explicitly by using fields = ('tags',) in admin. This will cause this validation exception
'QuestionAdmin.fields' can't include the ManyToManyField field 'tags' because 'tags' manually specifies a 'through' model.
This validation is implemented in https://github.com/django/django/blob/master/django/contrib/admin/validation.py#L256
if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created:
raise ImproperlyConfigured("'%s.%s' "
"can't include the ManyToManyField field '%s' because "
"'%s' manually specifies a 'through' model." % (
cls.__name__, label, field, field))
I don't think that you can bypass this validation unless you implement your own custom field to be used as ManyToManyField.
The docs may have changed since the previous answers were posted. I took a look at the django docs link that #Irfan mentioned and it seems to be a more straight forward then it used to be.
Add an inline class to your admin.py and set the model to your M2M model
class QuestionTagM2MInline(admin.TabularInline):
model = QuestionTagM2M
extra = 1
set inlines in your admin class to contain the Inline you just defined
class QuestionAdmin(admin.ModelAdmin):
#...other stuff here
inlines = (QuestionTagM2MInline,)
Don't forget to register this admin class
admin.site.register(Question, QuestionAdmin)
After doing the above when I click on a question I have the form to do all the normal edits on it and below that are a list of the elements in my m2m relationship where I can add entries or edit existing ones.
If a django model contains a foreign key field, and if that field is shown in list mode, then it shows up as text, instead of displaying a link to the foreign object.
Is it possible to automatically display all foreign keys as links instead of flat text?
(of course it is possible to do that on a field by field basis, but is there a general method?)
Example:
class Author(models.Model):
...
class Post(models.Model):
author = models.ForeignKey(Author)
Now I choose a ModelAdmin such that the author shows up in list mode:
class PostAdmin(admin.ModelAdmin):
list_display = [..., 'author',...]
Now in list mode, the author field will just use the __unicode__ method of the Author class to display the author. On the top of that I would like a link pointing to the url of the corresponding author in the admin site. Is that possible?
Manual method:
For the sake of completeness, I add the manual method. It would be to add a method author_link in the PostAdmin class:
def author_link(self, item):
return '%s' % (item.id, unicode(item))
author_link.allow_tags = True
That will work for that particular field but that is not what I want. I want a general method to achieve the same effect. (One of the problems is how to figure out automatically the path to an object in the django admin site.)
I was looking for a solution to the same problem and ran across this question... ended up solving it myself. The OP might not be interested anymore but this could still be useful to someone.
from functools import partial
from django.forms import MediaDefiningClass
class ModelAdminWithForeignKeyLinksMetaclass(MediaDefiningClass):
def __getattr__(cls, name):
def foreign_key_link(instance, field):
target = getattr(instance, field)
return u'%s' % (
target._meta.app_label, target._meta.module_name, target.id, unicode(target))
if name[:8] == 'link_to_':
method = partial(foreign_key_link, field=name[8:])
method.__name__ = name[8:]
method.allow_tags = True
setattr(cls, name, method)
return getattr(cls, name)
raise AttributeError
class Book(models.Model):
title = models.CharField()
author = models.ForeignKey(Author)
class BookAdmin(admin.ModelAdmin):
__metaclass__ = ModelAdminWithForeignKeyLinksMetaclass
list_display = ('title', 'link_to_author')
Replace 'partial' with Django's 'curry' if not using python >= 2.5.
I don't think there is a mechanism to do what you want automatically out of the box.
But as far as determining the path to an admin edit page based on the id of an object, all you need are two pieces of information:
a) self.model._meta.app_label
b) self.model._meta.module_name
Then, for instance, to go to the edit page for that model you would do:
'../%s_%s_change/%d' % (self.model._meta.app_label, self.model._meta.module_name, item.id)
Take a look at django.contrib.admin.options.ModelAdmin.get_urls to see how they do it.
I suppose you could have a callable that takes a model name and an id, creates a model of the specified type just to get the label and name (no need to hit the database) and generates the URL a above.
But are you sure you can't get by using inlines? It would make for a better user interface to have all the related components in one page...
Edit:
Inlines (linked to docs) allow an admin interface to display a parent-child relationship in one page instead of breaking it into two.
In the Post/Author example you provided, using inlines would mean that the page for editing Posts would also display an inline form for adding/editing/removing Authors. Much more natural to the end user.
What you can do in your admin list view is create a callable in the Post model that will render a comma separated list of Authors. So you will have your Post list view showing the proper Authors, and you edit the Authors associated to a Post directly in the Post admin interface.
See https://docs.djangoproject.com/en/stable/ref/contrib/admin/#admin-reverse-urls
Example:
from django.utils.html import format_html
def get_admin_change_link(app_label, model_name, obj_id, name):
url = reverse('admin:%s_%s_change' % (app_label, model_name),
args=(obj_id,))
return format_html('%s' % (
url, unicode(name)
))
I have a ModelAdmin where I need to insert some html-snippet that is not part of a model (it's a java-applet). Is there any way to do this?
You have a couple options. If the applet is related to one of the form fields then you could create a custom widget which includes the applet. Another way would be to override the template used by the model change form and include the applet. The template should be in admin/app_name/model_name/change_form.html in your templates directory where app_name and model_name are replaced by the appropriate values for your model.
I tend to do a lot of this sort of thing, which is pretty much what you seem to want:
class SomeModelAdmin(admin.ModelAdmin):
...
list_display = (
'visible',
'thumbnail',
'size',
'url',
)
...
def thumbnail(self, obj):
return u'<img src="%s" />' % obj.url
thumbnail.allow_tags = True
... et voila, ad-hoc HTML snippets. obj is the model instance in question. Personally I find this more flexible than endlessly subclassing Widgets, ModelForms et al -- your mileage may vary depending on what you do with the admin site, or if your're of the more orthodox object-oriented persuasion; it's helpful to know how to do it in any case.