Compare commits
No commits in common. "feature/#35-shimatta-search-language" and "develop" have entirely different histories.
feature/#3
...
develop
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
|||||||
[submodule "sly"]
|
|
||||||
path = sly
|
|
||||||
url = git@git.shimatta.de:sst/sly.git
|
|
@ -1,3 +1,4 @@
|
|||||||
|
import uuid
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.views.generic import TemplateView, DetailView
|
from django.views.generic import TemplateView, DetailView
|
||||||
@ -7,7 +8,6 @@ from django.db.models import Q
|
|||||||
from django.forms import formset_factory
|
from django.forms import formset_factory
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import ProtectedError
|
from django.db.models import ProtectedError
|
||||||
from shimatta_modules.ShimattaSearchLanguage import ShimattaSearchLanguage
|
|
||||||
from ..models import Stock, Component, ComponentParameter, DistributorNum, PackageParameter
|
from ..models import Stock, Component, ComponentParameter, DistributorNum, PackageParameter
|
||||||
from ..forms import *
|
from ..forms import *
|
||||||
from .component_import import import_components_from_csv
|
from .component_import import import_components_from_csv
|
||||||
@ -26,23 +26,15 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
|
|||||||
|
|
||||||
def get_component_query_set(self, search_string):
|
def get_component_query_set(self, search_string):
|
||||||
queryset = Component.objects.all()
|
queryset = Component.objects.all()
|
||||||
error_string = ''
|
|
||||||
|
|
||||||
if not search_string:
|
if not search_string:
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
search_parser = ShimattaSearchLanguage()
|
|
||||||
query, errors = search_parser.search_for_components(search_string)
|
|
||||||
|
|
||||||
if query and not errors:
|
search_fragments = search_string.strip().split()
|
||||||
try:
|
for search in search_fragments:
|
||||||
queryset = queryset.filter(query)
|
queryset = queryset.filter(Q(name__icontains = search) | Q(manufacturer__name__icontains = search) | Q(package__name__icontains = search))
|
||||||
except Exception as ex:
|
|
||||||
error_string = str(ex)
|
|
||||||
else:
|
|
||||||
error_string = '<br><br>'.join(errors)
|
|
||||||
|
|
||||||
return queryset, error_string
|
return queryset
|
||||||
|
|
||||||
def get_component_queryset_from_advanced_search(self, cleaned_data):
|
def get_component_queryset_from_advanced_search(self, cleaned_data):
|
||||||
queryset = Component.objects.all()
|
queryset = Component.objects.all()
|
||||||
@ -68,8 +60,6 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
|
|||||||
def get_context_data_int(self, advanced_search, parameter_formset : ParameterSearchFormSet, **kwargs):
|
def get_context_data_int(self, advanced_search, parameter_formset : ParameterSearchFormSet, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
errors = ''
|
|
||||||
|
|
||||||
comp_page_num = self.request.GET.get('comp_page', default=1)
|
comp_page_num = self.request.GET.get('comp_page', default=1)
|
||||||
|
|
||||||
if advanced_search and parameter_formset:
|
if advanced_search and parameter_formset:
|
||||||
@ -89,7 +79,7 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
search = self.request.GET.get('search', default=None)
|
search = self.request.GET.get('search', default=None)
|
||||||
paginator_queryset, errors = self.get_component_query_set(search)
|
paginator_queryset = self.get_component_query_set(search)
|
||||||
|
|
||||||
comp_paginator = Paginator(paginator_queryset, self.default_page_size)
|
comp_paginator = Paginator(paginator_queryset, self.default_page_size)
|
||||||
|
|
||||||
@ -97,11 +87,11 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
|
|||||||
context['comp_form'] = ComponentForm()
|
context['comp_form'] = ComponentForm()
|
||||||
context['import_comp_form'] = ImportComponentForm()
|
context['import_comp_form'] = ImportComponentForm()
|
||||||
context['search_string'] = search
|
context['search_string'] = search
|
||||||
context['errors'] = errors
|
|
||||||
|
|
||||||
if not parameter_formset:
|
if not parameter_formset:
|
||||||
context['advanced_search_param_formset'] = ParameterSearchFormSet()
|
context['advanced_search_param_formset'] = ParameterSearchFormSet()
|
||||||
|
|
||||||
|
|
||||||
if not advanced_search:
|
if not advanced_search:
|
||||||
context['advanced_search_form'] = AdvancedComponentSearchForm(auto_id='adv_search_%s')
|
context['advanced_search_form'] = AdvancedComponentSearchForm(auto_id='adv_search_%s')
|
||||||
|
|
||||||
|
@ -1,184 +0,0 @@
|
|||||||
import re
|
|
||||||
from django.db.models import Q
|
|
||||||
from sly import Lexer, Parser
|
|
||||||
from shimatta_modules.EngineeringNumberConverter import EngineeringNumberConverter
|
|
||||||
|
|
||||||
class ShimattaSearchConstants():
|
|
||||||
'''
|
|
||||||
Just a bunch of conversions and regular expression stored here.
|
|
||||||
'''
|
|
||||||
|
|
||||||
# convert the prefixes from the engineering number converter be used in the lexer
|
|
||||||
PREFIX_DICT = {k: v for k, v in EngineeringNumberConverter.prefixes if k}
|
|
||||||
PREFIX_DICT.update({k: v for k, v in EngineeringNumberConverter.it_prefixes if k})
|
|
||||||
PREFIX_RULE = r'(' + r'|'.join([rf'(?:{p})' for p in PREFIX_DICT.keys()]) + r')?'
|
|
||||||
|
|
||||||
TEXTUAL_REGEX = r'(?:([^"\s]+))|(?:"([^"]*)")'
|
|
||||||
VALUE_REGEX = rf'(\-?\d+(?:\.\d+)?){PREFIX_RULE}'
|
|
||||||
|
|
||||||
TEXTUAL_PATTERN = re.compile(TEXTUAL_REGEX)
|
|
||||||
VALUE_PATTERN = re.compile(VALUE_REGEX)
|
|
||||||
|
|
||||||
class ShimattaSearchLexer(Lexer):
|
|
||||||
'''
|
|
||||||
Stupid lexer to tokenize a search string.
|
|
||||||
'''
|
|
||||||
|
|
||||||
tokens = {GT, LT, GTE, LTE, EQ, NEQ, AND, OR, LPAREN, RPAREN, NUMBER, TEXTUAL}
|
|
||||||
|
|
||||||
# ignore whitespace only characters as well as newlines
|
|
||||||
ignore = ' \t\n'
|
|
||||||
|
|
||||||
# Regular expression rules for simple tokens
|
|
||||||
GT = r'>'
|
|
||||||
LT = r'<'
|
|
||||||
GTE = r'>='
|
|
||||||
LTE = r'<='
|
|
||||||
EQ = r'=='
|
|
||||||
NEQ = r'!='
|
|
||||||
AND = r'&'
|
|
||||||
OR = r'\|'
|
|
||||||
LPAREN = r'\('
|
|
||||||
RPAREN = r'\)'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.errors = []
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
@_(ShimattaSearchConstants.VALUE_REGEX)
|
|
||||||
def NUMBER(self, t):
|
|
||||||
'''
|
|
||||||
Parse numbers with engineering unit prefixes
|
|
||||||
'''
|
|
||||||
match = ShimattaSearchConstants.VALUE_PATTERN.match(t.value)
|
|
||||||
t.value = float(match.group(1))
|
|
||||||
prefix = match.group(2)
|
|
||||||
if prefix:
|
|
||||||
t.value *= ShimattaSearchConstants.PREFIX_DICT[prefix]
|
|
||||||
return t
|
|
||||||
|
|
||||||
@_(ShimattaSearchConstants.TEXTUAL_REGEX)
|
|
||||||
def TEXTUAL(self, t):
|
|
||||||
'''
|
|
||||||
Find texts with or without param_ prefix (used to filter for parameters)
|
|
||||||
'''
|
|
||||||
match = ShimattaSearchConstants.TEXTUAL_PATTERN.match(t.value)
|
|
||||||
|
|
||||||
# strip the quotation marks
|
|
||||||
value = match.group(1)
|
|
||||||
if match.group(2):
|
|
||||||
value = match.group(2)
|
|
||||||
|
|
||||||
t.value = value
|
|
||||||
return t
|
|
||||||
|
|
||||||
def error(self, t):
|
|
||||||
self.errors.append(f'Line {self.lineno}: Bad character {t.value}')
|
|
||||||
self.index += 1
|
|
||||||
|
|
||||||
class ShimattaSearchParser(Parser):
|
|
||||||
# Get the token list from the lexer (required)
|
|
||||||
tokens = ShimattaSearchLexer.tokens
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.errors = []
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_filter(key, value, compare_suffix='', invert=False):
|
|
||||||
'''
|
|
||||||
Assemble a filter to grep data from the relational database structure
|
|
||||||
'''
|
|
||||||
|
|
||||||
# filter for params - stored in two separate tables
|
|
||||||
if key.startswith('param_'):
|
|
||||||
key = key[len('param_'):]
|
|
||||||
|
|
||||||
key_query = Q(Q(**{f'componentparameter__parameter_type__parameter_name': key})| \
|
|
||||||
Q(**{f'package__packageparameter__parameter_type__parameter_name': key}))
|
|
||||||
|
|
||||||
if isinstance(value, str):
|
|
||||||
query = Q(Q(**{f'componentparameter__text_value{compare_suffix}': value})| \
|
|
||||||
Q(**{f'package__packageparameter__text_value{compare_suffix}': value}))&key_query
|
|
||||||
else:
|
|
||||||
query = Q(Q(**{f'componentparameter__value{compare_suffix}': value})| \
|
|
||||||
Q(**{f'package__packageparameter__value{compare_suffix}': value}))&key_query
|
|
||||||
# filter for direct attributes - or whatever the user throws into the search input
|
|
||||||
else:
|
|
||||||
query = Q(**{f'{key}{compare_suffix}': value})
|
|
||||||
|
|
||||||
if invert:
|
|
||||||
query = ~query
|
|
||||||
return Q(query)
|
|
||||||
|
|
||||||
# ruleset
|
|
||||||
|
|
||||||
@_('expression : textual GT number')
|
|
||||||
def expression(self, p):
|
|
||||||
return self._get_filter(p.textual.strip(), p.number, '__gt', False)
|
|
||||||
|
|
||||||
@_('expression : textual LT number')
|
|
||||||
def expression(self, p):
|
|
||||||
return self._get_filter(p.textual.strip(), p.number, '__lt', False)
|
|
||||||
|
|
||||||
@_('expression : textual GTE number')
|
|
||||||
def expression(self, p):
|
|
||||||
return self._get_filter(p.textual.strip(), p.number, '__gte', False)
|
|
||||||
|
|
||||||
@_('expression : textual LTE number')
|
|
||||||
def expression(self, p):
|
|
||||||
return self._get_filter(p.textual.strip(), p.number, '__lte', False)
|
|
||||||
|
|
||||||
@_('expression : textual EQ number')
|
|
||||||
def expression(self, p):
|
|
||||||
return self._get_filter(p.textual.strip(), p.number, '', False)
|
|
||||||
|
|
||||||
@_('expression : textual NEQ number')
|
|
||||||
def expression(self, p):
|
|
||||||
return self._get_filter(p.textual.strip(), p.number, '', True)
|
|
||||||
|
|
||||||
@_('expression : textual EQ textual')
|
|
||||||
def expression(self, p):
|
|
||||||
return self._get_filter(p.textual0.strip(), p.textual1, '', False)
|
|
||||||
|
|
||||||
@_('expression : textual NEQ textual')
|
|
||||||
def expression(self, p):
|
|
||||||
return self._get_filter(p.textual0.strip(), p.textual1, '', True)
|
|
||||||
|
|
||||||
@_('TEXTUAL')
|
|
||||||
def textual(self, p):
|
|
||||||
return p.TEXTUAL
|
|
||||||
|
|
||||||
@_('NUMBER')
|
|
||||||
def number(self, p):
|
|
||||||
return p.NUMBER
|
|
||||||
|
|
||||||
@_('expression : LPAREN expression RPAREN')
|
|
||||||
def expression(self, p):
|
|
||||||
return Q(p.expression)
|
|
||||||
|
|
||||||
@_('expression : expression AND expression')
|
|
||||||
def expression(self, p):
|
|
||||||
return p.expression0&p.expression1
|
|
||||||
|
|
||||||
@_('expression : expression OR expression')
|
|
||||||
def expression(self, p):
|
|
||||||
return p.expression0|p.expression1
|
|
||||||
|
|
||||||
@_('expression')
|
|
||||||
def expression(self, p):
|
|
||||||
return p.expression
|
|
||||||
|
|
||||||
# Error rule for syntax errors
|
|
||||||
def error(self, p):
|
|
||||||
self.errors.append(f'Syntax error in input {p}!')
|
|
||||||
|
|
||||||
class ShimattaSearchLanguage():
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.lexer = ShimattaSearchLexer()
|
|
||||||
self.parser = ShimattaSearchParser()
|
|
||||||
|
|
||||||
def search_for_components(self, search_string):
|
|
||||||
query = self.parser.parse(self.lexer.tokenize(search_string))
|
|
||||||
return query, self.lexer.errors + self.parser.errors
|
|
@ -6,26 +6,16 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<h2>Components</h2>
|
<h2>Components</h2>
|
||||||
<form class="needs-validation" action="" method="get">
|
<form action="" method="get">
|
||||||
{% if errors %}
|
<div class="input-group mb-3">
|
||||||
<div class="card mb-3">
|
<input class="form-control" name="search" type="search" placeholder="Search Component..." {% if search_string %}value="{{search_string}}"{% endif %}>
|
||||||
{% endif %}
|
<button type="submit" class="btn btn-primary">
|
||||||
<div class="input-group mb-3">
|
<i class="bi bi-search"></i>
|
||||||
<input class="form-control" name="search" type="search" placeholder="Search Component..." {% if search_string %}value="{{search_string}}"{% endif %}>
|
</button>
|
||||||
<button type="submit" class="btn btn-primary">
|
<button class="btn btn-secondary" type="button" data-bs-toggle="collapse" href="#advanced-search-collapse">Advanced <i class="bi bi-search"></i></button>
|
||||||
<i class="bi bi-search"></i>
|
<button class="btn btn-success" type="button" data-bs-toggle="modal" data-bs-target="#comp-edit-modal"><i class="bi bi-plus-circle"></i> Add Component</button>
|
||||||
</button>
|
<button class="btn btn-success" type="button" data-bs-toggle="modal" data-bs-target="#comp-import-modal"><i class="bi bi-plus-circle"></i> Import CSV</button>
|
||||||
<button class="btn btn-secondary" type="button" data-bs-toggle="collapse" href="#advanced-search-collapse">Advanced <i class="bi bi-search"></i></button>
|
|
||||||
<button class="btn btn-success" type="button" data-bs-toggle="modal" data-bs-target="#comp-edit-modal"><i class="bi bi-plus-circle"></i> Add Component</button>
|
|
||||||
<button class="btn btn-success" type="button" data-bs-toggle="modal" data-bs-target="#comp-import-modal"><i class="bi bi-plus-circle"></i> Import CSV</button>
|
|
||||||
</div>
|
|
||||||
{% if errors %}
|
|
||||||
<div class="card-body text-danger">
|
|
||||||
<h5>Error in search query:</h5>
|
|
||||||
{{ errors }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</form>
|
</form>
|
||||||
<div class="collapse mb-3{% if advanced_search_shown %} show{% endif %}" id="advanced-search-collapse" aria-expanded="{% if advanced_search_shown %}true{% else %}false{% endif %}">
|
<div class="collapse mb-3{% if advanced_search_shown %} show{% endif %}" id="advanced-search-collapse" aria-expanded="{% if advanced_search_shown %}true{% else %}false{% endif %}">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
|
1
sly
1
sly
@ -1 +0,0 @@
|
|||||||
Subproject commit 539a85a5d5818bf4e1cb5a9e749d6e2fab70a351
|
|
Loading…
Reference in New Issue
Block a user