Django with Haystack....customizing forms for templates

Hi there all over the world. Today I#m gonna talk about fulltext-search implementation in Django with Haystack. (My actual search engine is whoosh, but i don’t think that matters for question).

As the build in forms from Haystack are not very pretty (AND in some parts in English, which i could not use for a german APP) , I simply tried to customize the build in forms of haystack to fullfill my needs.
After some struggeling, i managed it somehow. with ONE little issue left:

My class/styles properties to format the q field (Standard query field that comes with haystack-package) are only working on first page display. after submitting a search-query, the format jumps back to the haystack-basic for the q-field.

my views.py:

def search(request):
    context = {}
    context['form'] = MyModelSearchForm
    return render(request, 'search/search.html', context)

so you see, nothing special.

my adapted haystack form:

from django import forms
from django.utils.encoding import smart_str
from django.utils.text import capfirst

from haystack import connections
from haystack.constants import DEFAULT_ALIAS
from haystack.query import SearchQuerySet
from haystack.utils import get_model_ct
from haystack.forms import SearchForm, EmptySearchQuerySet
from haystack.utils.app_loading import haystack_get_model


def model_choices(using=DEFAULT_ALIAS):
    choices = [
        (get_model_ct(m), capfirst(smart_str(m._meta.verbose_name_plural)))
        for m in connections[using].get_unified_index().get_indexed_models()
    ]
    return sorted(choices, key=lambda x: x[1])


class MySearchForm(SearchForm):

    q = forms.CharField(
        required=False,
        widget=forms.TextInput(attrs={
            'class': "form-control rounded-start-2",
            'placeholder': "Suchbegriff eingeben...",
            "type": "search"}),
        )

    def __init__(self, *args, **kwargs):
        self.searchqueryset = kwargs.pop("searchqueryset", None)
        self.load_all = kwargs.pop("load_all", False)

        if self.searchqueryset is None:
            self.searchqueryset = SearchQuerySet()

        super().__init__(*args, **kwargs)

    def no_query_found(self):
        """
        Determines the behavior when no query was found.
        By default, no results are returned (``EmptySearchQuerySet``).
        Should you want to show all results, override this method in your
        own ``SearchForm`` subclass and do ``return self.searchqueryset.all()``.
        """
        return EmptySearchQuerySet()

    def search(self):
        if not self.is_valid():
            return self.no_query_found()

        if not self.cleaned_data.get("q"):
            return self.no_query_found()

        sqs = self.searchqueryset.auto_query(self.cleaned_data["q"])

        if self.load_all:
            sqs = sqs.load_all()

        return sqs

    def get_suggestion(self):
        if not self.is_valid():
            return None

        return self.searchqueryset.spelling_suggestion(self.cleaned_data["q"])


class MyModelSearchForm(MySearchForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["models"] = forms.MultipleChoiceField(
            choices=model_choices(),
            required=False,
            widget=forms.CheckboxSelectMultiple,
        )

    def get_models(self):
        """Return a list of the selected models."""
        search_models = []

        if self.is_valid():
            for model in self.cleaned_data["models"]:
                search_models.append(haystack_get_model(*model.split(".")))

        return search_models

    def search(self):
        sqs = super().search()
        return sqs.models(*self.get_models())

and my template code for this part of page:

    <div class="container-fluid d-grid justify-content-center bg-primary bg-gradient border-bottom border-dark pb-1">
            <div class="input-group justify-content-center border border-1 rounded-2 border-dark mt-1">
                {{ form.q }}
                <button type="submit" value=" suchen" class="btn btn-primary">
                    <i class="fas fa-search"></i>
                </button>
            </div>
    </div>

So on first toggling of page i see this:
1

after submitting a searchterm this:
2

I think, haystack is sending its basic properties to template form after submitting a query, but i can not identify the code passage where i would have to interact with to solve the problem…

This is something that could be caused by the form, the view, or any JavaScript running in that page.

What I would suggest as a first step would be to use your browser’s developer tools to examine the html and css being used to render this form, to identify what is different between these two cases. Then you can begin your search to locate where those changes are being made.

Hi there, following your advice i got what I was still assuming in my initial post:
(There is no javascript running at the page at the moment)

After first calling the page/template, i got exactly the style tags i additionally put in my costomized haystack search form:

class MySearchForm(SearchForm):

    q = forms.CharField(
        required=False,
        widget=forms.TextInput(attrs={
            'class': "form-control rounded-start-2",  <== Here and
            'placeholder': "Suchbegriff eingeben...",  <==here
            "type": "search"}),
        )

simply reloading the page does noething to that. submitting a search query gives me the results but the changes in design and THIS in the code:

2

which is exactly the plain style of the haystack SearchForm without my custom additions in MySearchForm…so it seems that on submitting a query, there is called the Original SearchForm instead of MySearchForm…

What does the form tag in the rendered html look like?

One thing I noticed:
You have:

    context['form'] = MyModelSearchForm

where it should be:

    context['form'] = MyModelSearchForm()

(You’re referencing the class itself instead of an instance of that class.)

Yeah, you are right about the parenthesis…i simply forgot that, thank you. But adding it doesn’t unfortunately solve the problem…concerning your question about the form tag:


initially:

<form method="get" action=".">
<input type="hidden" name="csrfmiddlewaretoken" value="uGSybJOLfqnpQuHj8X9Tgb359tyR0ICQORObuC5Ve8jcYurRcqiC0Wx7NZe8frHZ">
    <div class="container-fluid d-grid justify-content-center bg-primary bg-gradient border-bottom border-dark pb-1">
            <div class="input-group justify-content-center border border-1 rounded-2 border-dark mt-1">
                <input type="search" name="q" class="form-control rounded-start-2" placeholder="Suchbegriff eingeben..." id="id_q">
                <button type="submit" value=" suchen" class="btn btn-primary">
                    <svg class="svg-inline--fa fa-magnifying-glass" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="magnifying-glass" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" data-fa-i2svg=""><path fill="currentColor" d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"></path></svg><!-- <i class="fas fa-search"></i> Font Awesome fontawesome.com -->
                </button>
            </div>
    </div>   
</form>


after searching something:

<form method="get" action=".">
<input type="hidden" name="csrfmiddlewaretoken" value="KIX7BV3oBcJYkPoJ960p5yJchf9glbm44TTKUOkyAUFLsP8hdz98PjdeVLPxAUrd">
    <div class="container-fluid d-grid justify-content-center bg-primary bg-gradient border-bottom border-dark pb-1">
            <div class="input-group justify-content-center border border-1 rounded-2 border-dark mt-1">
                <input type="search" name="q" value="searchterm" id="id_q">
                <button type="submit" value=" suchen" class="btn btn-primary">
                    <svg class="svg-inline--fa fa-magnifying-glass" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="magnifying-glass" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" data-fa-i2svg=""><path fill="currentColor" d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"></path></svg><!-- <i class="fas fa-search"></i> Font Awesome fontawesome.com -->
                </button>
            </div>
    </div>
</form>

What version of Python / Django are you using? (I don’t see anything in Haystack referencing Django 4.x compatibility, and since there have been some “internals” changes in how forms are rendered in 4.1, there’s the possibility that Haystack isn’t fully compatible.)

Side note: I’d also question the use of action="." vs action="", but whether that makes a difference at all depends in part in how your urls are defined.

Another possibility is that Haystack is doing something with that widget itself, and that you would need to set those css classes on the field after the parent class has been initialized.

That’s a funny thing. The “.” is from some code snippet out of a “getting started with haystack and Django” tutorial…if you remove it, the search doesn’t work anymore…if you replace it with the url of search page, the design issue is gone but the search doesn’t work anymore either…

To the other thing:
it’s Django 4.2.2. and Python 3.11…and maybe the assumption about a compatibility issue is a good explanation why this behaviour is not explainable while looking to the corresponding code…

So, until this can be finally fixed, i found a workaround i can live with:
as i am displaying a logo on page before first search, i use the logo-trigger for formatting the query-field:

...
{% if logo != 'yes' %}
    <div class="form-control rounded-start-2">{{ form.q }}</div>
{% else %}
    {{ form.q }}
{% endif %}
...

so i get this on initial load of page:
1

and this after performing a search operation:
2

The “problem” is still there, but i consider it more “like intended” :slight_smile: