djangowagtail

Dynamic initial values for a Django Model


How would i go about prepopulating a field in the Wagtail Admin UI with HTTP GET parameters? I have seen some solutions using the deprecated contrib.ModelAdmin, but have not really found something using the new ModelViewSet.

My simplified use case would be a simple calender in the Admin UI (using some javascript calender like fullcalandar.js) where i would create a new Event by dragging a timeframe and having the browser visit an url like /admin/event/new?start=startdate&end=enddate showing the Event Form with the start and end fields being prepoulated by the timeframe.

I have the following model

class Event(models.Model):
   title = models.CharField(max_length=255)
   [...]
   
class EventOccurrence(Orderable):
   event = ParentalKey(Event, on_delete=models.CASCADE, related_name='event_occurrence')
   start = DateTimeField()
   end = DateTimeField()

So far i have tried to use an Custom Form Class inherting from WagtailAdminModelForm, which works nicely for the prepopulating, but i have no way to access the request object to fetch the GET paramters.

Helpful AIs would like me to use the deprecated ModelAdmin or inject some javascript to prepopulate the fields on the frontend. My personal hail mary would be to create the event via an API and the just refer the user to the freshly created event, but i would like to avoid that :) I found some references to a CreatePageView, but this does not seem to exist anymore in modern Wagtail.

I have the slight suspicion that there is a more straightforward solution. Something like creating my own route and just shadowing and overwriting some basis class and methods. But my wagtail/django-fu is definitely not strong enough to figure out where to start).

Maybe somebody can at least point me in the right direction? Or maybe even has an idea, how i could use a ChooserWidget for the same task? Or at least a modal embedding the form with the prepopulated fields.


Solution

  • You get access to the request instance in FieldPanel.BoundPanel. You can use this to set the initial value if there is no value already on the field.

    This works for most widget cases, but some of the Wagtail ones need something extra to set the initial value (including the AdminDateTimeInput widget). You can do this in the render_html() method, though this does feel a little hacky, but there is zero in the docs on how these widgets work and the code comments don't provide any info either. I have used this for other cases where I needed to overcome the same obstacle.

    Maybe there is a better way for this, but this works at least:

    from bs4 import BeautifulSoup
    from dateutil.parser import parse
    from django.forms.fields import CharField, DateTimeField
    from django.utils import timezone
    from django.utils.safestring import mark_safe
    from wagtail.admin.panels import FieldPanel
    from wagtail.admin.widgets import AdminDateTimeInput
    
    
    class PrepopulatePanel(FieldPanel):
        """
        Prepopulate form fields in the Wagtail admin interface based on URL parameters.
        Attributes:
            url_parameter (str): The URL parameter used to prepopulate the field. Defaults to the field name if not provided.
        Inner Classes:
            BoundPanel:
                Handles the logic for prepopulating the field value and rendering the HTML with the initial value.
                Methods:
                    __init__(**kwargs):
                        Initializes the BoundPanel and sets the initial value of the field based on the URL parameter.
                    render_html(parent_context):
                        Renders the HTML for the panel, ensuring that the initial value is correctly set in the widget.
        """
        def __init__(
            self, field_name, url_parameter=None, *args, **kwargs
        ):
            # Set the url_parameter to the field_name if not provided
            self.url_parameter = url_parameter or field_name 
            super().__init__(field_name, *args, **kwargs)
    
        def clone_kwargs(self):
            super_kwargs = super().clone_kwargs()
            super_kwargs['url_parameter'] = self.url_parameter
            return super_kwargs
        
        class BoundPanel(FieldPanel.BoundPanel):
            def __init__(self, **kwargs):
                super().__init__(**kwargs)
                if not self.bound_field.value():
                    value = self.request.GET.get(self.panel.url_parameter)
                    if value:                
                        match self.bound_field.field:
                            case DateTimeField():
                                try:
                                    init_value = parse(value)
                                    # convert value to db timezone if tz component supplied
                                    if timezone.is_aware(init_value):
                                        init_value = timezone.localtime(init_value)
                                    self.bound_field.field.initial = init_value
                                except ValueError:
                                    value = None
                            case CharField():
                                # If the field is a CharField, we can set the initial value directly
                                self.bound_field.field.initial = value
                            # add other cases as needed 
    
            @mark_safe
            def render_html(self, parent_context):
                # Wagtail widgets are not always rendered with the initial value - fix it here
                html = super().render_html(parent_context)
                if not self.bound_field.value() and self.bound_field.field.initial:
                    match self.bound_field.field.widget:
                        case AdminDateTimeInput():
                            try:
                                parsed = self.bound_field.field.initial.strftime(self.bound_field.field.widget.format)
                                soup = BeautifulSoup(html, "html.parser")
                                input_element = soup.find("input")
                                if input_element:
                                    input_element["value"] = parsed
                                    return str(soup)
                            except (ValueError, TypeError):
                                pass
                        # add other cases as needed 
    
                return html                              
    

    CharField is in there to demo how it works for straightforward cases - no extra rendering needed.

    Replace FieldPanel in your content_panels declaration with

    PrepopulatePanel('start_date'),
    

    or, if the query string parameter differs from the field name

    PrepopulatePanel('start_date', 'some_parameter'),
    

    Example url: http://localhost:8000/admin/pages/31/edit/?start_date=2025-12-30T093000%2B1200

    This pulls in 30-DEC-2025 0930 UTC+12 (NZDT). The db (in the example) is set to UTC so the input is converted (29-DEC-2025 2030). The widget automatically converts that to the time zone setting according to the editor's user profile then back again on save.