diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..73052c8
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "sly"]
+ path = sly
+ url = git@git.shimatta.de:sst/sly.git
diff --git a/shimatta_kenkyusho/parts/views/component_views.py b/shimatta_kenkyusho/parts/views/component_views.py
index d092c84..c694d84 100644
--- a/shimatta_kenkyusho/parts/views/component_views.py
+++ b/shimatta_kenkyusho/parts/views/component_views.py
@@ -1,4 +1,3 @@
-import uuid
from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic import TemplateView, DetailView
@@ -8,6 +7,7 @@ from django.db.models import Q
from django.forms import formset_factory
from django.db import IntegrityError
from django.db.models import ProtectedError
+from shimatta_modules.ShimattaSearchLanguage import ShimattaSearchLanguage
from ..models import Stock, Component, ComponentParameter, DistributorNum, PackageParameter
from ..forms import *
from .component_import import import_components_from_csv
@@ -26,15 +26,23 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
def get_component_query_set(self, search_string):
queryset = Component.objects.all()
+ error_string = ''
if not search_string:
return queryset
+
+ search_parser = ShimattaSearchLanguage()
+ query, errors = search_parser.search_for_components(search_string)
- search_fragments = search_string.strip().split()
- for search in search_fragments:
- queryset = queryset.filter(Q(name__icontains = search) | Q(manufacturer__name__icontains = search) | Q(package__name__icontains = search))
+ if query:
+ try:
+ queryset = queryset.filter(query)
+ except Exception as ex:
+ error_string = str(ex)
+ else:
+ error_string = '
'.join(errors)
- return queryset
+ return queryset, error_string
def get_component_queryset_from_advanced_search(self, cleaned_data):
queryset = Component.objects.all()
@@ -60,6 +68,8 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
def get_context_data_int(self, advanced_search, parameter_formset : ParameterSearchFormSet, **kwargs):
context = super().get_context_data(**kwargs)
+ errors = ''
+
comp_page_num = self.request.GET.get('comp_page', default=1)
if advanced_search and parameter_formset:
@@ -79,7 +89,7 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
else:
search = self.request.GET.get('search', default=None)
- paginator_queryset = self.get_component_query_set(search)
+ paginator_queryset, errors = self.get_component_query_set(search)
comp_paginator = Paginator(paginator_queryset, self.default_page_size)
@@ -87,11 +97,11 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
context['comp_form'] = ComponentForm()
context['import_comp_form'] = ImportComponentForm()
context['search_string'] = search
+ context['errors'] = errors
if not parameter_formset:
context['advanced_search_param_formset'] = ParameterSearchFormSet()
-
if not advanced_search:
context['advanced_search_form'] = AdvancedComponentSearchForm(auto_id='adv_search_%s')
diff --git a/shimatta_kenkyusho/shimatta_modules/ShimattaSearchLanguage.py b/shimatta_kenkyusho/shimatta_modules/ShimattaSearchLanguage.py
new file mode 100644
index 0000000..d21c8b1
--- /dev/null
+++ b/shimatta_kenkyusho/shimatta_modules/ShimattaSearchLanguage.py
@@ -0,0 +1,184 @@
+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
diff --git a/shimatta_kenkyusho/templates/parts/components.html b/shimatta_kenkyusho/templates/parts/components.html
index f4011ac..e4dc496 100644
--- a/shimatta_kenkyusho/templates/parts/components.html
+++ b/shimatta_kenkyusho/templates/parts/components.html
@@ -6,16 +6,26 @@