Code


Version 50 (modified by mwdiers, 6 years ago) (diff)

--

Lllama's handy how-do-I guide to newforms admin.

I've been using the newforms admin for a while now. I switched to the branch as I was hacking up a data entry app which a modified admin would have been perfect for. Since I envisioned having to create some new forms I thought I'd try and use newforms in the admin. Newforms-admin seemed like the logical choice. As it turns out I didn't really need to create my own forms, and this is really because of the new outstanding powers of the branch. (I'm now of the opinion that the admin is so powerful it's effectively its own framework. Not sure what that means but that's the feeling I get).

What follows is a series of FAQ/Howto style questions that I've reverse engineered out of what I've come up with. I hope these will be helpful to someone (the first few are pretty much identical to the existing docs, so skip down a bit if you're using the branch already).

  1. Q: I have a newforms-admin question not answered here. How can I get it answered?
  2. Q: How do I use the admin app?
  3. Q: How do I change the admin options?
  4. Q: How do I set up edit_inline?
  5. Q: How do I change the attributes for a widget on a field in my model.
  6. Q: How do I change the attributes for a widget on a single field in my model.
  7. Q: I've defined a new permission. How do I enforce it?
  8. Q: I've tweaked something which generated a message for the user but I don't want them to see it. How can I get rid of it?
  9. Q: How do I add an extra column to the change list view?
  10. Q: I want to add some field specific template content in my change form. How, how?
  11. Q: How do I add new object tools to the top right of my change form?
  12. Q: How do I get at the ID of my model in an inline row?
  13. Q: How do I get at the original model in a template?
  14. Q: Okay. This is a tricky one. Not sure if you'll be able to help, but do you know how to generate a DOCX or ODF file from in the admin?
  15. Q: Where did prepopulate_from go? My SlugField don't work!
  16. Q: What happened to filter_interface?
  17. Q: How do I add custom JavaScript/CSS?
  18. Q: How do I add custom validation?
  19. Q: How do I use updated_at and created_on type fields but stop them being displayed?
  20. Q: How can I pass extra context variables into my add and change views?
  21. Q: How can I pass extra context variables into my index page?
  22. Q: How can I change where I get sent after saving a new entry to the database?
  23. Q: How do I filter the ChoiceField based upon attributes of the current ModelAdmin instance?

Q: I have a newforms-admin question not answered here. How can I get it answered?

A: Try asking on the Django users mailing list. Once you have your answer you can add an entry to the bottom of this FAQ.

Q: How do I use the admin app?

A: To get your basic admin app up and running add the following to your urls:

from django.contrib import admin

at the top of the file and

(r'^admin/(.*)', admin.site.root),

in the patterns.

To include a model in the admin app you'll need to register it in your models.py file. Again:

from django.contrib import admin

at the top and then:

admin.site.register(MyModel)

at the bottom.

Q: How do I change the admin options?

A: Options are now defined in its own class outside of the model. Define your own class that inherits from admin.ModelAdmin. E.g.

class MyModelAdmin(admin.ModelAdmin):
    save_on_top = True
    list_display = ('name','approved',)
    list_filter = ('category', 'approved',)
    search_fields = ['id', 'name', 'platform', 'description']
    fieldsets = (
                    (None, {'fields': ('title', 'client')}),
                    ('Details',
                        {'fields': ('location', 'project',
                            'number', 'start_date',
                            'end_date', 'grade', 'version', 'approved'),
                     'classes': ['collapse'],
                        }
                    ),
                )

To associate your admin class with the model you pass the ModelAdmin class as the second option to the register function

admin.site.register(MyModel, MyModelAdmin)

Q: How do I set up edit_inline?

A: You'll need to do two things. First, set up the options for the models to be edited inline:

class MyOtherModelInline(admin.StackedInline):
    model = ReportTemplateXML

The minimum definition is the model attribute. You can also pass in the number of extra rows to display and which template to use:

class MyOtherModelInline(admin.StackedInline):
    model = MyOtherModel
    extra = 2
    template = 'my_new_template_tabular.html'

To use the inline you need to define the inlines attribute in the parent model:

class MyModelAdmin(admin.ModelAdmin):
    save_on_top = True
    search_fields = ['title', 'client__name']
    list_display = ('title', 'client', 'approved')
    list_filter = ('approved',)
    inlines = [MyOtherModelInline]

Q: How do I change the attributes for a widget on a field in my model.

A: Override the formfield_for_dbfield in the ModelAdmin/StackedInline/TabularInline class

class MyOtherModelInline(admin.StackedInline):
    model = MyOtherModel
    extra = 1

    def formfield_for_dbfield(self, db_field, **kwargs):
        # This method will turn all TextFields into giant TextFields
        if isinstance(db_field, models.TextField):
            return forms.CharField(widget=forms.Textarea(attrs={'cols': 130, 'rows':30, 'class': 'docx'}))
        return super(MyOtherModelInline, self).formfield_for_dbfield(db_field, **kwargs)

Q: How do I change the attributes for a widget on a single field in my model.

A: Again, override the formfield_for_dbfield method:

    def formfield_for_dbfield(self, db_field, **kwargs):
        field = super(MyModelAdmin, self).formfield_for_dbfield(db_field, **kwargs) # Get the default field
        if db_field.name == "the_field_I_want_to_change": # Check if it's the one you want
            field.widget.attrs['class'] = "my_new_class" # Poke in the new 
        return field

Q: I've defined a new permission. How do I enforce it?

A: Override add_view and change_view in the ModelAdmin class:

class MyModelAdmin(admin.ModelAdmin):
    save_on_top = True

    def change_view(self, request, obj_id):
        if request.user.has_perm("my_new_permission"):
            return super(MyModelAdmin, self).change_view(request, obj_id)
        else:
            raise PermissionDenied

The above will enforce the permission on both GET and POST requests. Put in an if request.method =="POST": or similar if you want to distinguish between the two.

Q: I've tweaked something which generated a message for the user but I don't want them to see it. How can I get rid of it?

A: In the overridden view function call the following:

request.user.get_and_delete_messages()

Q: How do I add an extra column to the change list view?

A: I've got a report model and I've added a link on the change view to allow users to download the report as a Word file. To add your own link you'll need to do two things. First create the following template tags (in project/app/templatetags/change_list_extras.py):

from django.contrib.admin.templatetags.admin_list import items_for_result, result_headers
from django.template import Library
from django.utils.safestring import mark_safe
register = Library()

def results(cl, additional_links):
    """
    Rewrite of original function to add additional columns after each result
    in the change list.
    """
    for res in cl.result_list:
        rl = list(items_for_result(cl,res))
        for link in additional_links:
        rl.append(mark_safe(link['url_template'] % (VAR,))) # Make sure you have enough VARs for any %s's in your extra content. Note mark_safe
        yield rl

def extended_result_list(cl, additional_links):
    """
    Rewrite of original function to add an additional columns after each result
    in the change list.
    """
    headers = list(result_headers(cl))
    for header in additional_links:
        headers.append(header)

    return {
        'cl': cl,
        'result_headers': headers,
        'results': list(results(cl, additional_links))
    }

# This function is an example template tag for use in an overridden change_list.html template.
def my_model_result_list(cl):
    additional_links = (
        { 'text': 'Actions',
          'sortable': False,
          'url_template': '<td>YOUR ADDITIONAL CONTENT HERE</td>'
        },
    )

    return extended_result_list(cl, additional_links)
my_model_result_list = register.inclusion_tag("admin/change_list_results.html")(my_model_result_list)

Then create a change_list template (in project/<YOUR_TEMPLATE_DIR>/admin/<APP>/<MODEL>/change_list.html)

{% extends "admin/change_list.html" %}
{% load change_list_extras %}
{% block result_list %}{% my_model_result_list cl %}{% endblock %}

A variation on the above, using Template

I wanted to add columns to my model but I didn't like the above method very much, mostly because I would have to state variables in the results function, which I wanted to generalize away together with extended_result_list.

Instead of using Python string format (i.e. the (VAR,) above), I take advantage of Django's template system.

First, move results and extended_result_list away. I created a separate module called admin_extras and put them (rewritten) in __init__.py:

admin_extras/__init__.py

from django.contrib.admin.templatetags import admin_list
from django import template
from django.utils.safestring import mark_safe

def results(cl, col_templates):    

    for res in cl.result_list:
        rl = list(admin_list.items_for_result(cl, res))
        for col in col_templates:
            rendered_col = col.render(template.Context({'obj': res}))
            rl.append(mark_safe(rendered_col))
        yield rl

def extended_result_list(cl, additional_cols):
    headers = list(admin_list.result_headers(cl))
    headers.extend(additional_cols)
    
    # Parse the templates once
    col_templates = [template.Template(col['template']) for col in additional_cols]
    
    return {
        'cl': cl,
        'result_headers': headers,
        'results': list(results(cl, col_templates))
    }

These functions can now be hooked up to any model using a custom tag for it. I happen to have an Article-model and want to display an extra column with publishing options.

I wouldn't really recommend the name change_list_extras, since you probably want one of these for every model (to keep them decoupled). <model_name>_change_list_extras or <model_name>_admin_extras are probably better candidates.

articles/templatetags/change_list_extras.py

import admin_extras
from django import template

register = template.Library()

@register.inclusion_tag("admin/change_list_results.html")
def article_result_list(cl):
    additional_cols = (
        {'text': 'Actions', 'sortable': False, 'template': '<td><a href="{% url publish_view obj.id %}">Publish</a></td>'},
    )
    return admin_extras.extended_result_list(cl, additional_cols)
# article_result_list = register.inclusion_tag("admin/change_list_results.html")(article_result_list)

Note that I've changed 'additional_links' to 'additional_cols' and 'url_template' to more generic 'template'. Inside 'template', I can use the context variable 'obj' to refer to the current row. If you should be so inclined, you could of course modify article_result_list to send a dictionary of its own to merge with the one in results.

I haven't tested using {% url %} myself, but I see no reason why it wouldn't work!

You use this in a custom change_list-template for your model admin:

{% extends "admin/change_list.html" %}
{% load change_list_extras %}
{% block result_list %}{% article_result_list cl %}{% endblock %}

Another Way

Add a method to your model and the insert the method name in the list_display.

from django.utils.safestring import mark_safe

class MyModel(models.Model):
        # ...field definitions...
        
        def my_column(self):
            return mark_safe(u'<a href="">link</a>')
        my_column.allow_tags = True
        
class MyModelAdmin(admin.ModelAdmin):
        list_display = ('my_column',)

Note that by default the content returned from my_column will be escaped. To avoid this, allow_tags must be set to true and the string should be marked as safe. Here is two other options that might be handy:

# Change header titleof the column:
my_column.short_description = 'My nice column'
# Make column show the icon for true/false
my_column.boolean = True
# Make column sortable. Note it is ONLY possible to sort by an already existing model field
# and not on the actually values returned by the my_column method. 
my_column.admin_order_field = 'some_field'

Q: I want to add some field specific template content in my change form. How, how?

A: Copy the change_form.html file from the admin app. The fields are looped over in the following bit of code:

    {% for bound_field_line in bound_field_set %}
        {% for bound_field in bound_field_line %}
        {% admin_field_line bound_field_line %}
        {% filter_interface_script_maybe bound_field %}
        {% endfor %}
    {% endfor %}

You can get at your field name with the following check:

        {% ifequal bound_field.field.verbose_name "MY_FIELD_NAME" %}

So the loop becomes:

    {% for bound_field_line in bound_field_set %}
        {% for bound_field in bound_field_line %}
        {% ifequal bound_field.field.verbose_name "MY_FIELD_NAME" %}
            My field specific content
        {% else %}
        {% admin_field_line bound_field_line %}
        {% endifequal %}
        {% filter_interface_script_maybe bound_field %}
        {% endfor %}
    {% endfor %}

Q: How do I add new object tools to the top right of my change form?

A: Create an admin/app/model/change_form.html template and override the object-tools block:

{% extends "admin/change_form.html" %}
{% load i18n admin_modify adminmedia %}
{% block object-tools %}
{% if change %}{% if not is_popup %}
  <ul class="object-tools">
      <li><a href="MY_NEW_LINK" class="viewsitelink">{% trans "MY NEW LINK TEXT" %}</a></li>
      <li><a href="history/" class="historylink">{% trans "History" %}</a></li>
  </ul>
{% endif %}{% endif %}
{% endblock %}

Q: How do I get at the ID of my model in an inline row?

A: It's a bit of an effort but the following works for me:

        {% if inline_admin_form.original %}
            {{inline_admin_form.pk_field.field.form.initial.id}}
        {% endif %}

Q: How do I get at the original model in a template?

A: The admin app will pass the original model in as 'original' in the context:

{{original.YOUR_FIELD_HERE}}

Q: Okay. This is a tricky one. Not sure if you'll be able to help, but do you know how to generate a DOCX or ODF file from in the admin?

A: Why surely I do. As both of these file types are just zip files full of xml and binaries you can use the following models:

class ReportTemplate(models.Model):
    """ReportTemplates are used to generate DOCX and ODT files.

    They are zip files holding text (xml) and binary
    files.
    """
    name = models.CharField(maxlength=200)
    extension = models.CharField(maxlength=10)
    media_dir = models.CharField(maxlength=100)
    content_type = models.CharField(maxlength=300)

    def __unicode__(self):
        return self.name

class ReportTemplateXML(models.Model):
    """XML file to include in ReportTemplate"""
    report_template = models.ForeignKey(ReportTemplate, related_name="xml_files")
    text = models.TextField()
    zip_path = models.CharField(maxlength=200)
    render = models.BooleanField() # If true then the file will be passed a context and rendered.

    class Meta:
        ordering = ['-render', 'zip_path']

class ReportTemplateFile(models.Model):
    """Binary file to store in the model"""
    report_template = models.ForeignKey(ReportTemplate, related_name="media_files")
    file_to_include = models.FileField(blank=True, upload_to="report_templates/files")
    zip_path = models.CharField(maxlength=200)

class Screenshot(models.Model): 
    """This model holds additional binary files for inclusion
    It must have the 'image' field to be used with the views below."""
    image = models.ImageField(blank=True, upload_to="screenshots", height_field='height', width_field='width')

class ReportTemplateXMLInlines(admin.StackedInline):
    model = ReportTemplateXML
    extra = 1

class ReportTemplateFileInlines(admin.TabularInline):

    model = ReportTemplateFile
    extra = 3

class ReportTemplateOptions(admin.ModelAdmin):
    inlines = [ReportTemplateXMLInlines, ReportTemplateFileInlines]
    save_on_top = True

admin.site.register(ReportTemplate, ReportTemplateOptions)

You'll then need something similar to the following in your views:

@staff_member_required
def get_document(request, temp, rep_id):
    template = get_object_or_404(ReportTemplate, name=temp)
    report = get_object_or_404(Report, pk=rep_id) # The Report model holds all the information used in the ReportTemplate

    filename = report.SOME_FIELD
    filename = slugify(filename)
    filename += "."+template.extention

    response = HttpResponse()
    response['Content-Type'] = template.content_type
    response['Content-Disposition'] = 'attachment; filename=' + filename

    c = Context(
            {'report': report,
            ... Other context stuff here ...
            }
        )

    media_files = Screenshot.objects.all() # Or some other binary files to add in.

    response.write(_report_gen_from_model(template, c, media_files, template.media_dir))
    return response

def _report_gen_from_model(template, c, media, media_zip_dir):
    """ Build a zip file from a ReportTemplate instance.
        
        All XML files are rendered and all Files are included.
        The media files will also be included. These are assumed
        to be screenshots.
        Based on the Django equivalent from Javier Rivera
    """
    # Prepare the file
    fake_file = StringIO()
    document = zipfile.ZipFile(fake_file, mode="w", compression=zipfile.ZIP_DEFLATED)
    media_dir = reportwriter.settings.MEDIA_ROOT

    for xml in template.xml_files.all():
        path = str(xml.zip_path)
        text = xml.text.encode('utf-8')
        if xml.render:
            temp = Template(text)
            document.writestr(path, temp.render(c).encode('utf-8'))
        else:
            document.writestr(path, text)
            

    for file_ in template.media_files.all():
        fn = str(media_dir+file_.file_to_include)
        document.write(fn, file_.zip_path.encode('latin-1'))

    # Any additional media files are now zipped up
    for m in media:
        file_ = media_dir+m
        path = media_zip_dir+m
        document.write(str(file_), str(path))
    document.close()
    return fake_file.getvalue()

Shiny.

N.B. You'll have to learn how to edit ODT and DOCX xml by yourself. Good luck with that.

Q: Where did prepopulate_from go? My SlugField don't work!

A: There's no longer a prepopulate_from option on database Field classes. Instead, there's a new prepopulated_fields option for your admin class. This should be a dictionary mapping field names (as strings) to lists/tuples of field names that prepopulate them

Q: What happened to filter_interface?

A: Use filter_vertical and filter_horizontal on the ModelAdmin class like so:

class MyModelAdmin(admin.ModelAdmin):
    filter_horizontal = ('some_field',)

Q: How do I add custom JavaScript/CSS?

Just pass a media definition to ModelAdmin, eg:

class PageModelAdmin(admin.ModelAdmin):
    class Media: 
        js = ('/some/form/javascript',) 
        css = { 
        'all': ('/some/form/css',) 
        } 
  
admin.site.register(Page, PageModelAdmin)

Q: How do I add custom validation?

One option is to set the .form attribute of your ModelAdmin subclass to a subclass of ModelForm customized to your Model. Then override clean().

from django import forms
from django.contrib import admin
form myproject.myapp.models import MyModel

class MyModelForm(forms.ModelForm):
    class Meta:
        model = MyModel
    def clean(self):
        pass # Your custom validation here

class MyModelAdmin(admin.ModelAdmin):
    form = MyModelForm

If you want custom validation for a field in the admin and newforms ModelForm instances you can subclass a field in your model and override formfield(). You can then return the form field you like or a complete custom form field.

Here we add regex validation to a field:

class UraniaEventID(models.CharField):
    def formfield(self, **kwargs):
        defaults = {'regex':r"^[A-Za-z][1-9][1-7][0-9]$"}
        defaults.update(kwargs)
        return super(models.CharField, self).formfield(form_class=forms.RegexField, **defaults)

class Event(models.Model):
    urania_event_id = UraniaEventID(max_length=4)

Q: How do I use updated_at and created_on type fields but stop them being displayed?

You'll need to override a few methods: two to hide the fields and two to populate them with the correct values.

Let's say you have this model.

class Article(models.Model):
    headline = models.CharField(max_length=500)
    text = models.TextField()

    created = models.DateField()
    updated = models.DateTimeField()

And you want to use created and updated in your list view, but not in the change and add views.

class ArticleOptions(admin.ModelAdmin):
    save_on_top = True
    list_display = ('headline', 'updated', 'created')

    hidden_fields = ("created", "updated") # Define which fields you'd like to be hidden here.

    def get_fieldsets(self, request, obj=None):
        superclass = super(ArticleOptions, self)
        fieldsets = superclass.get_fieldsets(request, obj)

        # Here we cycle through the fieldsets and remove the created and updated
        # fields by name. Factoring this out left as an exercise for the reader.
        for fs in fieldsets:
            fs[1]['fields'] = [f for f in fs[1]['fields']
                                 if f not in self.hidden_fields]
        return fieldsets

    def add_view(self, request):
        if request.method == "POST":
            # Change the created field to be a datetime object.
            request.POST['created'] = datetime.datetime.today()
            # The admin will be expecting date and time as separate fields
            # as the default widget is rendered as two text fields.
            # You can get away with a date object for the date
            request.POST['updated_0'] = datetime.date.today()
            # But it's easier to poke in a text string for the time (less typing).
            request.POST['updated_1'] = datetime.datetime.now().strftime("%H:%M:%S")
        return super(ArticleOptions, self).add_view(request)

    def change_view(self, request, obj_id):
        if request.method == "POST":
            old_issue = Article.objects.get(id=obj_id)
            # Here we use the old created date as we're changing, not adding.
            request.POST['created'] = old_issue.created
            # Same as above for the updated time
            request.POST['updated_0'] = datetime.date.today()
            request.POST['updated_1'] = datetime.datetime.now().strftime("%H:%M:%S")
        return super(ArticleOptions, self).change_view(request, obj_id)

Q: How can I pass extra context variables into my add and change views?

You can do this by overriding render_change_form in the ModelAdmin.

class ArticleOptions(admin.ModelAdmin):
    save_on_top = True

    def render_change_form(self, request, context, *args, **kwargs):
        extra = {
            'has_file_field': True # Make your form render as multi-part.
        }

        context.update(extra)
        
        superclass = super(ReportOptions, self)
        return superclass.render_change_form(request, context, *args, **kwargs)

Q: How can I pass extra context variables into my index page?

You can't. At least not in the same way as above.

There are two ways you can do this at present, the messy way or the 'proper' way.

The messy way is as follows. Create a file called extras.py in your project's root directory (where urls.py lives). In it create a copy of the index method from django.contrib.admin.sites (from the class AdminSite). Modify the method to add in your extra context variables.

In your urls.py import your new method and the 'new' module.

from django.contrib import admin
from your_project.extras import new_index_method
import new

Now you'll need to replace the existing index method with your new one

admin.site.index = new.instancemethod(new_index_method)

this isn't that pretty but it works.

The 'proper' way will be to create your own AdminSite instance and register your models to it. The problem with this method is that you will also need to register all of the models from the auth app if you want to manage your users.

from your_project.extras import my_admin_site_instance
...
urlpatterns = patterns('',
    # Override index page to add in fortune output
    (SITE_BASE+r'/(.*)', my_admin_site_instance.root),
...

Q: How can I change where I get sent after saving a new entry to the database?

A: You'll need to override the save_add method in your ModelAdmin. In the example below I've got a link on one model's change form which points to the add view of another and passes along the GET variable "rep_id". In the overriden method I check for this and tweak the location field of the HTTPResponseRedirect. Overriding save_add gives us the added advantage of having all validation etc performed beforehand for us.

from django.utils.encoding import iri_to_uri
...

    def save_add(self, request, form, formsets, post_url_continue):
        rep_id = request.GET.get("rep_id", None)
        if rep_id:
            result = super(IssueOptions, self).save_add(request, form, formsets, post_url_continue)
            result['Location'] = iri_to_uri("/issuesdb/reports/report/%s/" % rep_id)
            return result
        return super(IssueOptions, self).save_add(request, form, formsets, post_url_continue)

Q: How do I filter the ChoiceField based upon attributes of the current ModelAdmin instance?

A: This is currently worked on in ticket #3987. However, this patch is not likely to be implemented, as it is not backward compatible.

There is another way that works with 1.0 just fine. Override the _ _ call _ _ method to add the request object to the current ModelAdmin instance. At that point, you can reference the request object inside formfield_for_dbfield, and retrieve the current Model instance, which you may then use to filter a ChoiceField based upon the current values in the Model. The following example filters a ChoiceField based upon attributes of the the current Model instance. The same thing may be done within an InlineAdmin, retrieving the parent object instance, and using it to filter the ChoiceField on the InlineAdmin instances:

class SiteAdmin(ModelAdmin):
    def __call__(self, request, url):
        #Add in the request object, so that it may be referenced
        #later in the formfield_for_dbfield function.
        self.request = request
        return super(SiteAdmin, self).__call__(request, url)
    
    def formfield_for_dbfield(self, db_field, **kwargs):
        field = super(SiteAdmin, self).formfield_for_dbfield(db_field, **kwargs) # Get the default field
        if db_field.name == 'home_page': 
            #Add the null object
            my_choices = [('', '---------')]
            #Grab the current site id from the URL.
            my_choices.extend(Page.objects.filter(site=self.request.META['PATH_INFO'].split('/')[-2]).values_list('id','name'))
            print my_choices
            field.choices = my_choices
        return field