Start content

This commit is contained in:
2021-08-07 17:37:36 +02:00
parent a3f31608a8
commit ec0a8c98e7
1391 changed files with 13744 additions and 14 deletions

View File

@@ -1,3 +1,18 @@
from django.contrib import admin
from . import models as parts_models
# Register your models here.
admin.site.register(parts_models.Component)
admin.site.register(parts_models.Package)
admin.site.register(parts_models.Manufacturer)
admin.site.register(parts_models.Storage)
admin.site.register(parts_models.Stock)
admin.site.register(parts_models.ComponentParameter)
admin.site.register(parts_models.ComponentParameterType)
admin.site.register(parts_models.ComponentType)
admin.site.register(parts_models.Distributor)
admin.site.register(parts_models.DistributorNum)

View File

@@ -0,0 +1,157 @@
# Generated by Django 3.2 on 2021-07-31 20:44
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import shimatta_modules.RandomFileName
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ComponentParameterType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('parameter_name', models.CharField(max_length=50, unique=True)),
('parameter_description', models.TextField(blank=True, null=True)),
('unit', models.CharField(max_length=10)),
('freetext_parameter', models.BooleanField()),
('engineering_unit', models.BooleanField()),
('it_unit', models.BooleanField()),
],
options={
'ordering': ['parameter_name'],
},
),
migrations.CreateModel(
name='Distributor',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=100, unique=True)),
('website', models.CharField(blank=True, max_length=200, null=True)),
('image', models.ImageField(blank=True, null=True, upload_to=shimatta_modules.RandomFileName.RandomFileName('distributor-logos'))),
('component_link_pattern', models.CharField(blank=True, max_length=255, null=True)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Manufacturer',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=100, unique=True)),
('website', models.CharField(blank=True, max_length=200, null=True)),
('image', models.ImageField(blank=True, null=True, upload_to=shimatta_modules.RandomFileName.RandomFileName('manufacturer-images'))),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Package',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=50, unique=True)),
('pin_count', models.PositiveIntegerField()),
('smd', models.BooleanField()),
('image', models.ImageField(blank=True, null=True, upload_to=shimatta_modules.RandomFileName.RandomFileName('package-images'))),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Storage',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=100, validators=[django.core.validators.RegexValidator('^[^/]*$', 'Slashes are not allowed in storage names')])),
('parent_storage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='parts.storage')),
('responsible', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['name'],
'unique_together': {('name', 'parent_storage')},
},
),
migrations.CreateModel(
name='ComponentType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('class_name', models.CharField(max_length=50, unique=True)),
('passive', models.BooleanField()),
('possible_parameter', models.ManyToManyField(to='parts.ComponentParameterType')),
],
options={
'ordering': ['class_name'],
},
),
migrations.CreateModel(
name='Component',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=100)),
('description', models.TextField(blank=True, null=True)),
('datasheet_link', models.CharField(blank=True, max_length=300, null=True)),
('image', models.ImageField(blank=True, null=True, upload_to=shimatta_modules.RandomFileName.RandomFileName('component-images'))),
('component_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='parts.componenttype')),
('manufacturer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='parts.manufacturer')),
('package', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='parts.package')),
('pref_distri', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='parts.distributor')),
],
options={
'ordering': ['name', 'manufacturer'],
'unique_together': {('name', 'manufacturer', 'package')},
},
),
migrations.CreateModel(
name='Stock',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('amount', models.PositiveIntegerField()),
('watermark', models.IntegerField()),
('component', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='parts.component')),
('storage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='parts.storage')),
],
options={
'ordering': ['id'],
'unique_together': {('component', 'storage')},
},
),
migrations.CreateModel(
name='DistributorNum',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('distributor_part_number', models.CharField(max_length=100)),
('component', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='parts.component')),
('distributor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='parts.distributor')),
],
options={
'ordering': ['distributor_part_number'],
'unique_together': {('component', 'distributor')},
},
),
migrations.CreateModel(
name='ComponentParameter',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('value', models.FloatField()),
('text_value', models.TextField(blank=True, null=True)),
('component', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='parts.component')),
('parameter_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='parts.componentparametertype')),
],
options={
'ordering': ['id'],
'unique_together': {('component', 'parameter_type')},
},
),
]

View File

@@ -1,3 +1,272 @@
from django.db import models
from shimatta_modules import RandomFileName
from django.db.models import F, Sum
from django.contrib.auth.models import User as AuthUser
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.dispatch import receiver
import os
import uuid
storage_name_validator = RegexValidator(r'^[^/]*$', 'Slashes are not allowed in storage names')
# Create your models here.
class ComponentParameterType(models.Model):
class Meta:
ordering = ['parameter_name']
parameter_name = models.CharField(max_length=50, unique=True)
parameter_description = models.TextField(null=True, blank=True)
unit = models.CharField(max_length=10)
freetext_parameter = models.BooleanField()
engineering_unit = models.BooleanField()
it_unit = models.BooleanField()
def __str__(self):
return self.parameter_name + ' in ' + self.unit
class ComponentType(models.Model):
class Meta:
ordering = ['class_name']
class_name = models.CharField(max_length=50, unique=True)
passive = models.BooleanField()
possible_parameter = models.ManyToManyField(ComponentParameterType)
def __str__(self):
return '[' + self.class_name + ']'
class Storage(models.Model):
class Meta:
# permissions = ()
# Allow only one child with the same name
unique_together = ('name', 'parent_storage')
ordering = ['name']
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
name = models.CharField(max_length=100, validators=[storage_name_validator])
parent_storage = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, null=True)
responsible = models.ForeignKey(AuthUser, on_delete=models.SET_NULL, blank=True, null=True)
def get_path_components(self):
chain = []
iterator = self
chain.append(self)
while iterator.parent_storage is not None:
chain.append(iterator.parent_storage)
iterator = iterator.parent_storage
return chain
def get_full_path(self):
output = ''
chain = self.get_path_components()
for i in range(len(chain) - 1, -1, -1):
output = output + '/' + chain[i].name
return output
def get_qr_code(self):
qrdata = '[stor_uuid]' + str(self.id)
return qrdata
def __str__(self):
return self.get_full_path()
def get_children(self):
if self is None: # Root node
return Storage.objects.filter(parent_storage=None)
else:
return self.storage_set.all()
def validate_unique(self, exclude=None):
if Storage.objects.exclude(id=self.id).filter(name=self.name, parent_storage__isnull=True).exists():
if self.parent_storage is None:
raise ValidationError('The fields name, parent_storage must make a unique set')
super(Storage, self).validate_unique(exclude)
def get_total_stock_amount(self):
stocks = Stock.objects.filter(storage=self)
return stocks.aggregate(Sum('amount'))['amount__sum']
def save(self, *args, **kwargs):
self.validate_unique()
super(Storage, self).save(*args, **kwargs)
class Distributor(models.Model):
class Meta:
ordering = ['name']
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
name = models.CharField(max_length=100, unique=True)
website = models.CharField(max_length=200, null=True, blank=True)
image = models.ImageField(upload_to=RandomFileName.RandomFileName('distributor-logos'), null=True, blank=True)
component_link_pattern = models.CharField(max_length=255, blank=True, null=True)
def __str__(self):
return self.name
class Package(models.Model):
class Meta:
ordering = ['name']
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
name = models.CharField(max_length=50, unique=True)
pin_count = models.PositiveIntegerField()
smd = models.BooleanField()
image = models.ImageField(upload_to=RandomFileName.RandomFileName('package-images'), blank=True, null=True)
def __str__(self):
return self.name
class Manufacturer(models.Model):
class Meta:
ordering = ['name']
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
name = models.CharField(max_length=100, unique=True)
website = models.CharField(max_length=200, null=True, blank=True)
image = models.ImageField(upload_to=RandomFileName.RandomFileName('manufacturer-images'), blank=True, null=True)
def __str__(self):
return str(self.name)
class Component(models.Model):
class Meta:
unique_together = ('name', 'manufacturer', 'package')
ordering = ['name', 'manufacturer']
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
name = models.CharField(max_length=100)
manufacturer = models.ForeignKey(Manufacturer, on_delete=models.SET_NULL, blank=True, null=True)
component_type = models.ForeignKey(ComponentType, on_delete=models.SET_NULL, blank=True, null=True)
pref_distri = models.ForeignKey(Distributor, on_delete=models.SET_NULL, blank=True, null=True)
description = models.TextField(null=True, blank=True)
datasheet_link = models.CharField(max_length=300, null=True, blank=True)
package = models.ForeignKey(Package, on_delete=models.SET_NULL, blank=True, null=True)
image = models.ImageField(upload_to=RandomFileName.RandomFileName('component-images'), blank=True, null=True)
def __str__(self):
pack_name = ''
man_name = ''
if self.package:
pack_name = ' in ' + self.package.name
if self.manufacturer:
man_name = ' by ' + self.manufacturer.name
return self.name + pack_name + man_name
def get_resolved_image(self):
media_url = ''
url = None
if self.image:
url = '%s%s' % (media_url, self.image.url)
else:
if self.package:
if self.package.image:
url = '%s%s' % (media_url, self.package.image.url)
return url
def get_qr_code(self):
qrdata = '[comp_uuid]' + str(self.id)
return qrdata
class ComponentParameter(models.Model):
class Meta:
unique_together = ('component', 'parameter_type')
ordering = ['id']
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
component = models.ForeignKey(Component, on_delete=models.CASCADE) # A target component is required!
parameter_type = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE)
value = models.FloatField()
text_value = models.TextField(null=True, blank=True)
def __str__(self):
return str(self.parameter_type) + ': ' + str(self.value)
class Stock(models.Model):
class Meta:
unique_together = ('component', 'storage')
ordering = ['id']
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
component = models.ForeignKey(Component, on_delete=models.PROTECT, blank=True, null=True)
storage = models.ForeignKey(Storage, on_delete=models.PROTECT, blank=True, null=True)
amount = models.PositiveIntegerField()
watermark = models.IntegerField() # negative is no watermark
def atomic_increment(self, increment):
if self.amount + increment < 0:
return False
self.amount = F('amount') + increment
self.save(update_fields=['amount'])
return True
def get_qr_code(self):
qr_data = '[stock]'+str(self.id)
return qr_data
def __str__(self):
return str(self.component) + ' @ ' + str(self.amount) + ' in ' + str(
self.storage)
def get_under_watermark():
return Stock.objects.filter(amount__lt = F('watermark'))
class DistributorNum(models.Model):
class Meta:
unique_together = ('component', 'distributor')
ordering = ['distributor_part_number']
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
distributor_part_number = models.CharField(max_length=100)
distributor = models.ForeignKey(Distributor, on_delete=models.CASCADE)
component = models.ForeignKey(Component, on_delete=models.CASCADE)
def __str__(self):
return self.component.name + '@' + self.distributor.name + ': ' + self.distributor_part_number
# These functions ensure that the uploaded images are deleted if the model is deleted or the image file is changed.
@receiver(models.signals.post_delete, sender=Component)
@receiver(models.signals.post_delete, sender=Distributor)
@receiver(models.signals.post_delete, sender=Package)
@receiver(models.signals.post_delete, sender=Manufacturer)
def auto_delete_file_on_delete(sender, instance, **kwargs):
"""
Deletes file from filesystem
when corresponding `MediaFile` object is deleted.
"""
if instance.image:
if os.path.isfile(instance.image.path):
os.remove(instance.image.path)
@receiver(models.signals.pre_save, sender=Component)
@receiver(models.signals.pre_save, sender=Distributor)
@receiver(models.signals.pre_save, sender=Package)
@receiver(models.signals.pre_save, sender=Manufacturer)
def auto_delete_file_on_change(sender, instance, **kwargs):
"""
Deletes old file from filesystem
when corresponding `MediaFile` object is updated
with new file.
"""
if not instance.pk:
return False
try:
old_file = sender.objects.get(pk=instance.pk).image
except sender.DoesNotExist:
return False
new_file = instance.image
if not old_file == new_file:
if not old_file:
return True
if os.path.isfile(old_file.path):
os.remove(old_file.path)

View File

@@ -33,9 +33,8 @@ class NavBar():
def get_navbar(active_entry, user = None):
items = {
'Main': BsNavBarItem('Main', reverse('parts-main'), False),
'Components':BsNavBarItem('Components', reverse('parts-main'), False),
'Stocks':BsNavBarItem('Stocks', reverse('parts-main'), False),
'Login':BsNavBarItem('Login', reverse('parts-main'), False),
'Components':BsNavBarItem('Components', reverse('parts-components'), False),
'Stocks':BsNavBarItem('Stocks', reverse('parts-stocks'), False),
}
try:

View File

@@ -0,0 +1,58 @@
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.urls import reverse as url_reverse
import re
from .models import Storage
class QrCode:
prefix = ''
detail_view = ''
model = None
def __init__(self, prefix, detail_view, model):
self.prefix = prefix
self.detail_view = detail_view
self.model = model
class QrCodeValidator:
qr_patterns = {
'stor_uuid': QrCode('stor_uuid', 'parts-stocks-detail', Storage)
}
def __init__(self):
self.qr_regex = re.compile(r'^\[(?P<prefix>[a-zA-Z_]+)\](?P<uuid>[a-fA-F0-9\-]+)')
self.redirect_url = None
def _get_model_from_qr(self, qr):
matches = self.qr_regex.match(qr)
if matches is None:
raise ValidationError("QR Code does not match expected pattern")
qr_type = matches.group('prefix')
qr_uuid = matches.group('uuid')
url_name = self.qr_patterns[qr_type].detail_view
model = None
try:
model = self.qr_patterns[qr_type].model
except:
model = None
if model is None:
raise ValidationError('QR Pattern not registered')
return (model,qr_uuid, url_name)
def get_redirect_url(self, data):
model, uuid, url_name = self._get_model_from_qr(data)
_ = model.objects.get(id=uuid)
return url_reverse(url_name, kwargs={'uuid':uuid})
def validate(self, data, *args, **kwargs):
try:
_ = self.get_redirect_url(data)
except ObjectDoesNotExist as err:
raise ValidationError('Object with given UUID could not be found')
def __call__(self, *args, **kwargs):
self.validate(*args, **kwargs)

View File

@@ -2,5 +2,10 @@ from django.urls import path, include
from . import views as parts_views
urlpatterns = [
path('', parts_views.main_view, name='parts-main')
path('', parts_views.MainView.as_view(), name='parts-main'),
path('components/', parts_views.ComponentView.as_view(), name='parts-components'),
path('stocks/', parts_views.StockView.as_view(), name='parts-stocks'),
path('logout/', parts_views.logout_view, name='logout'),
path('login/', parts_views.login_view, name='login'),
path('stocks/<slug:uuid>', parts_views.StockViewDetail.as_view(), name='parts-stocks-detail'),
]

View File

@@ -1,20 +1,128 @@
from django.shortcuts import render, redirect
from django.urls import resolve, reverse
from django.contrib.auth import logout, login
from django.http import HttpResponse
from .navbar import NavBar
from django.contrib.auth.forms import AuthenticationForm as AuthForm
from django.views import View
import django.forms as forms
from django.views.generic import TemplateView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from .models import Storage, Stock
from .qr_parser import QrCodeValidator
def main_view(request):
class QrSearchForm(forms.Form):
my_qr_validator = QrCodeValidator()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
qr_search = forms.CharField(label='qr_search', validators=[my_qr_validator])
class BaseTemplateMixin(object):
navbar_selected = ''
base_title = ''
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
base_context = {
'navbar': NavBar.get_navbar(self.navbar_selected, self.request.user),
'title': NavBar.get_brand()+' / '+ self.base_title,
'login_active': False,
}
context['base'] = base_context
return context
def post(self, request, *args, **kwargs):
data = request.POST
if 'qr_search' not in data:
super().post(request, *args, **kwargs)
print('QR',data['qr_search'])
f = QrSearchForm(data)
if f.is_valid():
return redirect(f.my_qr_validator.get_redirect_url(f.cleaned_data['qr_search']))
return self.get(request)
class MainView(BaseTemplateMixin, TemplateView):
template_name = 'parts/main.html'
navbar_selected = 'Main'
base_title = 'Main'
def logout_view(request):
logout(request)
return redirect('parts-main')
def login_view(request):
base_context = {
'navbar': NavBar.get_navbar('Main', request.user),
'title': NavBar.get_brand()+' / '+'Main',
'navbar': NavBar.get_navbar('Login', request.user),
'title': NavBar.get_brand()+' / '+'Login',
'login_active': True,
}
if request.user.is_authenticated:
next_param = request.GET.get('next')
if next_param is not None:
return redirect(next_param)
else:
return redirect('parts-main')
if request.method == 'POST':
form = AuthForm(data=request.POST)
if form.is_valid():
valid_user = form.get_user()
login(request, valid_user)
next_param = request.GET.get('next')
if next_param is not None:
return redirect(next_param)
else:
return redirect('parts-main')
else:
form = AuthForm()
context = {
'base': base_context,
'form': form,
}
return render(request, 'parts/main.html', context)
return render(request, 'parts/login.html', context)
# Create your views here.
class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/components.html'
base_title = 'Components'
navbar_selected = 'Components'
class StockView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/stocks.html'
base_title = 'Stocks'
navbar_selected = 'Stocks'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['low_stocks'] = Stock.get_under_watermark()
context['storages'] = Storage.objects.filter(parent_storage=None)
return context
class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
template_name = 'parts/stocks-detail.html'
model = Storage
pk_url_kwarg = 'uuid'
base_title = ''
navbar_selected = 'Stocks'
def get_breadcrumbs(self):
crumbs = self.object.get_path_components()
# Reverse list and drop the last element of the reversed list
crumbs = crumbs[::-1][:-1]
return crumbs
def get_context_data(self, **kwargs):
self.base_title = 'Stocks / ' + self.object.name
context = super().get_context_data(**kwargs)
context['breadcrumbs'] = self.get_breadcrumbs()
return context