Compare commits
24 Commits
462ed0c101
...
feature/#3
Author | SHA1 | Date | |
---|---|---|---|
3ef9ea3a3b | |||
8a676c096a | |||
65f25e282e | |||
8b3ef1af25 | |||
bbcbf6ab3d | |||
1b48e8f283 | |||
6dc8f3bfef | |||
871086c7b7 | |||
adf152938d | |||
7e36059605 | |||
ed1508f0ed | |||
2d78b4dcdd | |||
19852dd5ad | |||
0b26a81b94 | |||
befd5e452f | |||
cfb9970c26 | |||
146c2da4f3 | |||
e74a28b0a8 | |||
2fdcfe8baf | |||
c1b9c966dd | |||
08bae61fc0 | |||
4ff71d2b21 | |||
b47c7ad38d | |||
63b8a66ebb |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "sly"]
|
||||
path = sly
|
||||
url = git@git.shimatta.de:sst/sly.git
|
192
README.md
Normal file
192
README.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Shimatta Kenkyusho Parts Database
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
Shimatta Kenkyusho (しまった・研究所) is a Django based web application. It is highly recommended to run it using the supplied docker setup. This removes the need of any special installation on the host system. This guide assumes, that `nginx` is running on the host system and can serve as a reverse proxy and webserver. For easiest download, it is recommended to clone the desired release with `git`.
|
||||
|
||||
Install the requirements:
|
||||
|
||||
**For Debian / Ubuntu:**
|
||||
```
|
||||
# apt-get update
|
||||
# apt-get install docker docker-compose-plugin nginx git
|
||||
```
|
||||
|
||||
**For Arch based Systems:**
|
||||
```
|
||||
# pacman -S nginx docker docker-compose git
|
||||
```
|
||||
|
||||
### Setup Shimatta Kenkyusho
|
||||
|
||||
Clone this repository:
|
||||
```
|
||||
$ git clone https://git.shimatta.de/mhu/shimatta-kenkyusho.git
|
||||
```
|
||||
|
||||
> Note: Shimatta Kenkyusho is currently not stable yet and the newest verison is in the `develop` branch. This will change once actual releases are done and merged to the `master` branch. You will be able to get the latest stable version from the `master` branch or a respective tag. For now, the `develop` is recommended.
|
||||
|
||||
|
||||
Change directory into the `shimatta-kenkyusho` folder cloned by git.
|
||||
|
||||
Copy the `example.env` file to `.env` and edit it according to your needs:
|
||||
|
||||
The following settings are required to be adapted:
|
||||
- `DJANGO_STATIC_VOL`: The directory the application will extract its static data into, which needs to be served by your webserver. See the example reverse proxy setup for more details.
|
||||
- `DJANGO_MEDIA_VOL`: The directory all media files like images uploaded to the application are stored here. This folder must be served by your webserver on the configured media URL.
|
||||
- `PGDATA_VOL`: The directory, the postgres database will store its files.
|
||||
- `PORT`: The TCP/IP port that the whole setup will listen on. Use a reverse proxy to forward to this port. *Do not directly expose it to the internet!*
|
||||
- `DJANGO_SECRET_KEY`: Provide a secret, and randomly generated key. Do not share this with anybody!
|
||||
- `DJANGO_ALLOWED_HOST`: Set this to the domain, the application will be reached at. E.g: `lab.example.com`
|
||||
- `DJANGO_MEDIA_URL`: Set this to the media URL at which your webserver serves the `DJANGO_MEDIA_VOL` diretory. E.g: `media.lab.example.com/` Note the **slash at the end**. It is important.
|
||||
- `DJANGO_USER_ID`: The user ID to run the application inside the docker container. This is the user id, that is used to write the to `DJANGO_STATIC_VOL` and `DJANGO_MEDIA_VOL`. Make sure the user has access.
|
||||
- `DJANGO_USER_GID`: The group ID to run the application inside the docker container. This is the group id, that is used to write the to `DJANGO_STATIC_VOL` and `DJANGO_MEDIA_VOL`.
|
||||
|
||||
> Note: It is not recommended to run the docker container without a set `DJANGO_USER_ID` and `DJANGO_USER_GID`. It will default to `0 (root)`.
|
||||
|
||||
Once the environment is set up, the docker containers can be built and started. Run
|
||||
```
|
||||
$ docker compose build
|
||||
```
|
||||
This will generate two container images:
|
||||
1. `shimatta-kenkyusho-shimatta-kenkyusho-web`: The django application
|
||||
2. `postgres`: A alpine based docker container containing the postgres database.
|
||||
|
||||
Start the application as a service with
|
||||
```
|
||||
$ docker compose up -d
|
||||
```
|
||||
> Note: The initial startup might need a minute because the whole database etc. needs to be initialized first.
|
||||
|
||||
Use
|
||||
```
|
||||
$ docker ps
|
||||
```
|
||||
to check if the `shimatta-kenkyusho-shimatta-kenkyusho-db` and the `shimatta-kenkyusho-shimatta-kenkyusho-web` container are running and report a *healthy status*.
|
||||
|
||||
### Setup Initial Login User
|
||||
When started for the first time with a fresh database without any superuser configured, a superuser `admin` with password `admin` will be automatically generated.
|
||||
Use this user to login for the first time. In the django admin panel you can then either change the password of the `admin` user or create a new superuser with your own username and delete the `admin` user.
|
||||
As long as there is at least one superuser configured, no admin user will be regenerated upon startup.
|
||||
|
||||
|
||||
### Example Reverse Proxy Setup Using nginx
|
||||
Once the setup is configured the reverse proxy setup is needed. This setup serves three purposes:
|
||||
1. Redirect incoming requests to the django application running on the port `PORT` configured in the `.env`
|
||||
2. Serve static files at the URL: (e.g. `lab.example.com/static`). See `ALLOWED_HOST` configuration.
|
||||
3. Serve the media volume at the media URL (e.g. `media.lab.example.com`). See `DJANGO_MEDIA_URL`
|
||||
|
||||
Example nginx configuration for `nginx >v2.25` with SSL and http2 / http3 support:
|
||||
> Note: This is by no means a replacement for the documentation of nginx and only serves as an example. Consult the documentation of your nginx version reagrding security and other issues.
|
||||
```
|
||||
# Force redirection from http to https for application
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name lab.example.com; # This must match your ALLOWED_HOST. Adapt domain.
|
||||
allow all;
|
||||
|
||||
return 301 https://lab.example.com$request_uri; # Adapt domain
|
||||
}
|
||||
|
||||
# Force redirection from http to https for media url
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name media.lab.example.com; # Adapt domain name according to DJANGO_MEDIA_URL
|
||||
allow all;
|
||||
return 301 https://media.lab.example.com$request_uri; # Adapt domain name
|
||||
}
|
||||
|
||||
# Reverse Proxy for application
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
|
||||
|
||||
# Add this for HTTP3. If your nginx is older than 2.25 this might not be available
|
||||
######################################################################################
|
||||
# listen 443 quic reuseport;
|
||||
# listen [::]:443 quic reuseport;
|
||||
# Enable QUIC and HTTP/3
|
||||
# ssl_early_data on;
|
||||
# add_header Alt-Svc 'h3=":443"; ma=86400';
|
||||
#######################################################################################
|
||||
|
||||
server_name lab.example.com; # Adapt domain
|
||||
|
||||
# Use letsencrypt as SSL certificate provider.
|
||||
ssl_certificate /etc/letsencrypt/live/lab.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/lab.example.com/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
ssl_session_cache shared:SSL:1m;
|
||||
ssl_session_timeout 5m;
|
||||
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
allow all;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://127.0.0.1:8000; # Adapt PORT from .env
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
# Adapt path to static volume here. Note the slash at the end
|
||||
alias /path/to/DJANGO_STATIC_VOL/;
|
||||
allow all;
|
||||
}
|
||||
|
||||
client_max_body_size 60m;
|
||||
|
||||
}
|
||||
|
||||
# Serve the media files
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
# Add this for HTTP3. If your nginx is older than 2.25 this might not be available
|
||||
######################################################################################
|
||||
# listen 443 quic reuseport;
|
||||
# listen [::]:443 quic reuseport;
|
||||
# Enable QUIC and HTTP/3
|
||||
# ssl_early_data on;
|
||||
# add_header Alt-Svc 'h3=":443"; ma=86400';
|
||||
#######################################################################################
|
||||
http2 on;
|
||||
|
||||
server_name media.lab.example.com; # Adapt according to DJANGO_MEDIA_URL
|
||||
|
||||
# Use letsencrypt as SSL certificate provider.
|
||||
ssl_certificate /etc/letsencrypt/live/media.lab.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/media.lab.example.com/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
ssl_session_cache shared:SSL:1m;
|
||||
ssl_session_timeout 5m;
|
||||
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
error_page 502 /lab_down.html;
|
||||
|
||||
allow all;
|
||||
root /path/to /DJANGO_MEDIA_VOL/; # Adapt this to the volume provided.
|
||||
}
|
||||
```
|
||||
Congratulations. Your shimatta kenkyusho installation is now fully setup.
|
||||
> Note that, the `compose.yaml` contains a restart-policy. By default the containers will restart automatically, even after a reboot of the host machine, if the docker service is enabled.
|
||||
|
||||
|
||||
## Backup and Restore
|
||||
> TODO
|
||||
|
||||
## Debugging and Development
|
||||
> Todo
|
||||
|
@@ -5,7 +5,7 @@ services:
|
||||
shimatta-kenkyusho-web:
|
||||
<<: *restart_policy
|
||||
build: .
|
||||
user: "${DJANGO_USER_ID}:${DJANGO_USER_GID}"
|
||||
user: "${DJANGO_USER_ID:-0}:${DJANGO_USER_GID:-0}"
|
||||
volumes:
|
||||
- "${DJANGO_STATIC_VOL:-./run/static}:/var/static"
|
||||
- "${DJANGO_MEDIA_VOL:-./run/media}:/var/media"
|
||||
|
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.3 on 2025-02-02 10:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('parts', '0016_componentparametertype_interfix'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='componentparametertype',
|
||||
name='interfix',
|
||||
field=models.CharField(blank=True, help_text='char to be used as decimal point in dynamic description eg. 2R2', max_length=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='componenttype',
|
||||
name='description_template',
|
||||
field=models.TextField(blank=True, help_text="Template to assemble the dynamic description. Use template syntax, access the component with 'object', parameters with 'param_*'."),
|
||||
),
|
||||
]
|
@@ -83,20 +83,24 @@ class Storage(models.Model):
|
||||
# caching variable for subtrees
|
||||
storage_list = None
|
||||
|
||||
def get_path_components(self):
|
||||
def get_path_components(self, top_level=None):
|
||||
chain = []
|
||||
iterator = self
|
||||
chain.append(self)
|
||||
while iterator.parent_storage is not None:
|
||||
|
||||
if top_level and iterator.parent_storage == top_level:
|
||||
break
|
||||
|
||||
chain.append(iterator.parent_storage)
|
||||
iterator = iterator.parent_storage
|
||||
|
||||
return chain
|
||||
|
||||
def get_full_path(self):
|
||||
def get_full_path(self, top_level=None):
|
||||
output = ''
|
||||
|
||||
chain = self.get_path_components()
|
||||
chain = self.get_path_components(top_level)
|
||||
|
||||
for i in range(len(chain) - 1, -1, -1):
|
||||
output = output + '/' + chain[i].name
|
||||
@@ -276,6 +280,10 @@ class Component(models.Model):
|
||||
|
||||
@property
|
||||
def dynamic_description(self):
|
||||
|
||||
if not self.component_type or not self.component_type.description_template:
|
||||
return ''
|
||||
|
||||
django_engine = engines["django"]
|
||||
template = django_engine.from_string(self.component_type.description_template)
|
||||
|
||||
@@ -325,7 +333,7 @@ class AbstractParameter(models.Model):
|
||||
if my_type == 'E' or my_type == 'I':
|
||||
# Engineering float number
|
||||
(num, prefix) = NumConv.number_to_engineering(self.value, it_unit=(True if my_type=='I' else False))
|
||||
result = f'{num:g}'
|
||||
result = f'{round(num, 3):g}'
|
||||
interpostfix = (prefix if prefix else self.parameter_type.interfix or '.')
|
||||
if '.' in result:
|
||||
result = result.replace('.', interpostfix)
|
||||
@@ -334,7 +342,7 @@ class AbstractParameter(models.Model):
|
||||
return result
|
||||
elif my_type == 'N':
|
||||
# Standard float number
|
||||
return f'{self.value:g}{self.parameter_type.unit}'
|
||||
return f'{round(self.value, 3):g}{self.parameter_type.unit}'
|
||||
else:
|
||||
return self.resolved_value_as_string()
|
||||
|
||||
|
0
shimatta_kenkyusho/parts/templatetags/__init__.py
Normal file
0
shimatta_kenkyusho/parts/templatetags/__init__.py
Normal file
9
shimatta_kenkyusho/parts/templatetags/storage_tags.py
Normal file
9
shimatta_kenkyusho/parts/templatetags/storage_tags.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import datetime
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name="get_relative_storage_path")
|
||||
def get_relative_storage_path(storage, top_level):
|
||||
return f'.{storage.get_full_path(top_level)}'
|
@@ -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
|
||||
return queryset, error_string
|
||||
|
||||
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 and not errors:
|
||||
try:
|
||||
queryset = queryset.filter(query)
|
||||
except Exception as ex:
|
||||
error_string = str(ex)
|
||||
else:
|
||||
error_string = '<br><br>'.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')
|
||||
|
||||
|
184
shimatta_kenkyusho/shimatta_modules/ShimattaSearchLanguage.py
Normal file
184
shimatta_kenkyusho/shimatta_modules/ShimattaSearchLanguage.py
Normal file
@@ -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
|
||||
GTE = r'>='
|
||||
LTE = r'<='
|
||||
GT = r'>'
|
||||
LT = r'<'
|
||||
EQ = r'=='
|
||||
NEQ = r'!='
|
||||
AND = r'(?:&{1,2})|(?:and)|(?:AND)'
|
||||
OR = r'(?:\|{1,2})|(?:or)|(?:OR)'
|
||||
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
|
@@ -76,6 +76,11 @@
|
||||
No description available
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if component.dynamic_description %}
|
||||
<h2>Dynamic Description</h2>
|
||||
<pre>{{ component.dynamic_description }}</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{% if component.pref_distri %}
|
||||
|
@@ -6,16 +6,26 @@
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<h2>Components</h2>
|
||||
<form action="" method="get">
|
||||
<div class="input-group mb-3">
|
||||
<input class="form-control" name="search" type="search" placeholder="Search Component..." {% if search_string %}value="{{search_string}}"{% endif %}>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-search"></i>
|
||||
</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>
|
||||
<form class="needs-validation" action="" method="get">
|
||||
{% if errors %}
|
||||
<div class="card mb-3">
|
||||
{% endif %}
|
||||
<div class="input-group mb-3">
|
||||
<input class="form-control" name="search" type="search" placeholder="Search Component..." {% if search_string %}value="{{search_string}}"{% endif %}>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-search"></i>
|
||||
</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>
|
||||
{% endif %}
|
||||
</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 %}">
|
||||
<form method="POST">
|
||||
@@ -54,7 +64,12 @@
|
||||
Manufacturer: {{comp.manufacturer}}
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="badge bg-primary rounded-pill">{{comp.get_total_amount}}</span>
|
||||
<div class="flex-grow-1 d-block ms-3" style="text-align: right;">
|
||||
<pre>{{ comp.dynamic_description }}</pre>
|
||||
</div>
|
||||
<div style="width: 10%; text-align: right;">
|
||||
<span class="badge bg-primary rounded-pill me-2">{{comp.get_total_amount}}</span>
|
||||
</div>
|
||||
</li>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
{% load qr_code %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load storage_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
@@ -56,8 +57,8 @@
|
||||
Responsible: {{ storage.responsible }}
|
||||
</div>
|
||||
<span class="badge ms-1 bg-primary rounded-pill" data-bs-toggle="tooltip" data-bs-placement="top" title="Total number of stored parts">{{storage.get_total_stock_amount}}</span>
|
||||
<span class="badge ms-1 bg-secondary rounded-pill" data-bs-toggle="tooltip" data-bs-placement="top" title="Number of stored lots">{{storage.get_total_stock_count}}</span>
|
||||
<span class="badge ms-1 bg-info rounded-pill" data-bs-toggle="tooltip" data-bs-placement="top" title="Number of substorages">{{storage.get_total_substorage_amount}}</span>
|
||||
<span class="badge ms-1 bg-secondary rounded-pill d-none d-lg-block" data-bs-toggle="tooltip" data-bs-placement="top" title="Number of stored lots">{{storage.get_total_stock_count}}</span>
|
||||
<span class="badge ms-1 bg-info rounded-pill d-none d-lg-block" data-bs-toggle="tooltip" data-bs-placement="top" title="Number of substorages">{{storage.get_total_substorage_amount}}</span>
|
||||
</li>
|
||||
</a>
|
||||
{% endfor %}
|
||||
@@ -97,17 +98,20 @@
|
||||
{% if stock.component.manufacturer %}
|
||||
Manufacturer: {{stock.component.manufacturer}}
|
||||
{% endif %}
|
||||
{% if stock.storage != storage %}
|
||||
<span class="text-secondary"><br>{{ stock.storage|get_relative_storage_path:storage }}</span>
|
||||
{% endif %}
|
||||
{% if stock.lot %}
|
||||
<span class="text-secondary"><br>Lot: {{stock.lot}}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex-grow-2 ms-3 d-none d-lg-block" style="text-align: center;">
|
||||
<div class="flex-grow-2 ms-3 d-none d-lg-block" style="text-align: right;">
|
||||
<pre>{{ stock.component.dynamic_description }}</pre>
|
||||
</div>
|
||||
<div class="flex-grow-2 ms-5 d-none d-lg-block">
|
||||
{% qr_from_text stock.get_qr_code size="6" image_format="svg" %}
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<div class="ms-3" style="width: 20%;">
|
||||
Amount: {{stock.amount}}
|
||||
{% if stock.watermark >= 0 %}
|
||||
<br>Watermark: {{stock.watermark}}
|
||||
|
1
sly
Submodule
1
sly
Submodule
Submodule sly added at 539a85a5d5
Reference in New Issue
Block a user