Implement fulltext search function

This commit is contained in:
Johannes Schriewer 2025-01-06 03:17:25 +01:00
parent b256d12fee
commit 34e845d14d
9 changed files with 210 additions and 5 deletions

View file

@ -207,7 +207,6 @@ ul.tag-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 6px; gap: 6px;
max-width: 500px;
text-indent: 0; text-indent: 0;
padding-left: 0; padding-left: 0;
} }
@ -291,4 +290,23 @@ table.list tbody td {
.right { .right {
text-align: right; text-align: right;
} }
#search-container {
display: none;
position: absolute;
background-color: #292981;
padding: 10px;
border: 1px solid white;
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.8);
}
.search-result em {
color: #c00000;
}
.search-result .small {
font-size: 9pt;
font-weight: normal;
color: #808080;
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path 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"/></svg>

After

Width:  |  Height:  |  Size: 463 B

View file

@ -23,6 +23,29 @@
{% endblock %} {% endblock %}
</ul> </ul>
<ul class="icon-nav"> <ul class="icon-nav">
<li>
<script>
function showSearch(e) {
const searchBox = document.getElementById("search-container");
if ((searchBox.style.display === '') || (searchBox.style.display === 'none')) {
searchBox.style.display = 'block';
searchBox.getElementsByTagName('input')[0].focus();
} else {
searchBox.style.display = 'none';
}
if (e) {
e.preventDefault();
}
}
</script>
<a href="{% url 'search' %}" onclick="showSearch(event)"><img class="icon" title="Search" src="{% static "inventory/img/search.svg" %}"></a>
<div id="search-container">
<form action="{% url 'search' %}" method="get">
<input name="q" id="search" type="text">
<button type="submit">Search</button>
</form>
</div>
</li>
<li> <li>
<a href="{% url 'manufacturer-list' %}"><img class="icon" title="Manufacturers" src="{% static "inventory/img/manufacturer.svg" %}"></a> <a href="{% url 'manufacturer-list' %}"><img class="icon" title="Manufacturers" src="{% static "inventory/img/manufacturer.svg" %}"></a>
</li> </li>
@ -33,7 +56,7 @@
<a href="{% url 'tag-list' %}"><img class="icon" title="Tags" src="{% static "inventory/img/tags.svg" %}"></a> <a href="{% url 'tag-list' %}"><img class="icon" title="Tags" src="{% static "inventory/img/tags.svg" %}"></a>
</li> </li>
<li> <li>
<a href="{% url 'workshop-list' %}"><img class="icon" title="Workshops" src="{% static "inventory/img/workshop.svg" %}"></a> <a href="{% url 'index' %}"><img class="icon" title="Workshops" src="{% static "inventory/img/workshop.svg" %}"></a>
</li> </li>
</ul> </ul>

View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Search{% endblock %}
{% block header_bar %}
Search
{% endblock %}
{% block content %}
<form method="get" action="{% url 'search' %}">
<input type="text" id="search" name="q">
<button type="submit">Search</button>
</form>
{% endblock %}

View file

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% load static %}
{% load hilight %}
{% block title %}Search{% endblock %}
{% block header_bar %}
Search
{% endblock %}
{% block content %}
<form method="get" action="{% url 'search' %}">
<input type="text" id="search" name="q" value="{{ q }}">
<button type="submit">Search</button>
</form>
<h2>Search result for '{{ q }}'</h2>
{% for result in results %}
<div class="search-result">
<h3><a href="{{ result.url }}">{{ result.title | hilight:tokens }}</a></h3>
{{ result.text | safe }}
<hr>
</div>
{% empty %}
<p>Noting found</p>
{% endfor %}
{% if pages > 1 %}
<div class="pagination">
{% if page > 1 %}
<a href="{% url 'search' %}?q={{ q }}&page=1"><img src="{% static 'inventory/img/first.svg' %}" class="icon" title="First page"></a>
<a href="{% url 'search' %}?q={{ q }}&page={{ page | add:'-1'}}"><img src="{% static 'inventory/img/previous.svg' %}" class="icon" title="Previous page"></a>
{% endif %}
{{ page }}/{{ pages }}
{% if page < pages %}
<a href="{% url 'search' %}?q={{ q }}&page={{ page | add:'1'}}"><img src="{% static 'inventory/img/next.svg' %}" class="icon" title="Next page"></a>
<a href="{% url 'search' %}?q={{ q }}&page={{ pages }}"><img src="{% static 'inventory/img/last.svg' %}" class="icon" title="Last page"></a>
{% endif %}
</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,17 @@
{% load static %}
{% load hilight %}
<p>
{% if item.documentation.all %}
<a class="datasheet" href="{{ item.documentation.all.0.file.url }}"><img class="icon" src="{% static "inventory/img/datasheet.svg" %}"></a>
{% endif %}
{{ item | hilight:tokens }}
<span class="small">{{item.form_factor.name}}</span>
</p>
<p>Contained in: <a href="{{ item.container_url }}?hilight={{ item.id }}">{{ item.container.display_name }}</a></p>
<ul class="tag-list">
{% for tag in item.all_tags %}
<li><a href="{% url 'tag-detail' tag.id %}" title="{{ tag.name }}">{{ tag.name }}</a></li>
{% empty %}
No tags
{% endfor %}
</ul>

View file

@ -33,7 +33,8 @@ from .views import (
DistributorView, DistributorView,
ManufacturerView, ManufacturerView,
IndexView, IndexView,
TagView TagView,
SearchView
) )
urlpatterns = [ urlpatterns = [
@ -52,5 +53,6 @@ urlpatterns = [
path('distributor/<int:pk>', DistributorView.as_view(), name='distributor-detail'), path('distributor/<int:pk>', DistributorView.as_view(), name='distributor-detail'),
path('tags', TagListView.as_view(), name='tag-list'), path('tags', TagListView.as_view(), name='tag-list'),
path('tag/<int:pk>', TagView.as_view(), name='tag-detail'), path('tag/<int:pk>', TagView.as_view(), name='tag-detail'),
path('search', SearchView.as_view(), name='search'),
path('', IndexView.as_view(), name='index') path('', IndexView.as_view(), name='index')
] ]

View file

@ -6,6 +6,7 @@ from .manufacturer import ManufacturerView, ManufacturerListView
from .workshop import WorkshopView, WorkshopListView from .workshop import WorkshopView, WorkshopListView
from .index import IndexView from .index import IndexView
from .tag import TagListView, TagView from .tag import TagListView, TagView
from .search import SearchView
__all__ = [ __all__ = [
AreaView, AreaListView, AreaView, AreaListView,
@ -15,5 +16,6 @@ __all__ = [
ManufacturerView, ManufacturerListView, ManufacturerView, ManufacturerListView,
WorkshopView, WorkshopListView, WorkshopView, WorkshopListView,
IndexView, IndexView,
TagView, TagListView TagView, TagListView,
SearchView
] ]

84
inventory/views/search.py Normal file
View file

@ -0,0 +1,84 @@
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views.generic import View
from django.shortcuts import render
from django.urls import reverse
from django.template.loader import get_template
from django.db.models import Q
from django.core.paginator import Paginator
from django.conf import settings
from re import finditer
from inventory.models import Item
@method_decorator(login_required, name='dispatch')
class SearchView(View):
def get(self, request):
page = int(request.GET.get('page', "1"))
query = request.GET.get('q', None)
if not query:
return render(request, "inventory/search.html")
results: list[dict[str, str]] = []
tokens = query.split(" ")
tokens = map(str.lower, map(str.strip, tokens))
q: Q = None
item_template = get_template("inventory/search_result_item.html")
t: list[str] = []
for token in tokens:
combiner = 'or'
if token.startswith("+"):
token = token[1:]
combiner = 'and'
elif token.startswith("-"):
token = token[1:]
combiner = 'and not'
t.append(token)
q1 = Q(name__icontains=f'{token}')
q2 = Q(description__icontains=f'{token}')
q3 = Q(tags__name__icontains=f'{token}')
q4 = Q(tags__description__icontains=f'{token}')
q5 = Q(form_factor__tags__name__icontains=f'{token}')
q6 = Q(form_factor__tags__description__icontains=f'{token}')
qx = q1 | q2 | q3 | q4 | q5 | q6
if q == None:
q = qx
elif combiner == 'or':
q = q | qx
elif combiner == 'and':
q = q & qx
elif combiner == 'and not':
q = q & ~qx
items = Item.objects.filter(q).select_related("container", "form_factor").prefetch_related("tags", "documentation").distinct()
# FIXME: Rank search results
paginator = Paginator(items, getattr(settings, "PAGE_SIZE", 10))
for item in paginator.get_page(page):
text = item_template.render({
"item": item,
"tokens": t
})
result = {
"title": item.name,
"text": text,
"url": reverse('item-detail', kwargs={"pk": item.pk})
}
results.append(result)
return render(request, "inventory/search_result.html", {
"q": query,
"results": results,
"tokens": t,
"page": page,
"pages": paginator.num_pages
})