Django admin: Prefill data when clicking the add-a

2020-07-17 15:16发布

问题:

In the Django Admin, when you modify an objects properties, if there is a ForeignKey, it will have a dropdown box of all the choices, plus a "+" button to add more choices. When I click on it, I want to prefill some of the data.

I've noticed I could do it if I could modify the URL (example: http://localhost:8000/admin/app/model/add/?field=value where field and value are programmatically modified.)

I figure I have to override something in the forms.ModelForm that the admin.ModelAdmin uses, but I'm not sure what.

回答1:

Django allows you to replace a request's GET dict (which it uses to pre-populate the admin form).

Django will automatically fill values from URL GET parameters if you are sending field values of model form in the URL.

For example, considering "http://myhost/admin/app/model/add/?name=testname", it will prefill the name field of the form in the admin add-view template with the value 'testname'.

But, if you are sending any id in your URL, you need to modify the GET parameters by overriding the add_view function.

Taken from stackoverflow answer

class ArticleAdmin(admin.ModelAdmin):
    // ...

    def add_view(self, request, form_url='', extra_context=None):
        source_id = request.GET.get('source',None)
        if source_id != None:
            source = FeedPost.objects.get(id=source_id)
            // any extra processing can go here...
            g = request.GET.copy()
            g.update({
                'title':source.title,
                'contents':source.description + u"... \n\n[" + source.url + "]",
            })

            request.GET = g

        return super(ArticleAdmin, self).add_view(request, form_url, extra_context)

It just an example.DO it with Your model and fields :)



回答2:

This is an alternative to my other answer.

This alternative does not use JavaScript, so it works server-side only, which has a lot of drawbacks. Nevertheless, some parts of the puzzle may be of use to someone.

The basic idea is this:

The "Add another" (+) button provides a link to the related model admin view.

The URL for this link is constructed in the related_widget_wrapper.html template.

This template uses a url_params variable, which is created in the RelatedFieldWidgetWrapper.get_context() method.

To pre-fill the related form, we need to append our custom URL parameters to this url_params variable, in the context of the widget, so we can utilize the default widget template.

The simplest way to achieve this, as far as I can see, is to create a wrapper for the RelatedFieldWidgetWrapper.get_context() method.

We then extend the ModelAdmin.formfield_for_dbfield() method, and use our custom wrapper there, so it has access to the current request.

As far as I know, the formfield_for_dbfield method is not mentioned in the ModelAdmin docs...), but this is where Django applies the RelatedFieldWidgetWrapper.

Here's a minimal example:

from django.db import models
from django.contrib import admin


class Organization(models.Model):
    pass


class Participant(models.Model):
    organization = models.ForeignKey(to=Organization, on_delete=models.CASCADE)


class Event(models.Model):
    organization = models.ForeignKey(to=Organization, on_delete=models.CASCADE)
    participants = models.ManyToManyField(to=Participant)


def wrap_extra_url_params(original_get_context, extra_url_params: str):
    def get_context_with_extra_url_params(*args, **kwargs):
        context = original_get_context(*args, **kwargs)
        if 'url_params' in context:
            context['url_params'] += '&{}'.format(extra_url_params)
        return context
    return get_context_with_extra_url_params


class EventAdmin(admin.ModelAdmin):
    def formfield_for_dbfield(self, db_field, request, **kwargs):
        formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
        if db_field.name == 'participants':
            # get event organization from current request
            event_id = request.resolver_match.kwargs.get('object_id', None)
            if event_id:
                event = Event.objects.get(id=event_id)
                # override the "add another participant" link
                formfield.widget.get_context = wrap_extra_url_params(
                    original_get_context=formfield.widget.get_context,
                    extra_url_params='organization={}'.format(
                        event.organization_id))
        return formfield


admin.site.register(Organization)
admin.site.register(Participant)
admin.site.register(Event, EventAdmin)

Note that the server-side has no way of knowing whether the user changes the selected organization, so it only works when modifying existing Event instances.

This works, to a certain degree, but I would go for the JavaScript option.



回答3:

This is old, but it can be quite a puzzle, and the accepted answer did not address the true issue for me.

The question, as I understand it, is how to pre-fill data on the related model's admin form, which pops up after you click the green + ("Add another") next to a widget for a ForeignKey or ManyToManyField. This is illustrated below.

As suggested in the (currently) accepted answer, as well as here, this can be achieved by adding URL parameters for each of the fields that you want to pre-fill. This is also illustrated in the image.

The question that remains unanswered, however, is:

How, exactly, can we add these URL parameters to the URL used by the + ("Add another") link?

Assuming the pre-filled values depend on a the current state of the form on the client side, e.g. the selected organization in the image above, I suppose it makes sense to use JavaScript.

Note: If the pre-filled values do not depend on the form state, nor on the current request, other options might be more appropriate.

Here's a minimal example that was used to create the image above:

models.py: (we put the admin stuff in here as well for convenience)

from django.db import models
from django.contrib import admin


class Organization(models.Model):
    pass


class Participant(models.Model):
    organization = models.ForeignKey(to=Organization, on_delete=models.CASCADE)


class Event(models.Model):
    organization = models.ForeignKey(to=Organization, on_delete=models.CASCADE)
    participants = models.ManyToManyField(to=Participant)


admin.site.register(Organization)
admin.site.register(Participant)
admin.site.register(Event)

Now we extend the Django admin change_form template (based on the docs) for our Event model with a little bit of JavaScript.

Follow these instructions to set-up your templates folder, and don't forget to add you templates folder to DIRS in settings.TEMPLATES.

templates/admin/my_app/event/change_form.html:

{% extends 'admin/change_form.html' %}
{% load static %}

{% block admin_change_form_document_ready %}
{{ block.super }}

<script type="text/javascript">
var add_participant_url = document.getElementById("add_id_participants").href;
var organization = document.getElementById("id_organization")
organization.onchange = function() {
    document.getElementById("add_id_participants").href =
        add_participant_url + "&organization=" + organization.value;
};
</script>

{% endblock %}

DISCLAIMER: I have very limited experience with JavaScript, so any suggestions for improvement are welcome.

Basically, whenever the user changes the selected organization on the "Change event" form, the URL for the "Add another participant" (+) link is updated with the selected value, so the organization field on the "Add participant" form will be pre-filled.