feat: implement search
This commit is contained in:
parent
db45e163a9
commit
917e4caf9a
|
@ -1,5 +1,5 @@
|
|||
root = true
|
||||
|
||||
[*.html]
|
||||
[*.{html,js}]
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from .forms import SearchForm
|
||||
from .signals import populate_nav
|
||||
|
||||
|
||||
|
@ -10,3 +11,7 @@ def nav(request):
|
|||
]
|
||||
|
||||
return {"nav_items": nav_items}
|
||||
|
||||
|
||||
def search(request):
|
||||
return {"search_form": SearchForm()}
|
||||
|
|
|
@ -15,3 +15,21 @@ class AddStockForm(forms.Form):
|
|||
|
||||
|
||||
AddStockFormSet = forms.formset_factory(AddStockForm, extra=0)
|
||||
|
||||
|
||||
class SearchForm(forms.Form):
|
||||
PER_PAGE_CHOICES = [
|
||||
(10, "10"),
|
||||
(25, "25"),
|
||||
(50, "50"),
|
||||
(100, "100"),
|
||||
]
|
||||
|
||||
q = forms.CharField(label=_("Search"), required=False)
|
||||
per_page = forms.TypedChoiceField(
|
||||
choices=PER_PAGE_CHOICES,
|
||||
coerce=int,
|
||||
empty_value=25,
|
||||
label=_("Results per page"),
|
||||
widget=forms.Select(attrs={"form": "searchForm"}),
|
||||
)
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
document.getElementById('id_per_page').addEventListener('change', event => {
|
||||
event.target.form.requestSubmit();
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load django_bootstrap5 %}
|
||||
{% load i18n %}
|
||||
{% load properties %}
|
||||
{% load static %}
|
||||
|
||||
{% block page_title %}{% translate "Search results for" %} "{{ query }}"{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% translate "Search results for" %} "{{ query }}"</h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{% translate "Part number" %}
|
||||
<div class="d-sm-none fst-italic">{% translate "Total stock" %}</div>
|
||||
</th>
|
||||
<th class="d-none d-sm-table-cell">{% translate "Total stock" %}</th>
|
||||
{% for property in properties %}
|
||||
<th>{{ property.name }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for component, properties in results %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ component.part_number }}
|
||||
<dl class="d-sm-none mb-0">
|
||||
<dt class="visually-hidden">Total stock</dt>
|
||||
<dd class="mb-0 fst-italic">{{ component.total_stock }}</dd>
|
||||
</dl>
|
||||
</td>
|
||||
<td class="d-none d-sm-table-cell">{{ component.total_stock }}</td>
|
||||
{% for _, value in properties %}
|
||||
{% if value %}
|
||||
<td>{{ value|format_value }}</td>
|
||||
{% else %}
|
||||
<td class="fst-italic">{% translate "n/a" %}</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="d-flex flex-wrap column-gap-3 justify-content-between">
|
||||
{% bootstrap_pagination page %}
|
||||
{% bootstrap_field search_form.per_page show_label=False %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'core/search_results.js' %}"></script>
|
||||
{% endblock %}
|
|
@ -7,7 +7,7 @@
|
|||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-collapse collapse show" id="navbarContent">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<ul class="navbar-nav">
|
||||
{% for item in nav_items %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if item.is_active %} active{% endif %}" href="{{ item.link }}"{% if item.is_active %} aria-current="page"{% endif %}>
|
||||
|
@ -16,6 +16,9 @@
|
|||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<form action="{% url 'search' %}" class="d-flex mx-auto" id="searchForm" method="get" role="search">
|
||||
{% bootstrap_field search_form.q show_label="skip" wrapper_class="mb-1 mb-md-0 w-100" %}
|
||||
</form>
|
||||
{% if request.user.is_authenticated %}
|
||||
<form action="{% url 'oidc_logout' %}" class="d-flex justify-content-end" method="post">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
from django import template
|
||||
|
||||
from ..models import Property, Value
|
||||
|
||||
SI_PREFIXES = {
|
||||
-12: "p",
|
||||
-9: "n",
|
||||
-6: "µ",
|
||||
-3: "m",
|
||||
3: "k",
|
||||
6: "M",
|
||||
9: "G",
|
||||
}
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def format_value(value):
|
||||
if not isinstance(value, Value):
|
||||
return value
|
||||
|
||||
property = value.property
|
||||
match property.type:
|
||||
case Property.Type.QUANTITY:
|
||||
s = str(value.integer)
|
||||
|
||||
scale = property.scale
|
||||
if scale > 0:
|
||||
s += "0" * scale
|
||||
elif scale < 0:
|
||||
s = str(value.integer).rjust(abs(scale) + 1, "0")
|
||||
s = s[:scale] + "." + s[scale:]
|
||||
s = s.rstrip("0").removesuffix(".")
|
||||
|
||||
leading_zeroes = len(s.removeprefix("0.")) - len(
|
||||
s.removeprefix("0.").lstrip("0")
|
||||
)
|
||||
if prefix := SI_PREFIXES.get(-leading_zeroes // 3 * 3, ""):
|
||||
s = s.removeprefix("0.").lstrip("0")
|
||||
s = (s[: -leading_zeroes % 3] or "0") + "." + s[-leading_zeroes % 3 :]
|
||||
|
||||
trailing_zeroes = len(s) - len(s.rstrip("0"))
|
||||
if prefix := SI_PREFIXES.get(trailing_zeroes // 3 * 3, ""):
|
||||
s = s.rstrip("0") + "0" * (trailing_zeroes % 3)
|
||||
|
||||
return f"{s} {prefix}{value.property.unit}"
|
||||
case Property.Type.TEXT:
|
||||
return value.text
|
|
@ -6,5 +6,6 @@ urlpatterns = [
|
|||
path("", views.index, name="index"),
|
||||
path("add-stock", views.add_stock, name="add_stock"),
|
||||
path("unauthorized", views.unauthorized, name="unauthorized"),
|
||||
path("search", views.search, name="search"),
|
||||
path("admin/login/", views.admin_login),
|
||||
]
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import transaction
|
||||
from django.db.models import Q, Sum
|
||||
from django.forms.models import model_to_dict
|
||||
from django.http import QueryDict
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from .forms import AddStockFormSet
|
||||
from .forms import AddStockFormSet, SearchForm
|
||||
from .models import Component, Property, Stock, User, Value
|
||||
|
||||
|
||||
|
@ -115,6 +117,61 @@ def add_stock(request):
|
|||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def search(request):
|
||||
data = request.GET.copy()
|
||||
data.setdefault("per_page", 25)
|
||||
|
||||
form = SearchForm(data)
|
||||
if not form.is_valid() or form.cleaned_data["q"] == "":
|
||||
return redirect("index")
|
||||
|
||||
query = form.cleaned_data["q"]
|
||||
results = (
|
||||
Component.objects.prefetch_related("properties")
|
||||
.annotate(total_stock=Sum("stock__quantity", distinct=True))
|
||||
.filter(
|
||||
Q(part_number__icontains=query)
|
||||
| Q(value__text__icontains=query, value__property__searchable=True)
|
||||
)
|
||||
.order_by("pk")
|
||||
)
|
||||
|
||||
paginator = Paginator(results, form.cleaned_data["per_page"])
|
||||
page = paginator.get_page(data.get("page"))
|
||||
|
||||
properties = set()
|
||||
values = {}
|
||||
for component in page:
|
||||
properties |= set(component.properties.all())
|
||||
values[component.part_number] = {
|
||||
value.property.name: value for value in component.value_set.all()
|
||||
}
|
||||
|
||||
properties = list(properties)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"core/search_results.html",
|
||||
{
|
||||
"page": page,
|
||||
"properties": properties,
|
||||
"query": query,
|
||||
"results": [
|
||||
(
|
||||
component,
|
||||
[
|
||||
(property, values[component.part_number].get(property.name))
|
||||
for property in properties
|
||||
],
|
||||
)
|
||||
for component in page
|
||||
],
|
||||
"search_form": SearchForm(initial=data),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def admin_login(request):
|
||||
if request.user.is_authenticated:
|
||||
return redirect("unauthorized")
|
||||
|
|
|
@ -75,6 +75,7 @@ TEMPLATES = [
|
|||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
"lelcsc.core.context_processors.nav",
|
||||
"lelcsc.core.context_processors.search",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue