Code


Version 8 (modified by mrts, 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: How do I use the admin app?
  2. Q: How do I change the admin options?
  3. Q: How do I set up edit_inlines?
  4. Q: How do I change the attributes for a widget on a field in my model.
  5. Q: How do I change the attributes for a widget on a single field in my model.
  6. Q: I've defined a new permission. How do I enforce it?
  7. 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?
  8. Q: How to I add an extra column to the change list view?
  9. Q: I want to add some field specific template content in my change form. How, how?
  10. Q: How do I add new object tools to the top right of my change form?
  11. Q: Oh. Your. God. What is that ridiculous bit of text on every row of my edit_inline 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 SlugFields don't work!
  16. Q: What happened to filter_interface?
  17. Q: How do I add custom javascript?
  18. Q: How do I add custom validation?

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 separate from the models themselves. You'll have to define a class inheriting from admin.ModelAdmin and define your options in there. E.g.

class MyModelOptions(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 the options with the model you pass the ModelAdmin class as the second option to the register function

admin.site.register(MyModel, MyModelOptions)

Q: How do I set up edit_inlines?

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

class MyOtherModelInlines(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 MyOtherModelInlines(admin.StackedInline):
    model = MyOtherModel
    extra = 2
    template = 'my_new_template_tabular.html'

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

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

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 MyOtherModelInlines(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 db_field.formfield(**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 = db_field.formfield(**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 attrs
        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 MyModelOptions(admin.ModelAdmin):
    save_on_top = True

    def change_view(self, request, obj_id):
        if request.user.has_perm("my_new_permission"):
            return super(MyModelOptions, 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 to 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
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(link['url_template'] % (VAR,)) # Make sure you have enough VARs for any %s's in your extra content.
        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)
report_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 %}

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 MyModelOptions(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: Oh. Your. God. What is that ridiculous bit of text on every row of my edit_inline form?

A: That's your model's repr, that is. Why is it there? No idea. To get rid of it you'll need to create a copy of the 'admin/edit_inline/(tabular|stacked).html' template and replace the following lines:

        <td class="original">{% if inline_admin_form.original or inline_admin_form.show_url %}<p>
          {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %}
          {% if inline_admin_form.show_url %}<a href="/r/{{ inline_admin_form.original.content_type_id }}/{{ inline_admin_form.original.id }}/">View on site</a>{% endif %}
            </p>{% endif %}

with:

        <td class="original">

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 SlugFields 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 options 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 Options class like so. E.g.

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

Q: How do I add custom javascript?

See #6619.

Q: How do I add custom validation?

Use newforms as you would normally. Just look at the ModelAdmin class to see the hooks.