yourlabs / django-autocomplete-light

A fresh approach to autocomplete implementations, specially for Django. Status: v4 alpha, v3 stable, v2 & v1 deprecated.
https://django-autocomplete-light.readthedocs.io
MIT License
1.8k stars 468 forks source link

Select2 Initial Values #1076

Open CoryADavis opened 5 years ago

CoryADavis commented 5 years ago

I'm working with a large FormWizard, and the way that users will interact with the data after form submission is such that Foreign Key fields in the model would be a problem. So, I'm using the plain Select2, not ModelSelect2.

The Select2 works! You can submit the form, and the selection comes through just fine. But, if you go to edit, the initial values populate for everything but the example Select2 field. It's not just a display issue, if you submit the edit, the value is blanked out in the database.

My question, is this supported and not working as a bug, or implementation failure on my part? Or is this not supported? If not, how might this be accomplished? I've tried setting the initial in the choice field manually, but that didn't appear to be the answer like in other issues I saw while searching my problem.

-- EDIT START --

I solved my problem, it's not supported, using ModelSelect2 is specifically what's required to get the information needed to set the initial values. I used a combination of seemingly hacky nonsense to get it to work without using a foreign key. Is there a more proper way to do this? If so, would that be a good addition to the documentation?

hacky forms.py snippet

class CustomModelChoiceField(forms.ModelChoiceField):
    def to_python(self, value):
        if value in self.empty_values:
            return None
        try:
            key = self.to_field_name or 'pk'
            value = self.queryset.get(**{key: value})
        except (ValueError, TypeError, self.queryset.model.DoesNotExist):
            raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
        return value.name

class Form1(autocomplete.FutureModelForm):
    example = CustomModelChoiceField(widget=CustomSelect2(url='autocomplete-example'),
                                  queryset=ExampleChoices.objects.all(),
                                  to_field_name='name')

hacky widgets.py snippet (just the filter_choices_to_render)

class Select2BootstrapWidgetMixin(object):
    class Media:
        extend = True
        css = {'all': ('css/select2-bootstrap4.css',)}

    def build_attrs(self, *args, **kwargs):
        attrs = super(Select2BootstrapWidgetMixin, self).build_attrs(*args, **kwargs)
        attrs.setdefault('data-theme',
                         'bootstrap4')
        return attrs

    def filter_choices_to_render(self, selected_choices):
        """Filter out un-selected choices if choices is a QuerySet."""
        self.choices.queryset = self.choices.queryset.filter(
            choice__in=[c for c in selected_choices if c]

-- EDIT END --

models.py snippet

class Example(models.Model):
    example = models.CharField(max_length=500, blank=True, null=True)

forms.py snippet (CustomSelect2 is just Select2 with media for bootstrap styling)

class Form1(autocomplete.FutureModelForm):
    example = forms.ChoiceField(widget=CustomSelect2(url='autocomplete-site'))

    class Meta:
        model = Example

forms.py alternate snippet (This is the failed setting initial attempt)

class Form1(autocomplete.FutureModelForm):
    def __init__(self, *args, **kwargs):
        super(Form1, self).__init__(*args, **kwargs)
        example = self.initial.get('example', None)
        if example:
            self.fields['example'] = forms.CharField(widget=CustomSelect2(url='autocomplete-site', attrs={'data-placeholder': f'Un-edited Value: {example}'}), initial=example)

views.py snippet

class ExampleAutocomplete(autocomplete.Select2QuerySetView):
    def get_queryset(self):
        qs = Select2Choices.objects.filter(field='example').only('name').order_by('name')
        if self.q:
            qs = qs.filter(choice__istartswith=self.q)

        return qs

    def get_result_value(self, result):
        """Return the value of a result."""
        return str(result.name)
jpic commented 5 years ago

Nice 0day hack !

Accepted, let's see what a pull request looks like :joy:

CoryADavis commented 5 years ago

Apologies, I'm really naive when it comes to this sort of thing! Does that mean my code has some sort of vulnerability in it?

jpic commented 5 years ago

Well you said your patches were hacks, and you found them the day you opened the issue so it's like a 0day hack you hacker you.

I don't understand the last filter_choices_to_render:

    self.choices.queryset = self.choices.queryset.filter(
        choice__in=[c for c in selected_choices if c]

Shouldn't it be more like ?

    filter(**{
       self.to_field_name or 'pk' + '__in': [c for c in selected_choices if c]
    })
CoryADavis commented 5 years ago

OH, haha! You're likely right about the filter_choices_to_render, will test the change!

jpic commented 5 years ago

Go ahead and submit a pull request if you feel like it.

Have fun !

moorchegue commented 4 years ago

I went with a similar hack recently:

class BaseForm(forms.ModelForm):

    form_specific_option = None

    field1 = autocomplete.Select2ListChoiceField(
        get_all_options(),
        widget=autocomplete.ListSelect2(url='autocomplete-url'),
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        field1_widget = self.fields['field1'].widget
        field1_widget.initial = self.initial.get('field1', '')
        field1_widget.forward = [
            'field2',
            forward.Const(self.form_specific_option, 'option'),
        ]

I was wondering why displaying the initial value is not the default behavior in the first place?

joaquimds commented 3 years ago

Thanks for the help everyone! I was struggling populating the value for a field that doesn't have a defined list of choices in advance.

In case of anyone searching from google like me - keywords are: populate, initial value, Select2ListCreateChoiceField


class FlatUsageForm(forms.ModelForm):
    project_name = autocomplete.Select2ListCreateChoiceField(
        widget=autocomplete.Select2(
            url="project-autocomplete", attrs={"data-tags": "true"}
        ),
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.instance.id:
            project_name_field = self.fields['project_name']
            project_name = self.instance.project.name
            project_name_field.initial = project_name
            project_name_field.choices = [(project_name, project_name)]
joaquimds commented 3 years ago

Also see here: https://github.com/yourlabs/django-autocomplete-light/issues/924

jilljenn commented 1 year ago

This should definitely be documented somewhere

pcustic commented 1 year ago

I can't believe this isn't documented somewhere! This fix was crazy hard to find.

rene-schwabe commented 1 year ago

Someone can help me solve this problem with "Select2Multiple" If I use the example above the Multiselect-Field is populated but the value is a single list item.

class ExportTemplateForm(forms.ModelForm):
    content_type = forms.ModelChoiceField(
        queryset=ContentType.objects.all(),
        required=True,
        widget=autocomplete.ModelSelect2(url="custom_export:content_type-autocomplete"),
        label="Datenbank",
    )
    columns = forms.MultipleChoiceField(
        required=False, widget=autocomplete.Select2Multiple(), label="Felder"
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        content_type = self.instance.content_type if self.instance.pk else None
        if content_type:
            model = content_type.model_class()
            self.fields["columns"].widget.url = "custom_export:column-autocomplete"
            self.fields["columns"].choices = [
                (field.name, field.verbose_name) for field in model._meta.fields
            ]
            self.fields["columns"].widget.attrs.update(
                {"data-placeholder": "Wählen Sie Felder aus..."}
            )
            self.fields["columns"].widget.forward = ["content_type"]

            # Setze die initialen Werte für das columns-Feld basierend auf der Modellinstanz
            if self.instance.id:
                project_name_field = self.fields["columns"]
                project_name = self.instance.columns
                project_name_field.initial = project_name.split(",")
                project_name_field.choices = [(project_name, project_name)]
rohitgarud commented 2 months ago

Someone can help me solve this problem with "Select2Multiple" If I use the example above the Multiselect-Field is populated but the value is a single list item.

class ExportTemplateForm(forms.ModelForm):
    content_type = forms.ModelChoiceField(
        queryset=ContentType.objects.all(),
        required=True,
        widget=autocomplete.ModelSelect2(url="custom_export:content_type-autocomplete"),
        label="Datenbank",
    )
    columns = forms.MultipleChoiceField(
        required=False, widget=autocomplete.Select2Multiple(), label="Felder"
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        content_type = self.instance.content_type if self.instance.pk else None
        if content_type:
            model = content_type.model_class()
            self.fields["columns"].widget.url = "custom_export:column-autocomplete"
            self.fields["columns"].choices = [
                (field.name, field.verbose_name) for field in model._meta.fields
            ]
            self.fields["columns"].widget.attrs.update(
                {"data-placeholder": "Wählen Sie Felder aus..."}
            )
            self.fields["columns"].widget.forward = ["content_type"]

            # Setze die initialen Werte für das columns-Feld basierend auf der Modellinstanz
            if self.instance.id:
                project_name_field = self.fields["columns"]
                project_name = self.instance.columns
                project_name_field.initial = project_name.split(",")
                project_name_field.choices = [(project_name, project_name)]

@rene-schwabe were you able to solve this issue, facing the same.. Thank you