Merge pull request 'sst/storage_templates' (#3) from sst/storage_templates into develop

Reviewed-on: #3
This commit is contained in:
Stefan Strobel 2024-11-15 22:58:18 +01:00
commit 50ecaa2cc0
16 changed files with 215 additions and 134 deletions

View File

@ -1,11 +1,12 @@
from django.contrib.auth.models import User, Group
from django.contrib.auth.models import Group
from django.contrib.auth import get_user_model
from rest_framework import serializers
from parts import models as parts_models
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ['username', 'email', 'first_name', 'last_name', 'groups']
model = get_user_model()
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'groups']
class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta:

View File

@ -7,6 +7,7 @@ router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
router.register(r'groups', GroupViewSet)
router.register(r'parts/storages', PartsStorageViewSet)
router.register(r'parts/storage_templates', PartsStorageTemplatesViewSet, basename='storage-template')
router.register(r'parts/components', PartsComponentViewSet)
router.register(r'parts/stocks', PartsStockViewSet)
router.register(r'parts/packages', PartsPackageViewSet)

View File

@ -1,5 +1,6 @@
from django.shortcuts import render
from django.contrib.auth.models import User, Group
from django.contrib.auth.models import Group
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import viewsets, status
from rest_framework import permissions
@ -25,7 +26,7 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all()
queryset = get_user_model().objects.all()
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [filters.SearchFilter]
@ -44,8 +45,17 @@ class PartsStorageViewSet(viewsets.ModelViewSet):
serializer_class = StorageSerializer
permission_classes = [permissions.DjangoModelPermissions]
filter_backends = [django_filters.rest_framework.DjangoFilterBackend]
search_fields = ['id', 'name', 'parent_storage']
filterset_fields = ['id', 'name', 'parent_storage']
class PartsStorageTemplatesViewSet(viewsets.ReadOnlyModelViewSet):
queryset = parts_models.Storage.objects.filter(is_template=True)
serializer_class = StorageSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [filters.SearchFilter]
search_fields = ['id', 'name']
filterset_fields = ['id', 'name']
class PartsComponentViewSet(viewsets.ModelViewSet):
queryset = parts_models.Component.objects.all()
serializer_class = ComponentSerializer

View File

@ -1,23 +1,26 @@
from django import forms
from django.forms import widgets
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.forms import widgets
from parts import models as parts_models
from shimatta_modules.EngineeringNumberConverter import EngineeringNumberConverter
import uuid
from django.urls import reverse
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Fieldset, Row, Column
from crispy_forms.layout import Layout, Row, Column
class AutoCompleteWidget(widgets.Input):
template_name = 'widgets/autocomplete-foreign-key.html'
def __init__(self, api_search_url, image_field_name, foreign_model, name_field_name, *args, **kwargs):
def __init__(self, api_search_url, image_field_name, foreign_model, name_field_name, prepend=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.image_field_name = image_field_name
self.foreign_model = foreign_model
self.api_search_url = api_search_url
self.name_field_name = name_field_name
self.prepend = prepend
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
@ -47,14 +50,25 @@ class AutoCompleteWidget(widgets.Input):
'current_instance': instance,
'image': image,
'name_field_name': self.name_field_name,
'prepend': self.prepend,
'name': display_name,
}
return context
class AutocompleteForeingKeyField(forms.UUIDField):
def __init__(self, foreign_model=None, api_search_url=None, image_field_name='image', name_field_name='name', **kwargs):
def __init__(self,
foreign_model=None,
api_search_url=None,
image_field_name='image',
name_field_name='name',
prepend=None,
**kwargs):
super().__init__(**kwargs)
self.widget = AutoCompleteWidget(api_search_url, image_field_name, foreign_model, name_field_name)
self.widget = AutoCompleteWidget(api_search_url,
image_field_name,
foreign_model,
name_field_name,
prepend)
self.foreign_model = foreign_model
@ -73,14 +87,25 @@ class AutocompleteForeingKeyField(forms.UUIDField):
except self.foreign_model.DoesNotExist:
raise ValidationError('Given element does not exist')
return obj
class MyTestForm(forms.Form):
pass
class AddSubStorageForm(forms.Form):
class ChangeStorageForm(forms.Form):
storage_name = forms.CharField(label="storage_name", initial='')
responsible = forms.CharField(label='responsible_user')
responsible = AutocompleteForeingKeyField(api_search_url='user-list',
image_field_name=None,
name_field_name='username',
foreign_model=get_user_model(),
prepend='@')
is_template = forms.BooleanField(label='is_template', required=False)
class AddSubStorageForm(ChangeStorageForm):
template = AutocompleteForeingKeyField(api_search_url='storage-template-list',
image_field_name=None,
foreign_model=parts_models.Storage,
required=False)
class DeleteStockForm(forms.Form):
stock_uuid = forms.UUIDField()

View File

@ -0,0 +1,24 @@
# Generated by Django 3.2.5 on 2024-11-10 12:42
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('parts', '0010_auto_20220103_1606'),
]
operations = [
migrations.AddField(
model_name='storage',
name='is_template',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='storage',
name='template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='template_of', to='parts.storage'),
),
]

View File

@ -1,7 +1,7 @@
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.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.dispatch import receiver
@ -63,7 +63,15 @@ class Storage(models.Model):
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)
responsible = models.ForeignKey(get_user_model(), on_delete=models.SET_NULL, blank=True, null=True)
# allow storages to be templates which can be selected when adding new storages
is_template = models.BooleanField(default=False)
template = models.ForeignKey('self',
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name='template_of')
def get_path_components(self):
chain = []
@ -288,7 +296,7 @@ class DistributorNum(models.Model):
class QrPrintJob(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
user = models.ForeignKey(AuthUser, on_delete=models.CASCADE, null=False, blank=False)
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, null=False, blank=False)
qrdata = models.CharField(max_length=256, blank=True, null=False)
text = models.TextField(max_length=512, blank=True, null=False)
print_count = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0), MaxValueValidator(32)])
@ -335,4 +343,20 @@ def auto_delete_file_on_change(sender, instance, **kwargs):
if not old_file:
return True
if os.path.isfile(old_file.path):
os.remove(old_file.path)
os.remove(old_file.path)
@receiver(models.signals.post_save, sender=Storage)
def auto_apply_template_structure(sender, instance, created, **kwargs):
"""
Add the sub-storages from the template.
If there are nested sub-storages these will be added when the sub-storages
are created automatically.
"""
if created:
if instance.template:
for sub_storage in instance.template.storage_set.all():
Storage.objects.create(name=sub_storage.name,
parent_storage=instance,
responsible=instance.responsible,
is_template=False,
template=sub_storage)

View File

@ -1,13 +1,10 @@
from django.shortcuts import render, redirect
from django.urls import resolve, reverse
from django.urls import reverse
from django.contrib.auth import logout, login
from django.contrib.auth.models import User
from django.http import HttpResponse
from .navbar import NavBar
from django.contrib.auth.forms import AuthenticationForm as AuthForm
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth import update_session_auth_hash
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
@ -400,44 +397,31 @@ class StockView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
context['low_stocks'] = low_stock_paginator.get_page(low_stock_page)
context['storages'] = storage_paginator.get_page(storage_page)
add_stor_form = AddSubStorageForm()
add_stor_form.fields['responsible'].initial = self.request.user.username
add_stor_form.fields['responsible'].initial = self.request.user.id
context['add_storage_form'] = add_stor_form
return context
def handle_add_storage(self, request, **kwargs):
return_invalid_form = False
f = AddSubStorageForm(data=request.POST)
if f.is_valid():
new_storage_name = f.cleaned_data['storage_name']
try:
resp_user = User.objects.get(username=f.cleaned_data['responsible'])
except Exception as _:
resp_user = None
f.add_error('responsible', 'Invalid Responsible User')
return_invalid_form = True
if resp_user is not None:
try:
Storage.objects.create(name=new_storage_name, responsible=resp_user, parent_storage=None)
except ValidationError as verr:
return_invalid_form = True
f.add_error('storage_name', ' .'.join(verr.messages))
else:
return_invalid_form = True
sub_name = f.cleaned_data['storage_name']
try:
Storage.objects.create(name=sub_name,
responsible=f.cleaned_data['responsible'],
is_template=f.cleaned_data['is_template'],
template=f.cleaned_data.get('template'))
except ValidationError as v_err:
f.add_error('storage_name', '. '.join(v_err.messages))
context = self.get_context_data(**kwargs)
if return_invalid_form:
context['add_storage_form'] = f
context['add_storage_form'] = f
return self.render_to_response(context)
def post(self, request, **kwargs):
if 'submit-add-storage' in request.POST:
return self.handle_add_storage(request, **kwargs)
return super().post(request, **kwargs)
class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
template_name = 'parts/stocks-detail.html'
model = Storage
@ -496,8 +480,13 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
context['stocks'] = stocks
context['stock_search'] = stock_search_input
add_storage_form = AddSubStorageForm()
add_storage_form.fields['responsible'].initial = self.request.user.username
add_storage_form.fields['responsible'].initial = self.request.user.id
context['add_storage_form'] = add_storage_form
change_storage_form = ChangeStorageForm()
change_storage_form.fields['storage_name'].initial = self.object.name
change_storage_form.fields['responsible'].initial = self.object.responsible.id
change_storage_form.fields['is_template'].initial = self.object.is_template
context['change_storage_form'] = change_storage_form
context['delete_storage_error'] = None
context['add_stock_form'] = AddStockForm()
@ -508,17 +497,32 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
if f.is_valid():
sub_name = f.cleaned_data['storage_name']
try:
user = User.objects.get(username=f.cleaned_data['responsible'])
try:
Storage.objects.create(name=sub_name, parent_storage=self.object, responsible=user)
except ValidationError as v_err:
f.add_error('storage_name', '. '.join(v_err.messages))
except:
f.add_error('responsible', 'Invalid user')
Storage.objects.create(name=sub_name,
parent_storage=self.object,
responsible=f.cleaned_data['responsible'],
is_template=f.cleaned_data['is_template'],
template=f.cleaned_data.get('template'))
except ValidationError as v_err:
f.add_error('storage_name', '. '.join(v_err.messages))
context = self.get_context_data(**kwargs)
context['add_storage_form'] = f
return self.render_to_response(context)
def handle_change_storage_post(self, request, **kwargs):
f = ChangeStorageForm(data=request.POST)
if f.is_valid():
sub_name = f.cleaned_data['storage_name']
try:
self.object.name = f.cleaned_data['storage_name']
self.object.responsible = f.cleaned_data['responsible']
self.object.is_template = f.cleaned_data['is_template']
self.object.save()
except ValidationError as v_err:
f.add_error('storage_name', '. '.join(v_err.messages))
context = self.get_context_data(**kwargs)
context['change_storage_form'] = f
return self.render_to_response(context)
def handle_del_storage_post(self, request, **kwargs):
parent = self.object.parent_storage
try:
@ -532,6 +536,7 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
return redirect('parts-stocks')
else:
return redirect(reverse('parts-stocks-detail', kwargs={'uuid':parent.id}))
def handle_del_stock_post(self, request, **kwargs):
del_error = None
if 'stock_uuid' in request.POST:
@ -593,6 +598,8 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
if 'submit-add-storage' in request.POST:
return self.handle_add_storage_post(request, **kwargs)
elif 'submit-change-storage' in request.POST:
return self.handle_change_storage_post(request, **kwargs)
elif 'submit-delete-storage' in request.POST:
return self.handle_del_storage_post(request, **kwargs)
elif 'submit-delete-stock' in request.POST:

View File

@ -12,9 +12,9 @@ function initialize_autocompletion_foreign_key_field(search_element) {
var name_field_name = search_element.getAttribute('data-ac-name-field');
var search_url = search_element.getAttribute('data-ac-url');
var base_id = search_element.getAttribute('id');
var uuid_field = search_element.parentElement.querySelector('#'+base_id+'-uuid-field');
var dflex_container = search_element.parentElement.querySelector('#'+base_id+'-dflex-container');
var initial_delete_button = search_element.parentElement.querySelector('[data-ac-delete]');
var uuid_field = search_element.parentElement.parentElement.querySelector('#'+base_id+'-uuid-field');
var dflex_container = search_element.parentElement.parentElement.querySelector('#'+base_id+'-dflex-container');
var initial_delete_button = search_element.parentElement.parentElement.querySelector('[data-ac-delete]');
console.log(initial_delete_button);
console.log(image_field_name);

View File

@ -29,6 +29,10 @@ function api_search_user(search, onSuccess, onFail) {
return api_ajax_request_without_send('GET', api_urls_v1['user-list']+`?search=${encodeURIComponent(search)}`, function(method, url, json) {onSuccess(json);}, onFail);
}
function api_search_storage_template(search, onSuccess, onFail) {
return api_ajax_request_without_send('GET', api_urls_v1['storage-template-list']+`?search=${encodeURIComponent(search)}`, function(method, url, json) {onSuccess(json);}, onFail);
}
function api_search_component(search, onSuccess, onFail) {
return api_ajax_request_without_send('GET', api_urls_v1['component-list']+`?search=${encodeURIComponent(search)}`, function(method, url, json) {onSuccess(json);}, onFail);
}

View File

@ -65,6 +65,7 @@
'user-list': '{% url 'user-list' %}',
'groups-list': '{% url 'user-list' %}',
'storage-list': '{% url 'storage-list' %}',
'storage-template-list': '{% url 'storage-template-list' %}',
'component-list': '{% url 'component-list' %}',
'package-list': '{% url 'package-list' %}',
'stock-list': '{% url 'stock-list' %}',

View File

@ -0,0 +1,22 @@
{% load static %}
{% load crispy_forms_tags %}
<div class="modal fade" id="add-sub-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Storage</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="modal-body">
{{form|crispy}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" value="Add Storage" name="submit-add-storage">
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,22 @@
{% load static %}
{% load crispy_forms_tags %}
<div class="modal fade" id="change-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Change Storage</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="modal-body">
{{form|crispy}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" value="Change Storage" name="submit-change-storage">
</div>
</form>
</div>
</div>
</div>

View File

@ -1,46 +0,0 @@
{% load static %}
<div class="modal fade" id="add-sub-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Storage</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="" method="post">
{% csrf_token %}
<div class="modal-body">
<label for="add-storage-name" class="form-label">Storage Name</label>
<div class="input-group has-validation">
<input value="{{form.storage_name.value}}" class="form-control{% if form.storage_name.errors or form.non_field_errors %} is-invalid{% endif %}" id="add-storage-name" name="{{form.storage_name.name}}" type="text" aria-describedby="validationStorageName" required>
<div id="validationStorageName" class="invalid-feedback">
{% for msg in form.storage_name.errors %}
{{msg}}
{% endfor %}
</div>
</div>
<label for="{{form.responsible.id_for_label}}">Responsible</label>
<div class="input-group has-validation dropdown">
<span class="input-group-text" id="add_storage_username_prepend">@</span><input autocomplete="off" data-bs-toggle="dropdown" type="text" value="{{form.responsible.value}}" class="form-control{% if form.responsible.errors or form.non_field_errors %} is-invalid{% endif %}" id="{{form.responsible.id_for_label}}" name="{{form.responsible.name}}" aria-describedby="add_storage_username_prepend validationServerUsernameFeedback" required>
<ul class="dropdown-menu" aria-labelledby="{{form.responsible.id_for_label}}" id="{{form.responsible.id_for_label}}-ac-dropdown">
</ul>
<div id="validationServerUsernameFeedback" class="invalid-feedback">
{% for msg in form.responsible.errors %}
{{msg}}
{% endfor %}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" value="Add Storage" name="submit-add-storage">
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p class="text-danger text-center">{{ error }}</p>
{% endfor %}
{% endif %}
</div>
</form>
</div>
</div>
</div>

View File

@ -27,6 +27,7 @@
{% endif %}
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#delete-storage-modal">Delete</button>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-sub-modal"><i class="bi bi-plus-circle"></i></button>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#change-modal"><i class="bi bi-pen-fill"></i></button>
</h1>
<div class="list-group">
{% for storage in storages %}
@ -109,7 +110,11 @@
{% endfor %}
<!-- Modal for adding a substorage-->
{% with add_storage_form as form %}
{% include 'parts/modals/new-substorage-modal.html' %}
{% include 'parts/modals/add-substorage-modal.html' %}
{% endwith %}
<!-- Modal to change current storag-->
{% with change_storage_form as form %}
{% include 'parts/modals/change-storage-modal.html' %}
{% endwith %}
<!-- Modal for deleting this storage -->
{% with delete_storage_errors as err_msgs %}
@ -158,18 +163,6 @@ api_get_component_from_id(uuid, function(component){
}
{% endif %}
new AutocompleteText('{{add_storage_form.responsible.id_for_label}}', '{{add_storage_form.responsible.id_for_label}}-ac-dropdown',
function(search, autocomplete_obj) {
api_search_user(search, function(results) {
var usernames = new Array();
console.log(results);
for (var i = 0; i < results.results.length; i++) {
usernames.push(results.results[i].username);
}
console.log(usernames);
autocomplete_obj.show_results(usernames);
}, function(){});
});
</script>
{% endblock custom_scripts %}

View File

@ -54,7 +54,7 @@
<!-- Add storage modal form -->
{% with add_storage_form as form %}
{% include 'parts/modals/new-substorage-modal.html' %}
{% include 'parts/modals/add-substorage-modal.html' %}
{% endwith %}
</div>
@ -67,18 +67,6 @@
var modal = bootstrap.Modal.getOrCreateInstance(addSubStorageModal);
modal.show();
{% endif %}
new AutocompleteText('{{add_storage_form.responsible.id_for_label}}', '{{add_storage_form.responsible.id_for_label}}-ac-dropdown',
function(search, autocomplete_obj) {
api_search_user(search, function(results) {
var usernames = new Array();
console.log(results);
for (var i = 0; i < results.results.length; i++) {
usernames.push(results.results[i].username);
}
console.log(usernames);
autocomplete_obj.show_results(usernames);
}, function(){});
});
</script>
{% endblock custom_scripts %}

View File

@ -1,7 +1,12 @@
<div class="dropdown">
<input autocomplete="off" id="{{widget.attrs.id}}" data-ac-url="{{custom.search_url}}" data-ac-name-field="{{custom.name_field_name}}" {% if custom.image_field_name %}data-ac-image-field="{{custom.image_field_name}}"{% endif %} data-bs-toggle="dropdown" type="text" placeholder="Search..." class="{{widget.attrs.class}}">
<ul id="{{widget.attrs.id}}-ac-ul" class="dropdown-menu">
</ul>
<div class="input-group">
{% if custom.prepend %}
<span class="input-group-text" id="{{widget.attrs.id}}-prepend">{{custom.prepend}}</span>
{% endif %}
<input autocomplete="off" id="{{widget.attrs.id}}" data-ac-url="{{custom.search_url}}" data-ac-name-field="{{custom.name_field_name}}" {% if custom.image_field_name %}data-ac-image-field="{{custom.image_field_name}}"{% endif %} data-bs-toggle="dropdown" type="text" placeholder="Search..." class="{{widget.attrs.class}}">
<ul id="{{widget.attrs.id}}-ac-ul" class="dropdown-menu">
</ul>
</div>
<div class="d-flex align-items-center mt-3 mb-3" id="{{widget.attrs.id}}-dflex-container">
{% if custom.current_instance %}
{% if custom.image_field_name %}