Implement fulltext search function
This commit is contained in:
parent
b256d12fee
commit
34e845d14d
9 changed files with 210 additions and 5 deletions
|
@ -207,7 +207,6 @@ ul.tag-list {
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
max-width: 500px;
|
||||
text-indent: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
@ -292,3 +291,22 @@ table.list tbody td {
|
|||
.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;
|
||||
}
|
||||
|
|
1
inventory/static/inventory/img/search.svg
Normal file
1
inventory/static/inventory/img/search.svg
Normal 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 |
|
@ -23,6 +23,29 @@
|
|||
{% endblock %}
|
||||
</ul>
|
||||
<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>
|
||||
<a href="{% url 'manufacturer-list' %}"><img class="icon" title="Manufacturers" src="{% static "inventory/img/manufacturer.svg" %}"></a>
|
||||
</li>
|
||||
|
@ -33,7 +56,7 @@
|
|||
<a href="{% url 'tag-list' %}"><img class="icon" title="Tags" src="{% static "inventory/img/tags.svg" %}"></a>
|
||||
</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>
|
||||
</ul>
|
||||
|
||||
|
|
15
inventory/templates/inventory/search.html
Normal file
15
inventory/templates/inventory/search.html
Normal 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 %}
|
43
inventory/templates/inventory/search_result.html
Normal file
43
inventory/templates/inventory/search_result.html
Normal 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 %}
|
17
inventory/templates/inventory/search_result_item.html
Normal file
17
inventory/templates/inventory/search_result_item.html
Normal 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>
|
|
@ -33,7 +33,8 @@ from .views import (
|
|||
DistributorView,
|
||||
ManufacturerView,
|
||||
IndexView,
|
||||
TagView
|
||||
TagView,
|
||||
SearchView
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
|
@ -52,5 +53,6 @@ urlpatterns = [
|
|||
path('distributor/<int:pk>', DistributorView.as_view(), name='distributor-detail'),
|
||||
path('tags', TagListView.as_view(), name='tag-list'),
|
||||
path('tag/<int:pk>', TagView.as_view(), name='tag-detail'),
|
||||
path('search', SearchView.as_view(), name='search'),
|
||||
path('', IndexView.as_view(), name='index')
|
||||
]
|
||||
|
|
|
@ -6,6 +6,7 @@ from .manufacturer import ManufacturerView, ManufacturerListView
|
|||
from .workshop import WorkshopView, WorkshopListView
|
||||
from .index import IndexView
|
||||
from .tag import TagListView, TagView
|
||||
from .search import SearchView
|
||||
|
||||
__all__ = [
|
||||
AreaView, AreaListView,
|
||||
|
@ -15,5 +16,6 @@ __all__ = [
|
|||
ManufacturerView, ManufacturerListView,
|
||||
WorkshopView, WorkshopListView,
|
||||
IndexView,
|
||||
TagView, TagListView
|
||||
TagView, TagListView,
|
||||
SearchView
|
||||
]
|
84
inventory/views/search.py
Normal file
84
inventory/views/search.py
Normal 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
|
||||
})
|
Loading…
Reference in a new issue