1

I am very new to web development and specifically using Django Framework.

I am trying to find a clean, efficient and non external package dependant implementation for an autocomplete-datalist form field inside a Generic class based CreateView template in Django.

I have found numerous resources on various implementations, but most of them depend on external packages(autocomplete-light, jqueryCDN, etc.) and none of it is based on a class based generic CreateView.

I have been experimenting and I have managed to make the autocomplete-datalist work in a way but I am stuck in a very simple problem when I try to post my form with the data from the datalist element.

I get a validation error:

"city_name: This field is required"

I also noticed that the city object queried from the database inside the datalist has also the id of the city_name

models.py

from django.db import models


class City(models.Model):
    name = models.CharField(max_length=50)

    class Meta:
        verbose_name_plural = "cities"
        ordering = ['name']

    def __str__(self):
        return self.name


class Person(models.Model):
    first_name = models.CharField(max_length=40)
    last_name = models.CharField(max_length=40)
    address = models.CharField(max_length=150)
    city_name = models.ForeignKey(City, on_delete=models.CASCADE)

    def __str__(self):
        return f'{self.first_name} {self.last_name}'

views.py

from django.views.generic import ListView, CreateView
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Person, City
from .forms import PersonForm
# Create your views here.


class PersonList(LoginRequiredMixin, ListView):
    model = Person
    template_name = "home.html"
    paginate_by = 20
    login_url = "/login/"
    redirect_field_name = 'redirect_to'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        return context


class PersonCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView):
    model = Person
    template_name = "testform.html"
    login_url = "/login/"
    form_class = PersonForm
    success_url = 'testapp/add/'
    success_message = 'Person registered successfully!'
    redirect_field_name = 'redirect_to'

forms.py

from django import forms
from .models import Person, City


class PersonForm(forms.ModelForm):

    class Meta:
        model = Person
        fields = ["first_name", "last_name", "address", "city_name"]

testform.html

{% extends 'home.html' %}
{% load static %}
{% block content %}
{% if messages %}
  {% for message in messages %}
  <div class="alert alert-success alert-dismissible fade show" role="alert">
    <span style="font-size: 18px;padding: 1mm"><i class="fa-solid fa-circle-check"></i></span>{{ message }}
    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
  </div>
  {% endfor %}
{% endif %}
    <form method="POST">
        {% csrf_token %}
    
        <div class="mb-3">
            <label for="first_name charfield" class="form-label"> First Name</label>
            {{form.first_name}}
        </div>
        <div class="mb-3">
            <label for="last_name charfield" class="form-label">Last Name</label>
            {{form.last_name}}
        </div>
        <div class="mb-3">
            <label for="address charfield" class="form-label">Address</label>
            {{form.address}}
        </div>
        <div class="mb-3">
            <label for="city_name datalist" class="form-label">City Name</label>
            <input type="text" list="cities" class="form-control">
                <datalist id="cities">
                    {% for city in form.city_name %}
                    <option>{{ city }}</option>
                    {% endfor %}
                </datalist>
        </div>
        <button class="btn btn-outline-primary" type="submit">Submit</button>
    </form>
    {{form.errors}}
{% endblock %}

Result:

testform_html output

I believe it is a necessary feature for all modern web applications to have this kind of functionality within their database query-autocomplete form fields system. It is a pity that although Django provides this feature for the AdminModels through its autocomplete_fields attribute, it makes it so hard to implement on Generic Class Based Views on the actual application models.

How can I approach this issue, and is there a efficient and more optimized way to implement it?

halfer
  • 19,824
  • 17
  • 99
  • 186

3 Answers3

0

If you don't want a field required you can set the attribute blank=True in the model class. A question I have is why would you want to have a Foreignkey to just a city name. Or are you trying to use the a list of cities to populate the drop down? In that case the Foreign Key is definitely not the answer.

Pat
  • 81
  • 4
  • I am using this as an example for implementation on a wider range of Django relational models and how to apply the autocomplete feature on a Form with a relational field. It is very easy to implement it inside the AdminModels but I am still trying to make it work in a Generic CreateView as shown in my code. I use the Foreign Key to populate the drop down datalist but on a Form Post, it does not save. – OldSchoolProgrammer Feb 05 '23 at 12:55
  • What do you mean by autocomplete? Are you looking for something like google search where the users search is guessed. If someone is typing Texas when they type 'T' all the states with 'T' pop up? Or are you looking to fill in a form like when you checkout and your name and address are autopolutated? Either way you're not going to get around an external package. – Pat Feb 05 '23 at 19:01
  • I am trying to implement an input field of city names inside a form where the user types the letters of a city and the prepopulated list of city names popup according the user input. i.e types 'T' and the list shows 'Tokyo','Toronto','Taipei' etc and as the user types more consecutive letters the list narrows down to the cities according the user input.@Pat – OldSchoolProgrammer Feb 06 '23 at 17:34
  • I would recommend HTMX for that. Although its an external package there is a django package called django-htmx. Their documentation has this exact example. https://htmx.org/examples/active-search/ – Pat Feb 06 '23 at 18:03
0

This is an example I have to autocomplete a list of Titles from a database table - 'get_titles'. It uses a view to provide the title values Then in HTML I have produced a data list within a search field that enables autocomplete.

Views.py

########################## Autocomplete Search Field ##############

@require_GET
def get_titles(request):
    term = request.GET.get('term')
    if term:
        #titles = ComicInput.objects.filter(Title__icontains=term).annotate(Title=F('Title')).values('Title').distinct()
        titles = ComicInput.objects.filter(Category = 'Sell').filter(Title__icontains=term).values('Title')
    else:
        titles = []
    title_list = [title['Title'] for title in titles] # Convert QuerySet to list
    return JsonResponse(title_list, safe=False)

Html

<script>
        var $j = jQuery.noConflict();
        $j(document).ready(function() {
            $j("#search").autocomplete({
                source: function(request, response) {
                    $j.getJSON("{% url 'get_titles' %}", {
                        term: request.term
                    }, function(data) {
                        var uniqueValues = [];
                        $j.each(data, function(index, value) {
                            if ($j.inArray(value, uniqueValues) === -1) {
                                uniqueValues.push(value);
                            }
                        });
                        response(uniqueValues);
                    });
                },
                minLength: 2,
                select: function(event, ui) {
                    $j("#search").val(ui.item.label);
                    $j("#search-form").submit();
                }
            }).autocomplete("widget").addClass("custom-autocomplete");
    });
</script>

<style>
    .custom-autocomplete {
        position: absolute;
        padding: 20px 0;
        width: 200px;
        background-color: #DBF9FD;
        border: 1px solid #ddd;
        max-height: 200px;
        overflow-y: auto;
    }
    .custom-autocomplete li {
        padding: 5px;
        cursor: pointer;
    }
    .custom-autocomplete li:hover {
        background-color: #eee;
    }
</style>

</head>

<body>

<form action="{% url 'search_page' %}" method="get" class="search-bar"  style="width: 200px; height: 30px;font-size: small; align: top;" >
                            <input list="Titles" type="search"  id ="search" name="search" pattern=".*\S.*" placeholder="Search Collections by Title" onFocus="this.value=''" >
                                <datalist id="title-options">
                                    {% for title in titles %}
                                        <option value="{{ title }}">
                                    {% endfor %}
                                </datalist>

        </div>
johnperc
  • 73
  • 8
0

I was looking for help with solving this question, but ended up rolling my own widget class. It's not actually as hard as it might seem. Subclass TextInput, and attach the desired datalist to what it generates. The relevant Django source is here.

Code and usage: The Widget class

from django.forms.widgets import TextInput
from django.utils.safestring import mark_safe

class DatalistTextInput(TextInput):
    def __init__(self, attrs=None):
        super().__init__( attrs)
        if 'list' not in self.attrs or 'datalist' not in self.attrs:
            raise ValueError(
              'DatalistTextInput widget is missing required attrs "list" or "datalist"')
        self.datalist_name = self.attrs['list']

        # pop datalist for use by our render method. 
        # its a pseudo-attr rather than an actual one
        # a string of option values separated by dunders ('__')
        self.datalist = self.attrs.pop('datalist') 

    def render(self, **kwargs):
        DEBUG( self, kwargs)
        part1 = super().render( **kwargs)
        opts = ' '.join(
            [ f'<option>{x}</option>' for x in self.datalist.split('__') ]
        )
        part2 = f'<datalist id="{self.datalist_name}">{opts}</datalist>'
        return part1 + mark_safe( part2)

And a form and a view to test it

class TestDatalist( forms.Form):
    foo = forms.CharField(
        max_length=10,
        widget = DatalistTextInput( attrs={
            'list':'foolist',
            'datalist': "foo__bar__baz__quux"
            }
    ))

class TestView( FormView):
    form_class = TestDatalist
    template_name = 'jobs/simple_form.html'
    success_url='/wafers/OK'
    initial={ 'foo':'b'}
    def form_valid( self, form):
        print( form.cleaned_data)  # in real life do something useful!
        return super().form_valid( form)

A snip of the generated HTML ({{form.as_table()}}:

<tr><th><label for="id_foo">Foo:</label></th>
<td><input type="text" name="foo" value="b" list="foolist" maxlength="10" required id="id_foo">
<datalist id="foolist">
  <option>foo</option> 
  <option>bar</option> 
  <option>baz</option> 
  <option>quux</option>
</datalist>
</td></tr>
nigel222
  • 7,582
  • 1
  • 14
  • 22