Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django admin: Prefill data when clicking the add-another button next to a ForeignKey dropdown

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.

like image 813
DrRobotNinja Avatar asked Jan 22 '14 01:01

DrRobotNinja


3 Answers

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 :)

like image 184
Aks Avatar answered Oct 20 '22 05:10

Aks


This is an alternative to my other answer.

This alternative does not use JavaScript, so it works server-side only, which has a number 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.

NOTE: If you are trying to do something similar for a raw-id-field, there is an easier way, viz. by extending ForeignKeyRawIdWidget.url_parameters() (source).

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.

like image 30
djvg Avatar answered Oct 20 '22 05:10

djvg


Aks's answer didn't work for me as I couldn't find the source field in request.GET dict. However, I was able to make it work by adding the following function to my TestAdmin class - This is the admin that opens up on clicking the add button

def get_form(self, request, obj=None, **kwargs):
    form = super(TestAdmin, self).get_form(request, obj, **kwargs)

    // using URL pattern to get the refrrer object id 
    admin_referer = re.match(".*(/adminame/)(.+)(/change/)", 
                                        request.META.get('HTTP_REFERER'))

    if request.GET.get('_popup', None) and admin_referer:
        test_model = TestModel.objects.get(id=admin_referer[2])
        form.base_fields['prepopulate_field'].initial = test_model
        form.base_fields['prepopulate_field'].widget.can_add_related = False
        form.base_fields['prepopulate_field'].widget.can_change_related = False
        form.base_fields['prepopulate_field'].widget.can_delete_related = False
        
    return form 

I would have preferred a better way to fetch the referer's object id, but I could only make this way to work.

like image 44
Afsan Abdulali Gujarati Avatar answered Oct 20 '22 06:10

Afsan Abdulali Gujarati