Search code examples
pythondjangolistdrop-down-menusearchable

Searchable Dropdown in Website - Django - Saving to data base


I am building a website and part of it involves a drop-down which is easily called in the HTML by {{ form.inspection_type }}. This is a pretty big drop-down and a pain to scroll all the way to what you need to select so I have been trying for the past few days to implement a searchable drop-down to replace it. I have tried every single library I would find on searchable drop-downs and non have worked with the specifics of my project. I have reverted to doing it through HTML and have again gone through about 40 different examples and iterations and have gotten close. The NEW code is a searchable drop-down that is populated with my Django database drop-down. However when I click submit it shows that no data was added to the list view that I have. Below is the old code that worked and didn't have a searchable drop-down and also I have added the new code that has the searchable drop-down but won't submit the data through. Let me know if I missed anything and you have any questions. Thanks!

OLD - works but no searchable drop-down

<form method="post" action="">
    {{ form.inspection_type }}
    <input class="btn btn-primary" value="Submit">
</form>

NEW - doesn't submit but has searchable drop-do

<form method="post" action="">
    <input list="Dealer" name="Dealer">
    <datalist class="SmallFormField" name="Dealer" id="Dealer" onChange="submitForm(this.form, true)" onkeypress="gblKeyPress()" onkeydown="gblKeyDown()" onfocus="gblOnFocus('Dealer')" onblur="gblOnBlur()" onClick="gblOnClick('QuoteMgrSearchCriteria', this.form)">
    <option value="">Select a Dealer ...</option>
    <option value="2919">{{ form.inspection_type }}</option>
    <input class="btn btn-primary" value="Submit">
</form>

EDIT:

Working with no search: enter image description here

Not submitting with search: enter image description here

forms.py

class SheetForm_Building(forms.ModelForm):
class Meta:
    model = Sheet_Building
    fields = '__all__'
    exclude = ['user']

    widgets = {
        'inspection_type': forms.Select(choices=INSPECTION_TYPE_BI, attrs={'class': 'form-control'}),
        'department': forms.Select(choices=DEPARTMENT_BI, attrs={'class': 'form-control'}),
        'concern': forms.Select(choices=CONCERN_BI, attrs={'class': 'form-control'}),
        'hazard_level': forms.Select(choices=HAZARD_LEVEL_BI, attrs={'class': 'form-control'}),
        'building_address': forms.Select(choices=BUILDING_ADDRESS_BI, attrs={'class': 'form-control'}),
        'floor': forms.Select(choices=FLOOR_LEVEL_BI, attrs={'class': 'form-control'}),
        'location': forms.Select(choices=LOCATION_BI, attrs={'class': 'form-control'}),
        'codes': forms.Select(choices=CODES_BI, attrs={'class': 'form-control'}),
    }

models.py:

class Sheet_Building(models.Model):
    user = models.ForeignKey(User, default=True, related_name="Building", on_delete=models.PROTECT)
    date = models.DateField(blank=True, null=True, verbose_name='Inspection Date')
    time = models.TimeField(blank=True, null=True, verbose_name='Inspection Time')
    inspection_type = models.CharField(max_length=16, choices=INSPECTION_TYPE_BI, blank=True, null=True, verbose_name='Inspection Type')
    flname = models.CharField(max_length=25, blank=True, null=True, verbose_name='Inspector')
    report_date = models.DateField(blank=True, null=True, verbose_name='Report Date')
    department = models.CharField(max_length=29, choices=DEPARTMENT_BI, blank=True, null=True, verbose_name='Department')
    responsible_name = models.CharField(max_length=25, blank=True, null=True, verbose_name='Responsible Person')
    building_address = models.CharField(max_length=52, choices=BUILDING_ADDRESS, blank=True, null=True, verbose_name='Building and Address')
    floor = models.CharField(max_length=8, choices=FLOOR_LEVEL_BI, blank=True, null=True, verbose_name='Floor / Level')
    room = models.CharField(max_length=35, blank=True, null=True, verbose_name='Area / Room')
    location = models.CharField(max_length=10, choices=LOCATION_BI, blank=True, null=True, verbose_name='Location')
    priority = models.IntegerField(blank=True, null=True, verbose_name='Priority')
    hazard_level = models.CharField(max_length=20, choices=HAZARD_LEVEL_BI, blank=True, null=True, verbose_name='Hazard Level')
    concern = models.CharField(max_length=31, choices=CONCERN_BI, blank=True, null=True, verbose_name='Concern')
    codes = models.CharField(max_length=51, choices=CODES_BI, blank=True, null=True, verbose_name='Element and Code')
   correction = models.TextField(max_length=160, blank=True, null=True, verbose_name='Corrective Action')
    image = models.ImageField(blank=True, null=True, verbose_name='Image', upload_to='gallery')
    notes = models.TextField(max_length=500, blank=True, null=True, verbose_name="Inspector's note")

    class Meta:
        ordering = ['-pk']

    def __str__(self):
        return self.flname or 'None'

    def get_absolute_url(self):
        return reverse('list_building')

EDIT2:

These are the changes I've done to get your suggested view to get it a little closer

INSPECTION_TYPE_BI = (
    ('Building Code', 'Building Code'),
    ('DEP / EPA', 'DEP / EPA'),
    ('DEP / OSHA', 'DEP / OSHA'),
    ('DEP / EPA / OSHA', 'DEP / EPA / OSHA'),
    ('Electrical Code', 'Electrical Code'),
    ('Elevator Code', 'Elevator Code'),
    ('Fire Code', 'Fire Code'),
    ('Laboratory', 'Laboratory'),
    ('Life Safety', 'Life Safety'),
    ('Multi-Media', 'Multi-Media'),
    ('OSHA', 'OSHA'),
    ('Playground', 'Playground'),
    ('Satellite Area', 'Satellite Area'),
    ('Town of Amherst', 'Town of Amherst'),
    ('Health', 'Health'),
    ('Kitchen', 'Kitchen'),
)

def adddata_building(request):
    if request.method == "POST":
        # To make sure that the option we selected is being sent in the form data
        print(request.POST)
    form = SheetForm_Building()
    optionList = INSPECTION_TYPE_BI[1:]
    return render(request, "testapp/layout.html", {
        "form": form,
        "optionList": optionList
    })

This is a form I typically use that works to submit but it doesn't have a searchable dropdown functionality.

def adddata_building(response):
    if response.method == 'POST':
        form = SheetForm_Building(response.POST, response.FILES)
        if form.is_valid():
            instance = form.save(commit=False)
            instance.user = response.user
            instance.save()
            response.user.Building.add(instance)
            return redirect('list_building')
    else:
        form = SheetForm_Building()
    return render(response, 'sheets/add_data/add_data_building.html', {'form': form})

And this is where I currently am in integrating the two to have a form with a searchable dropdown that can also submit. This submits the form and all other fields in the form however it does not submit the searchable dropdown field. It shows all other fields that has been submitted and their values but shows no indication that the searchable dropdown field has any value to it submitted...

def adddata_building(response):
    if response.method == 'POST':
        form = SheetForm_Building(response.POST, response.FILES)
        optionList = INSPECTION_TYPE_BI[1:]
        if form.is_valid():
            instance = form.save(commit=False)
            instance.user = response.user
            instance.save()
            response.user.Building.add(instance)
            return redirect('list_building')
    else:
        form = SheetForm_Building()
        optionList = INSPECTION_TYPE_BI[1:]
    return render(response, 'sheets/add_data/add_data_building.html', {'form': form, 'optionList': optionList})

Solution

  • Okay, I'm not sure why, in the form inspection type you have 2 values only for some of the options.

    In Django, when creating the options/choices list, data is entered as a list of tuples [(a,b), (c,d) ...] with the first option in the tuple being the value that is actually stored and the second option being the label/human-readable part in the dropdown. You can see what I'm talking about here. That's the first potential issue I'm noticing; the fact that you only have 2 values for some of them.

    The second thing I'm noticing is that, in the new version, your option value is hardcoded to be 2919. I think that that may be the main cause of the issue. If you were to print request.POST you'd see a key-value pair of 'Dealer': ['2919'] in the QueryDict that is printed. You're submitting a value which is probably not matching anything in your list.

    So, I personally would recommend doing something similar to what I've done before:

    <form method="post" action="">
        <input list="Dealer" name="Dealer">
        <datalist class="SmallFormField" name="Dealer" id="Dealer" onChange="submitForm(this.form, true)" onkeypress="gblKeyPress()" onkeydown="gblKeyDown()" onfocus="gblOnFocus('Dealer')" onblur="gblOnBlur()" onClick="gblOnClick('QuoteMgrSearchCriteria', this.form)">
        <option value="">Select a Dealer ...</option>
        {% for option in optionList %}
        <option value="{{option.0}}">{{ option.1 }}</option>
        {% endfor %}
        <input class="btn btn-primary" value="Submit">
    </form>
    

    Just pass in whatever optionsList you've defined directly to the template and use the templating language's for loop to populate the options in the tag.

    Hope this works for you

    EDIT:

    Alright, I've got a clearer idea of how you're generating your forms. Thanks for that extra info. Here's what I would do. I'm pretty sure someone out there would have a better method, but for now this will work for you.

    Django does not have an inbuilt widget for datalists, and customizing existing widget files/ creating new widgets is a bit too complicated for me. So what I'm going to do is show you a way to fit the code I've written earlier with what you've got currently.

    In views.py:

    from .models import Sheet_Building
    from .forms import SheetForm_Building
    
    def YourViewName(request):
        if request.method == "POST":
            # To make sure that the option we selected is being sent in the form data
            print(request.POST)
        form = SheetForm_Building()
        optionList = Sheet_Building.INSPECTION_TYPE_BI[1:]
        return render(request, "testapp/layout.html", {
            "form": form,
            "optionList": optionList
        })
    

    In the html file, I've spread the form fields (as was shown here), so that we render the dataList where it would usually appear in the form, instead of right at the beginning or at the end.

    We're going to loop through all fields in the form until we come across form.inspection_type. At this point, we won't use the default Django input field, but rather define our own in the html. So {{ field.label_tag }} will be used as is, but {{ field }} will be replaced by the code for the datalist.

    Here is what the file will look like:

    <form action="" method="post">
      {% csrf_token %}
      {% for field in form %}
      <div class="fieldWrapper">
      {{ field.errors }}
      {% if field == form.inspection_type %}
      {{ field.label_tag}}
      <input list="id_inspection_type" name="inspection_type">
      <datalist name="inspection_type" id="id_inspection_type">
      <option value="">Select a Dealer ...</option>
      {% for option in optionList %}
      <option value="{{option.0}}">{{ option.1 }}</option>
      {% endfor %}
      </datalist>
      {% else %}
      {{ field.label_tag }} {{ field }}
      {% endif %}
      {% if field.help_text %}
      <p class="help">{{ field.help_text|safe }}</p>
      {% endif %}
      </div>
      {% endfor %}
      <input type="submit" value="submit">
    </form>
    

    And here is a screenshot of the request.POST:

    Screenshot of submitted form data

    As you can see, the option selected in the datalist (Electrical Code) was submitted through the form without any issues.

    EDIT 2:

    I used the ID and name (Dealer) that you've used in your code. The problem with using this is that in your model Sheet_Building, you have a field called inspection_type. Your form SheetForm_Building is created from this model. So it also has a field inspection_type.

    When you try to save this form, it will make sure that the values submitted in the POST request are the same as what were defined.

    Now what's happening in your case is that when you're submitting data with the name Dealer, the formData gets a key with the same name, which is not what your instance/model was expecting. So although the data has been submitted through the form, it was not mapped to anything because your model has no field Dealer. This is why even though everything else gets saved, this particular field has a value of None.

    To fix this issue, keep the name attribute the same as what you've defined in your form. ie. inspection_type

    <input list="id_inspection_type" name="inspection_type">
    <datalist id="id_inspection_type">