diff --git a/shimatta_kenkyusho/api/serializers.py b/shimatta_kenkyusho/api/serializers.py index ffb405f..7bf01a8 100644 --- a/shimatta_kenkyusho/api/serializers.py +++ b/shimatta_kenkyusho/api/serializers.py @@ -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: diff --git a/shimatta_kenkyusho/api/urls.py b/shimatta_kenkyusho/api/urls.py index 18b7644..1aa3de9 100644 --- a/shimatta_kenkyusho/api/urls.py +++ b/shimatta_kenkyusho/api/urls.py @@ -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) diff --git a/shimatta_kenkyusho/api/views.py b/shimatta_kenkyusho/api/views.py index 8ca59aa..137e344 100644 --- a/shimatta_kenkyusho/api/views.py +++ b/shimatta_kenkyusho/api/views.py @@ -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 diff --git a/shimatta_kenkyusho/parts/forms.py b/shimatta_kenkyusho/parts/forms.py index e33173b..fbbf9fd 100644 --- a/shimatta_kenkyusho/parts/forms.py +++ b/shimatta_kenkyusho/parts/forms.py @@ -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() diff --git a/shimatta_kenkyusho/parts/migrations/0011_auto_20241110_1242.py b/shimatta_kenkyusho/parts/migrations/0011_auto_20241110_1242.py new file mode 100644 index 0000000..692e1aa --- /dev/null +++ b/shimatta_kenkyusho/parts/migrations/0011_auto_20241110_1242.py @@ -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'), + ), + ] diff --git a/shimatta_kenkyusho/parts/models.py b/shimatta_kenkyusho/parts/models.py index 075003c..f5c3de8 100644 --- a/shimatta_kenkyusho/parts/models.py +++ b/shimatta_kenkyusho/parts/models.py @@ -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) \ No newline at end of file + 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) diff --git a/shimatta_kenkyusho/parts/views.py b/shimatta_kenkyusho/parts/views.py index 2eff882..585f19f 100644 --- a/shimatta_kenkyusho/parts/views.py +++ b/shimatta_kenkyusho/parts/views.py @@ -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: diff --git a/shimatta_kenkyusho/static/js/autocomplete-foreign-key-field.js b/shimatta_kenkyusho/static/js/autocomplete-foreign-key-field.js index 37bb883..0ff2e41 100644 --- a/shimatta_kenkyusho/static/js/autocomplete-foreign-key-field.js +++ b/shimatta_kenkyusho/static/js/autocomplete-foreign-key-field.js @@ -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); diff --git a/shimatta_kenkyusho/static/js/kenyusho-api-v1.js b/shimatta_kenkyusho/static/js/kenyusho-api-v1.js index d801c74..4acd877 100644 --- a/shimatta_kenkyusho/static/js/kenyusho-api-v1.js +++ b/shimatta_kenkyusho/static/js/kenyusho-api-v1.js @@ -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); } diff --git a/shimatta_kenkyusho/templates/base.html b/shimatta_kenkyusho/templates/base.html index 7ec0325..298c945 100644 --- a/shimatta_kenkyusho/templates/base.html +++ b/shimatta_kenkyusho/templates/base.html @@ -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' %}', diff --git a/shimatta_kenkyusho/templates/parts/modals/add-substorage-modal.html b/shimatta_kenkyusho/templates/parts/modals/add-substorage-modal.html new file mode 100644 index 0000000..9bf9d0e --- /dev/null +++ b/shimatta_kenkyusho/templates/parts/modals/add-substorage-modal.html @@ -0,0 +1,22 @@ +{% load static %} +{% load crispy_forms_tags %} + \ No newline at end of file diff --git a/shimatta_kenkyusho/templates/parts/modals/change-storage-modal.html b/shimatta_kenkyusho/templates/parts/modals/change-storage-modal.html new file mode 100644 index 0000000..6a13675 --- /dev/null +++ b/shimatta_kenkyusho/templates/parts/modals/change-storage-modal.html @@ -0,0 +1,22 @@ +{% load static %} +{% load crispy_forms_tags %} + \ No newline at end of file diff --git a/shimatta_kenkyusho/templates/parts/modals/new-substorage-modal.html b/shimatta_kenkyusho/templates/parts/modals/new-substorage-modal.html deleted file mode 100644 index c362f55..0000000 --- a/shimatta_kenkyusho/templates/parts/modals/new-substorage-modal.html +++ /dev/null @@ -1,46 +0,0 @@ -{% load static %} - \ No newline at end of file diff --git a/shimatta_kenkyusho/templates/parts/stocks-detail.html b/shimatta_kenkyusho/templates/parts/stocks-detail.html index e2c79a5..4276312 100644 --- a/shimatta_kenkyusho/templates/parts/stocks-detail.html +++ b/shimatta_kenkyusho/templates/parts/stocks-detail.html @@ -27,6 +27,7 @@ {% endif %} +
{% for storage in storages %} @@ -109,7 +110,11 @@ {% endfor %} {% with add_storage_form as form %} - {% include 'parts/modals/new-substorage-modal.html' %} + {% include 'parts/modals/add-substorage-modal.html' %} + {% endwith %} + + {% with change_storage_form as form %} + {% include 'parts/modals/change-storage-modal.html' %} {% endwith %} {% 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(){}); -}); {% endblock custom_scripts %} diff --git a/shimatta_kenkyusho/templates/parts/stocks.html b/shimatta_kenkyusho/templates/parts/stocks.html index f6bbef0..38147c6 100644 --- a/shimatta_kenkyusho/templates/parts/stocks.html +++ b/shimatta_kenkyusho/templates/parts/stocks.html @@ -54,7 +54,7 @@ {% with add_storage_form as form %} - {% include 'parts/modals/new-substorage-modal.html' %} + {% include 'parts/modals/add-substorage-modal.html' %} {% endwith %}
@@ -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(){}); - }); {% endblock custom_scripts %} diff --git a/shimatta_kenkyusho/templates/widgets/autocomplete-foreign-key.html b/shimatta_kenkyusho/templates/widgets/autocomplete-foreign-key.html index bf80086..66d48b3 100644 --- a/shimatta_kenkyusho/templates/widgets/autocomplete-foreign-key.html +++ b/shimatta_kenkyusho/templates/widgets/autocomplete-foreign-key.html @@ -1,7 +1,12 @@