sst/storage_templates #3

Merged
sst merged 5 commits from sst/storage_templates into develop 2024-11-15 22:58:18 +01:00
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()
mhu marked this conversation as resolved
Review

Mh. According to https://stackoverflow.com/a/24630589/5761843
another way of retreiving the user model should be used.

get_user_model cannot guarantee that the User model is already loaded into the app cache. It might work in your specific setup, but it is a hit-and-miss scenario. If you change some settings (e.g. the order of INSTALLED_APPS) it might very well break the import and you will have to spend additional time debugging.

settings.AUTH_USER_MODEL will pass a string as the foreign key model, and if the retrieval of the model class fails at the time this foreign key is imported, the retrieval will be delayed until all model classes are loaded into the cache.

Mh. According to https://stackoverflow.com/a/24630589/5761843 another way of retreiving the user model should be used. > get_user_model cannot guarantee that the User model is already loaded into the app cache. It might work in your specific setup, but it is a hit-and-miss scenario. If you change some settings (e.g. the order of INSTALLED_APPS) it might very well break the import and you will have to spend additional time debugging. >settings.AUTH_USER_MODEL will pass a string as the foreign key model, and if the retrieval of the model class fails at the time this foreign key is imported, the retrieval will be delayed until all model classes are loaded into the cache.
Review

Ah. shit. Nevermind. Very old post. They changed that way back in django 1.xxx

Ah. shit. Nevermind. Very old post. They changed that way back in django 1.xxx
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
@ -74,13 +88,24 @@ class AutocompleteForeingKeyField(forms.UUIDField):
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)])
@ -336,3 +344,19 @@ def auto_delete_file_on_change(sender, instance, **kwargs):
return True
if os.path.isfile(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,36 +397,23 @@ 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']
sub_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
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
return self.render_to_response(context)
def post(self, request, **kwargs):
@ -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)
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))
except:
f.add_error('responsible', 'Invalid user')
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:
sst marked this conversation as resolved Outdated
Outdated
Review

Any reason, that is not an elif? @sst

Any reason, that is not an elif? @sst
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">
<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 %}