diff --git a/shimatta_kenkyusho/.vscode/launch.json b/.vscode/launch.json similarity index 75% rename from shimatta_kenkyusho/.vscode/launch.json rename to .vscode/launch.json index 93516fc..888ae82 100644 --- a/shimatta_kenkyusho/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,10 +6,9 @@ "configurations": [ { "name": "Python: Django", - "type": "python", + "type": "debugpy", "request": "launch", - "pythonPath": "${workspaceFolder}/../myenv/bin/python", - "program": "${workspaceFolder}/manage.py", + "program": "${workspaceFolder}/shimatta_kenkyusho/manage.py", "args": [ "runserver", "0.0.0.0:8000" diff --git a/requirements.txt b/requirements.txt index ba086c8..6dd0753 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,16 @@ -asgiref==3.4.1 +annotated-types==0.7.0 +asgiref==3.8.1 astroid==2.6.5 certifi==2024.8.30 charset-normalizer==3.4.0 -crispy-bootstrap5==0.6 -Django==3.2.5 -django-crispy-forms==1.13.0 +crispy-bootstrap5==2024.10 +Django==5.1.3 +django-crispy-forms==2.3 django-filter==2.4.0 -django-qr-code==2.2.0 +django-qr-code==4.1.0 django-rest-framework==0.1.0 -django-tex==1.1.9.post1 -djangorestframework==3.12.4 +django-tex==1.1.10 +djangorestframework==3.15.2 gunicorn==21.2.0 idna==3.10 isort==5.9.3 @@ -19,15 +20,16 @@ MarkupSafe==2.0.1 mccabe==0.6.1 packaging==24.2 Pillow==8.3.1 +pipdeptree==2.23.4 psycopg2-binary==2.9.9 +pydantic==2.9.2 +pydantic_core==2.23.4 pylint==2.9.6 -pytz==2021.1 -qrcode==7.2 requests==2.32.3 -segno==1.3.3 +segno==1.6.1 setuptools==75.3.0 -six==1.16.0 sqlparse==0.4.1 toml==0.10.2 +typing_extensions==4.12.2 urllib3==2.2.3 wrapt==1.12.1 diff --git a/shimatta_kenkyusho/api/urls.py b/shimatta_kenkyusho/api/urls.py index 688b109..65a3907 100644 --- a/shimatta_kenkyusho/api/urls.py +++ b/shimatta_kenkyusho/api/urls.py @@ -1,7 +1,6 @@ -from django.urls import include, path +from django.urls import include, path, re_path from rest_framework import routers from .views import * -from django.conf.urls import url router = routers.DefaultRouter() router.register(r'users', UserViewSet) @@ -20,6 +19,6 @@ router.register(r'parts/component-param-types', PartsComponentParameterTypeViewS urlpatterns = [ path('', include(router.urls)), - url(r'^token-auth/', ObtainExpiringAuthToken.as_view()), - url(r'^token-logout/', TokenLogout.as_view()), + re_path(r'^token-auth/', ObtainExpiringAuthToken.as_view()), + re_path(r'^token-logout/', TokenLogout.as_view()), ] \ No newline at end of file diff --git a/shimatta_kenkyusho/parts/forms.py b/shimatta_kenkyusho/parts/forms.py index 7c7b392..5e1743c 100644 --- a/shimatta_kenkyusho/parts/forms.py +++ b/shimatta_kenkyusho/parts/forms.py @@ -6,6 +6,7 @@ from parts import models as parts_models from shimatta_modules.EngineeringNumberConverter import EngineeringNumberConverter import uuid from django.urls import reverse +from .qr_parser import QrCodeValidator from crispy_forms.helper import FormHelper @@ -338,3 +339,12 @@ class ComponentParameterCreateForm(forms.Form): text_value = '' value = self.cleaned_data['number_value'] parts_models.ComponentParameter.objects.create(parameter_type=param_type, component=component, value=value, text_value=text_value) + +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]) \ No newline at end of file diff --git a/shimatta_kenkyusho/parts/urls.py b/shimatta_kenkyusho/parts/urls.py index 8e9fd1b..301d83a 100644 --- a/shimatta_kenkyusho/parts/urls.py +++ b/shimatta_kenkyusho/parts/urls.py @@ -14,7 +14,7 @@ urlpatterns = [ path('components//', parts_views.ComponentDetailView.as_view(), name='parts-components-detail'), path('packages//', parts_views.PackageDetailView.as_view(), name='parts-packages-detail'), path('distributors//', parts_views.DistributorDetailView.as_view(), name='parts-distributors-detail'), - path('manufacturers/', parts_views.ManufacturersViewSet.as_view(), name='parts-manufacturers'), - path("manufacturers//", parts_views.ManufacturerDetailViewSet.as_view(), name='parts-manufacturers-detail'), + path('manufacturers/', parts_views.ManufacturersView.as_view(), name='parts-manufacturers'), + path("manufacturers//", parts_views.ManufacturerDetailView.as_view(), name='parts-manufacturers-detail'), path("healthcheck/", parts_views.health_check_view, name='parts-health-check'), ] diff --git a/shimatta_kenkyusho/parts/views.py b/shimatta_kenkyusho/parts/views.py deleted file mode 100644 index 4d42474..0000000 --- a/shimatta_kenkyusho/parts/views.py +++ /dev/null @@ -1,983 +0,0 @@ -from django.shortcuts import render, redirect -from django.urls import reverse -from django.contrib.auth import logout, login -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 -import django.forms as forms -from django.views.generic import TemplateView, DetailView -from django.contrib.auth.mixins import LoginRequiredMixin -from .models import Storage, Stock, Component, Distributor, Manufacturer, Package, ComponentParameter, DistributorNum -from .qr_parser import QrCodeValidator -from django.core.paginator import Paginator -from django.core.exceptions import ValidationError -from django.db import IntegrityError -from django.db.models import ProtectedError -from .forms import * -from .component_import import import_components_from_csv -from django.db.models import Q -from django.db.models.functions import Lower -from django.forms import formset_factory -from django.http import HttpResponse -import uuid - - -ParameterSearchFormSet = formset_factory(ComponentParameterSearchForm, extra=1) - -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 ChangePasswordView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): - template_name = 'parts/change-pw.html' - navbar_selected = 'Main' - base_title = 'Change Password' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['form'] = PasswordChangeForm(self.request.user) - return context - - def post(self, request, *args, **kwargs): - if 'submit-change-pw' not in request.POST: - return super().post(request, *args, **kwargs) - - form = PasswordChangeForm(request.user, data=request.POST) - - if form.is_valid(): - user = form.save() - update_session_auth_hash(request, user) - return redirect('parts-main') - else: - pass - - context = self.get_context_data(**kwargs) - if form.errors: - context['form'] = form - - return self.render_to_response(context) - - -class MainView(BaseTemplateMixin, TemplateView): - template_name = 'parts/main.html' - navbar_selected = 'Main' - base_title = 'Main' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['user'] = self.request.user - return context - - -def logout_view(request): - logout(request) - return redirect('parts-main') - -def login_view(request): - base_context = { - '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/login.html', context) - -# Create your views here. - -class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): - template_name = 'parts/components.html' - base_title = 'Components' - navbar_selected = 'Components' - default_page_size = 25 - - def get_component_query_set(self, search_string): - queryset = Component.objects.all() - - if not search_string: - return queryset - - 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)) - - return queryset - - def get_component_queryset_from_advanced_search(self, cleaned_data): - queryset = Component.objects.all() - - if cleaned_data['name']: - queryset = queryset.filter(Q(name__icontains=cleaned_data['name'])) - if cleaned_data['package']: - queryset = queryset.filter(package=cleaned_data['package']) - if cleaned_data['package_pin_count']: - queryset = queryset.filter(package__pin_count=cleaned_data['package_pin_count']) - if cleaned_data['component_type']: - queryset = queryset.filter(component_type=cleaned_data['component_type']) - if cleaned_data['distributor_num']: - if cleaned_data['distributor']: - distri = cleaned_data['distributor'] - queryset = queryset.filter(Q(distributornum__distributor_part_number__icontains=cleaned_data['distributor_num']) & Q(distributornum__distributor=distri)) - else: - queryset = queryset.filter(Q(distributornum__distributor_part_number__icontains=cleaned_data['distributor_num'])) - if cleaned_data['manufacturer']: - queryset = queryset.filter(manufacturer=cleaned_data['manufacturer']) - return queryset - - def get_context_data_int(self, advanced_search, parameter_formset : ParameterSearchFormSet, **kwargs): - context = super().get_context_data(**kwargs) - - comp_page_num = self.request.GET.get('comp_page', default=1) - - if advanced_search and parameter_formset: - search = None - context['advanced_search_shown'] = True - context['advanced_search_form'] = advanced_search - context['advanced_search_param_formset'] = parameter_formset - if advanced_search.is_valid(): - paginator_queryset = self.get_component_queryset_from_advanced_search(advanced_search.cleaned_data) - else: - paginator_queryset = Component.objects.all() - - if parameter_formset.is_valid(): - # Process parameters - pass - - else: - search = self.request.GET.get('search', default=None) - paginator_queryset = self.get_component_query_set(search) - - comp_paginator = Paginator(paginator_queryset, self.default_page_size) - - context['components'] = comp_paginator.get_page(comp_page_num) - context['comp_form'] = ComponentForm() - context['import_comp_form'] = ImportComponentForm() - context['search_string'] = search - - if not parameter_formset: - context['advanced_search_param_formset'] = ParameterSearchFormSet() - - - if not advanced_search: - context['advanced_search_form'] = AdvancedComponentSearchForm(auto_id='adv_search_%s') - - return context - - def get_context_data(self, **kwargs): - return self.get_context_data_int(advanced_search = None, parameter_formset=None, **kwargs) - - def handle_new_component_post(self, request, open=False, **kwargs): - cform = ComponentForm(data=request.POST, files=request.FILES) - new_component = None - if cform.is_valid(): - new_component = cform.save() - - context = self.get_context_data(**kwargs) - if not cform.is_valid(): - context['comp_form'] = cform - - if open and new_component: - return redirect(reverse('parts-components-detail', kwargs={'uuid':new_component.id})) - return self.render_to_response(context) - - def handle_import_components_post(self, request, open=False, **kwargs): - cform = ImportComponentForm(data=request.POST, files=request.FILES) - context = self.get_context_data(**kwargs) - if cform.is_valid(): - try: - import_components_from_csv(cform.files['csv_file'].file) - except Exception as ex: - cform.add_error('csv_file', str(ex)) - context['import_comp_form'] = cform - else: - context['import_comp_form'] = cform - - return self.render_to_response(context) - - def handle_advanced_search_post(self, request, **kwargs): - - form = AdvancedComponentSearchForm(auto_id='adv_search_%s', data=request.POST) - param_formset = ParameterSearchFormSet(data=request.POST) - - if form.is_valid(): - print('Valid') - - if param_formset.is_valid(): - print('Formset is valid!') - - - context = self.get_context_data_int(form, param_formset, **kwargs) - return self.render_to_response(context) - - def post(self, request, *args, **kwargs): - if 'submit-edit-component' in request.POST: - return self.handle_new_component_post(request, open=False, **kwargs) - elif 'submit-edit-component-open' in request.POST: - return self.handle_new_component_post(request, open=True, **kwargs) - elif 'submit-import-components' in request.POST: - return self.handle_import_components_post(request, open=True, **kwargs) - elif 'submit-advanced-search' in request.POST: - return self.handle_advanced_search_post(request, **kwargs) - else: - return super().post(request, *args, **kwargs) - -class PackageView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): - template_name = 'parts/packages.html' - base_title = 'Packages' - navbar_selected = 'Packages' - default_page_size = 25 - - def search_packages(self, search): - qs = Package.objects.all() - - if not search: - return qs - - search_fragments = search.strip().split() - for search in search_fragments: - if search.lower() == 'smd': - s_filter = Q(name__icontains = search) | Q(smd = True) - else: - try: - pin_count = int(search) - s_filter = Q(name__icontains = search) | Q(pin_count=pin_count) - except: - s_filter = Q(name__icontains = search) - - qs = qs.filter(s_filter) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - page_num = self.request.GET.get('page', default=1) - search_string = self.request.GET.get('search', default=None) - - package_queryset = self.search_packages(search_string) - - paginator = Paginator(package_queryset, self.default_page_size) - - - context['search_string'] = search_string - context['packages'] = paginator.get_page(page_num) - context['new_pkg_form'] = PackageForm() - - return context - - def handle_add_new_package(self, request): - - form = PackageForm(data=request.POST, files=request.FILES) - if form.is_valid(): - form.save() - - context = self.get_context_data() - - if not form.is_valid(): - context['new_pkg_form'] = form - - return self.render_to_response(context) - - def post(self, request, *args, **kwargs): - if 'submit-pkg-add-new' in request.POST: - return self.handle_add_new_package(request) - - return super().post(request, *args, **kwargs) - - -class DistributorView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): - template_name = 'parts/distributors.html' - base_title = 'Distributors' - navbar_selected = 'Distributors' - default_page_size = 25 - - def search_distributors(self, search): - qs = Distributor.objects.all() - - if not search: - return qs - - search_fragments = search.strip().split() - - for search in search_fragments: - qs = qs.filter(Q(name__icontains = search) | Q(website__icontains = search)) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - page_num = self.request.GET.get('page', default=1) - search_string = self.request.GET.get('search', default=None) - - queryset = self.search_distributors(search_string) - - paginator = Paginator(queryset, self.default_page_size) - - - context['search_string'] = search_string - context['distributors'] = paginator.get_page(page_num) - context['new_distri_form'] = DistributorForm() - - return context - - def handle_add_new_distributor(self, request): - - form = DistributorForm(data=request.POST, files=request.FILES) - if form.is_valid(): - form.save() - - context = self.get_context_data() - - if not form.is_valid(): - context['new_distri_form'] = form - - return self.render_to_response(context) - - def post(self, request, *args, **kwargs): - if 'submit-distri-add-new' in request.POST: - return self.handle_add_new_distributor(request) - - return super().post(request, *args, **kwargs) - -class StockView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): - template_name = 'parts/stocks.html' - base_title = 'Stocks' - navbar_selected = 'Stocks' - default_pagination_size = 25 - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - storage_page = self.request.GET.get('storage_page') - if storage_page is None: - storage_page = 1 - low_stock_page = self.request.GET.get('low_stock_page') - if low_stock_page is None: - low_stock_page = 1 - - storage_paginator = Paginator(Storage.objects.filter(parent_storage=None), self.default_pagination_size) - low_stock_paginator = Paginator(Stock.get_under_watermark(), - self.default_pagination_size) - - 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.id - context['add_storage_form'] = add_stor_form - return context - - def handle_add_storage(self, request, **kwargs): - f = AddSubStorageForm(data=request.POST) - if f.is_valid(): - 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) - 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 - pk_url_kwarg = 'uuid' - base_title = '' - navbar_selected = 'Stocks' - default_pagination_size = 8 - - 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 search_stock_queryset(self, search): - stocks_in_storage = Stock.objects.filter(storage=self.object).order_by(Lower('component__name')) - - if search is None or search == '': - return stocks_in_storage - - if search.startswith('[comp_uuid]'): - search = search.replace('[comp_uuid]', '') - - - # Check if the searhc equals a UUID - test_uuid = None - try: - test_uuid = uuid.UUID(search) - except: - pass - - if test_uuid is not None: - stocks_in_storage = stocks_in_storage.filter(Q(component__id = test_uuid) | Q(id= test_uuid)) - else: - stocks_in_storage = stocks_in_storage.filter(Q(component__name__icontains = search) | - Q(component__package__name__icontains = search) | - Q(component__manufacturer__name__icontains = search)) - - return stocks_in_storage - - def get_context_data(self, **kwargs): - self.base_title = 'Stocks / ' + self.object.name - context = super().get_context_data(**kwargs) - context['breadcrumbs'] = self.get_breadcrumbs() - - storage_page = self.request.GET.get('storage_page', default=1) - storage_paginator = Paginator(Storage.objects.filter(parent_storage=self.object), self.default_pagination_size) - stock_search_input = self.request.GET.get('search') - - componente_stock_page = self.request.GET.get('stock_page', default=1) - - stock_paginator = Paginator(self.search_stock_queryset(stock_search_input), self.default_pagination_size) - - context['storages'] = storage_paginator.get_page(storage_page) - stocks = stock_paginator.get_page(componente_stock_page) - context['stocks'] = stocks - context['stock_search'] = stock_search_input - add_storage_form = AddSubStorageForm() - 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['verbose_name'].initial = self.object.verbose_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() - - return context - - def handle_add_storage_post(self, request, **kwargs): - f = AddSubStorageForm(data=request.POST) - if f.is_valid(): - sub_name = f.cleaned_data['storage_name'] - try: - Storage.objects.create(name=sub_name, - verbose_name=f.cleaned_data.get('verbose_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.verbose_name = f.cleaned_data.get('verbose_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: - self.object.delete() - except: - context = self.get_context_data(**kwargs) - context['delete_storage_errors'] = ['Error deleting Storage '+str(self.object)] - return self.render_to_response(context) - - if parent is None: - 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: - f = DeleteStockForm(data=request.POST) - if f.is_valid(): - try: - s = Stock.objects.get(id=f.cleaned_data['stock_uuid']) - print(s.storage) - print(self.object) - if s.storage == self.object: - s.delete() - else: - del_error = 'Cannot delete stock from another storage.' - except: - del_error = 'Could not find requested stock in this storage.' - - context = self.get_context_data(**kwargs) - return self.render_to_response(context) - - def handle_update_watermark(self, request, **kwargs): - edit_form = EditWatermarkForm(data=request.POST) - update_watermark_error = None - if edit_form.is_valid(): - edit_form.save() - else: - pass # Todo: Handle error - - context = self.get_context_data(**kwargs) - return self.render_to_response(context) - - def handle_amount_change_post(self, request, increase, **kwargs): - edit_form = EditStockAmountForm(data=request.POST) - if edit_form.is_valid(): - edit_form.save(increase) - - context = self.get_context_data(**kwargs) - return self.render_to_response(context) - - def handle_add_stock_post(self, request, **kwargs): - f = AddStockForm(data=request.POST) - error_occured = False - - if f.is_valid(): - try: - f.save(self.object) - except Exception as ex: - f.add_error('', str(ex)) - error_occured = True - else: - error_occured = True - - context = self.get_context_data(**kwargs) - if error_occured: - context['add_stock_form'] = f - return self.render_to_response(context) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - - 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: - return self.handle_del_stock_post(request, **kwargs) - elif 'submit-edit-watermark' in request.POST: - return self.handle_update_watermark(request, **kwargs) - elif 'submit-amount-reduce' in request.POST: - return self.handle_amount_change_post(request, False, **kwargs) - elif 'submit-amount-increase' in request.POST: - return self.handle_amount_change_post(request, True, **kwargs) - elif 'submit-add-stock' in request.POST: - return self.handle_add_stock_post(request, **kwargs) - - return super().post(request, *args, **kwargs) - -class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): - template_name = 'parts/components-detail.html' - model = Component - pk_url_kwarg = 'uuid' - base_title = '' - navbar_selected = 'Components' - - def get_context_data(self, **kwargs): - self.base_title = 'Component / '+self.object.name - context = super().get_context_data(**kwargs) - context['component'] = self.object - context['stocks'] = Stock.objects.filter(component=self.object) - context['comp_form'] = ComponentForm(instance=self.object) - context['new_distri_num_form'] = DistributorNumberCreateForm() - context['new_param_form'] = ComponentParameterCreateForm() - context['distri_nums'] = DistributorNum.objects.filter(component=self.object).order_by('distributor__name') - context['parameters'] = ComponentParameter.objects.filter(component=self.object).order_by('parameter_type__parameter_name') - - return context - - def handle_submit_edit_component_post(self, request, **kwargs): - cform = ComponentForm(instance=self.object, data=request.POST, files=request.FILES) - if cform.is_valid(): - cform.save() - - context = self.get_context_data(**kwargs) - if not cform.is_valid(): - context['comp_form'] = cform - - return self.render_to_response(context) - - def handle_submit_delete_post(self, request, **kwargs): - delete_error = None - protected_stuff = None - - try: - self.object.delete() - except ProtectedError as pe: - delete_error = 'Component is protected' - protected_stuff = pe.protected_objects - except: - delete_error = 'Cannot delete component. Unknown error' - - if delete_error is None: - return redirect('parts-components') - else: - context = self.get_context_data(**kwargs) - context['delete_error'] = delete_error - context['protected_stuff'] = protected_stuff - return self.render_to_response(context) - - def handle_submit_new_distri_num_post(self, request, **kwargs): - form = DistributorNumberCreateForm(data=request.POST) - if form.is_valid(): - new_number = form.save(commit=False) - new_number.component = self.object - try: - new_number.save() - except IntegrityError as ie: - form.add_error('__all__', 'Number for given distributor already exists') - - context = self.get_context_data(**kwargs) - if not form.is_valid(): - context['new_distri_num_form'] = form - return self.render_to_response(context) - - def handle_submit_delete_distri_num_post(self, request, **kwargs): - form = DistributorNumberDeleteForm(data=request.POST) - if form.is_valid(): - form.cleaned_data['distributor_num'].delete() - - context = self.get_context_data(**kwargs) - return self.render_to_response(context) - - def handle_submit_delete_param_post(self, request, **kwargs): - form = ComponentParameterDeleteForm(data=request.POST) - if form.is_valid(): - form.cleaned_data['param_num'].delete() - - context = self.get_context_data(**kwargs) - return self.render_to_response(context) - - def handle_submit_new_param_post(self, request, **kwargs): - form = ComponentParameterCreateForm(data=request.POST) - - if form.is_valid(): - try: - form.save(self.object) - except IntegrityError: - form.add_error('__all__', 'This parameter is already set') - context = self.get_context_data(**kwargs) - if not form.is_valid(): - context['new_param_form'] = form - return self.render_to_response(context) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - if 'submit-edit-component' in request.POST: - return self.handle_submit_edit_component_post(request, **kwargs) - elif 'submit-component-delete' in request.POST: - return self.handle_submit_delete_post(request, **kwargs) - elif 'submit-create-new-distri-num' in request.POST: - return self.handle_submit_new_distri_num_post(request, **kwargs) - elif 'submit-delete-distributor-num' in request.POST: - return self.handle_submit_delete_distri_num_post(request, **kwargs) - elif 'submit-delete-param' in request.POST: - return self.handle_submit_delete_param_post(request, **kwargs) - elif 'submit-create-new-param' in request.POST: - return self.handle_submit_new_param_post(request, **kwargs) - else: - return super().post(request, *args, **kwargs) - -class PackageDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): - template_name = 'parts/packages-detail.html' - model = Package - pk_url_kwarg = 'uuid' - base_title = '' - navbar_selected = 'Packages' - - def get_context_data(self, **kwargs): - self.base_title = 'Package / '+self.object.name - context = super().get_context_data(**kwargs) - context['package'] = self.object - context['edit_form'] = PackageForm(instance=self.object) - - return context - - def handle_delete_package(self, request): - delete_error = None - protected_objects = None - - # Try to delete this instance - try: - self.object.delete() - except ProtectedError as pe: - delete_error = 'Cannot delete this package. It is referenced by a component.' - protected_objects = pe.protected_objects - except: - delete_error = 'Cannot delete this package. Unknown error' - - if delete_error: - context = self.get_context_data() - context['delete_error'] = delete_error - context['protected_components'] = protected_objects - return self.render_to_response(context) - else: - return redirect('parts-packages') - - def edit_package(self, request): - edit_form = PackageForm(data=request.POST, files=request.FILES, instance=self.object) - - if edit_form.is_valid(): - edit_form.save() - - context = self.get_context_data() - if not edit_form.is_valid(): - context['edit_form'] = edit_form - return self.render_to_response(context) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - - if 'submit-pkg-delete' in request.POST: - return self.handle_delete_package(request) - elif 'submit-pkg-edit' in request.POST: - return self.edit_package(request) - - return super().post(request, *args, **kwargs) - -class DistributorDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): - template_name = 'parts/distributors-detail.html' - model = Distributor - pk_url_kwarg = 'uuid' - base_title = '' - navbar_selected = 'Distributors' - - def get_context_data(self, **kwargs): - self.base_title = 'Distributor / '+self.object.name - context = super().get_context_data(**kwargs) - context['distributor'] = self.object - context['edit_form'] = DistributorForm(instance=self.object) - - return context - - def handle_delete_distributor(self, request): - delete_error = None - protected_objects = None - - # Try to delete this instance - try: - self.object.delete() - except ProtectedError as pe: - delete_error = 'Cannot delete this distributor. It is referenced by a component.' - protected_objects = pe.protected_objects - except: - delete_error = 'Cannot delete this distributor. Unknown error' - - if delete_error: - context = self.get_context_data() - context['delete_error'] = delete_error - context['protected_components'] = protected_objects - return self.render_to_response(context) - else: - return redirect('parts-distributors') - - def edit_distributor(self, request): - edit_form = DistributorForm(data=request.POST, files=request.FILES, instance=self.object) - - if edit_form.is_valid(): - edit_form.save() - - context = self.get_context_data() - if not edit_form.is_valid(): - context['edit_form'] = edit_form - return self.render_to_response(context) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - - if 'submit-distri-delete' in request.POST: - return self.handle_delete_distributor(request) - elif 'submit-distri-edit' in request.POST: - return self.edit_distributor(request) - - return super().post(request, *args, **kwargs) - -class ManufacturersViewSet(LoginRequiredMixin, BaseTemplateMixin, TemplateView): - template_name = 'parts/manufacturers.html' - base_title = 'Manufacturers' - navbar_selected = 'Manufacturers' - default_page_size = 25 - - def search_manufacturers(self, search): - qs = Manufacturer.objects.all() - - if not search: - return qs - - search_fragements = search.strip().split() - - for search in search_fragements: - qs = qs.filter(Q(name__icontains = search) | Q(website__icontains = search)) - - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - page_num = self.request.GET.get('page', default=1) - search_string = self.request.GET.get('search', default=None) - - queryset = self.search_manufacturers(search_string) - - paginator = Paginator(queryset, self.default_page_size) - - - context['search_string'] = search_string - context['manufacturers'] = paginator.get_page(page_num) - context['new_manufacturer_form'] = ManufacturerForm() - - return context - - def handle_add_new_manufacturer(self, request): - - form = ManufacturerForm(data=request.POST, files=request.FILES) - if form.is_valid(): - form.save() - - context = self.get_context_data() - - if not form.is_valid(): - context['new_manufacturer_form'] = form - - return self.render_to_response(context) - - def post(self, request, *args, **kwargs): - if 'submit-manufacturer-add-new' in request.POST: - return self.handle_add_new_manufacturer(request) - - return super().post(request, *args, **kwargs) - -class ManufacturerDetailViewSet(LoginRequiredMixin, BaseTemplateMixin, DetailView): - template_name = 'parts/manufacturers-detail.html' - model = Manufacturer - pk_url_kwarg = 'uuid' - base_title = '' - navbar_selected = 'Manufacturers' - - def get_context_data(self, **kwargs): - self.base_title = 'Manufacturer / '+self.object.name - context = super().get_context_data(**kwargs) - context['manufacturer'] = self.object - context['edit_form'] = ManufacturerForm(instance=self.object) - - return context - - def handle_delete_manufacturer(self, request): - delete_error = None - protected_objects = None - - # Try to delete this instance - try: - self.object.delete() - except ProtectedError as pe: - delete_error = 'Cannot delete this distributor. It is referenced by a component.' - protected_objects = pe.protected_objects - except: - delete_error = 'Cannot delete this distributor. Unknown error' - - if delete_error: - context = self.get_context_data() - context['delete_error'] = delete_error - context['protected_components'] = protected_objects - return self.render_to_response(context) - else: - return redirect('parts-manufacturers') - - def edit_manufacturer(self, request): - edit_form = ManufacturerForm(data=request.POST, files=request.FILES, instance=self.object) - - if edit_form.is_valid(): - edit_form.save() - - context = self.get_context_data() - if not edit_form.is_valid(): - context['edit_form'] = edit_form - return self.render_to_response(context) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - - if 'submit-manufacturer-delete' in request.POST: - return self.handle_delete_manufacturer(request) - elif 'submit-manufacturer-edit' in request.POST: - return self.edit_manufacturer(request) - - return super().post(request, *args, **kwargs) - - -def health_check_view(request) -> HttpResponse: - """ - Health checking view. Returns empty http response with HTTP status OK. - This will be used to check if the system is actually running correctly - """ - return HttpResponse(status=200) \ No newline at end of file diff --git a/shimatta_kenkyusho/parts/views/__init__.py b/shimatta_kenkyusho/parts/views/__init__.py new file mode 100644 index 0000000..a5f06b7 --- /dev/null +++ b/shimatta_kenkyusho/parts/views/__init__.py @@ -0,0 +1,6 @@ +from .component_views import * +from .distributor_views import * +from .generic_views import * +from .manufacturer_views import * +from .package_views import * +from .storage_views import * \ No newline at end of file diff --git a/shimatta_kenkyusho/parts/component_import.py b/shimatta_kenkyusho/parts/views/component_import.py similarity index 97% rename from shimatta_kenkyusho/parts/component_import.py rename to shimatta_kenkyusho/parts/views/component_import.py index 39b262f..189b9e6 100644 --- a/shimatta_kenkyusho/parts/component_import.py +++ b/shimatta_kenkyusho/parts/views/component_import.py @@ -3,7 +3,7 @@ import csv import requests from django.db import transaction from django.core.files.images import ImageFile -from.models import ComponentParameter, ComponentType, Manufacturer, Component, \ +from ..models import ComponentParameter, ComponentType, Manufacturer, Component, \ DistributorNum, Stock, Storage, Distributor, Package, ComponentParameterType def _stock_component(component, storage_uuid, substorage_path, amount, lot, watermark): @@ -105,6 +105,6 @@ def import_components_from_csv(csv_file): data.pop('amount'), data.pop('lot'), data.pop('watermark')) - + for key, value in data.items(): _set_additional_parameters(comp, key, value) diff --git a/shimatta_kenkyusho/parts/views/component_views.py b/shimatta_kenkyusho/parts/views/component_views.py new file mode 100644 index 0000000..76eed61 --- /dev/null +++ b/shimatta_kenkyusho/parts/views/component_views.py @@ -0,0 +1,272 @@ +import uuid +from django.shortcuts import redirect +from django.urls import reverse +from django.views.generic import TemplateView, DetailView +from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.paginator import Paginator +from django.db.models import Q +from django.forms import formset_factory +from django.db import IntegrityError +from django.db.models import ProtectedError +from ..models import Stock, Component, ComponentParameter, DistributorNum +from ..forms import * +from .component_import import import_components_from_csv +from .generic_views import BaseTemplateMixin + + +ParameterSearchFormSet = formset_factory(ComponentParameterSearchForm, extra=1) + +# Create your views here. + +class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): + template_name = 'parts/components.html' + base_title = 'Components' + navbar_selected = 'Components' + default_page_size = 25 + + def get_component_query_set(self, search_string): + queryset = Component.objects.all() + + if not search_string: + return queryset + + 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)) + + return queryset + + def get_component_queryset_from_advanced_search(self, cleaned_data): + queryset = Component.objects.all() + + if cleaned_data['name']: + queryset = queryset.filter(Q(name__icontains=cleaned_data['name'])) + if cleaned_data['package']: + queryset = queryset.filter(package=cleaned_data['package']) + if cleaned_data['package_pin_count']: + queryset = queryset.filter(package__pin_count=cleaned_data['package_pin_count']) + if cleaned_data['component_type']: + queryset = queryset.filter(component_type=cleaned_data['component_type']) + if cleaned_data['distributor_num']: + if cleaned_data['distributor']: + distri = cleaned_data['distributor'] + queryset = queryset.filter(Q(distributornum__distributor_part_number__icontains=cleaned_data['distributor_num']) & Q(distributornum__distributor=distri)) + else: + queryset = queryset.filter(Q(distributornum__distributor_part_number__icontains=cleaned_data['distributor_num'])) + if cleaned_data['manufacturer']: + queryset = queryset.filter(manufacturer=cleaned_data['manufacturer']) + return queryset + + def get_context_data_int(self, advanced_search, parameter_formset : ParameterSearchFormSet, **kwargs): + context = super().get_context_data(**kwargs) + + comp_page_num = self.request.GET.get('comp_page', default=1) + + if advanced_search and parameter_formset: + search = None + context['advanced_search_shown'] = True + context['advanced_search_form'] = advanced_search + context['advanced_search_param_formset'] = parameter_formset + if advanced_search.is_valid(): + paginator_queryset = self.get_component_queryset_from_advanced_search( + advanced_search.cleaned_data) + else: + paginator_queryset = Component.objects.all() + + if parameter_formset.is_valid(): + # Process parameters + pass + + else: + search = self.request.GET.get('search', default=None) + paginator_queryset = self.get_component_query_set(search) + + comp_paginator = Paginator(paginator_queryset, self.default_page_size) + + context['components'] = comp_paginator.get_page(comp_page_num) + context['comp_form'] = ComponentForm() + context['import_comp_form'] = ImportComponentForm() + context['search_string'] = search + + if not parameter_formset: + context['advanced_search_param_formset'] = ParameterSearchFormSet() + + + if not advanced_search: + context['advanced_search_form'] = AdvancedComponentSearchForm(auto_id='adv_search_%s') + + return context + + def get_context_data(self, **kwargs): + return self.get_context_data_int(advanced_search = None, parameter_formset=None, **kwargs) + + def handle_new_component_post(self, request, open=False, **kwargs): + cform = ComponentForm(data=request.POST, files=request.FILES) + new_component = None + if cform.is_valid(): + new_component = cform.save() + + context = self.get_context_data(**kwargs) + if not cform.is_valid(): + context['comp_form'] = cform + + if open and new_component: + return redirect(reverse('parts-components-detail', kwargs={'uuid':new_component.id})) + return self.render_to_response(context) + + def handle_import_components_post(self, request, open=False, **kwargs): + cform = ImportComponentForm(data=request.POST, files=request.FILES) + context = self.get_context_data(**kwargs) + if cform.is_valid(): + try: + import_components_from_csv(cform.files['csv_file'].file) + except Exception as ex: + cform.add_error('csv_file', str(ex)) + context['import_comp_form'] = cform + else: + context['import_comp_form'] = cform + + return self.render_to_response(context) + + def handle_advanced_search_post(self, request, **kwargs): + + form = AdvancedComponentSearchForm(auto_id='adv_search_%s', data=request.POST) + param_formset = ParameterSearchFormSet(data=request.POST) + + if form.is_valid(): + print('Valid') + + if param_formset.is_valid(): + print('Formset is valid!') + + + context = self.get_context_data_int(form, param_formset, **kwargs) + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + if 'submit-edit-component' in request.POST: + return self.handle_new_component_post(request, open=False, **kwargs) + elif 'submit-edit-component-open' in request.POST: + return self.handle_new_component_post(request, open=True, **kwargs) + elif 'submit-import-components' in request.POST: + return self.handle_import_components_post(request, open=True, **kwargs) + elif 'submit-advanced-search' in request.POST: + return self.handle_advanced_search_post(request, **kwargs) + else: + return super().post(request, *args, **kwargs) + +class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): + template_name = 'parts/components-detail.html' + model = Component + pk_url_kwarg = 'uuid' + base_title = '' + navbar_selected = 'Components' + + def get_context_data(self, **kwargs): + self.base_title = 'Component / '+self.object.name + context = super().get_context_data(**kwargs) + context['component'] = self.object + context['stocks'] = Stock.objects.filter(component=self.object) + context['comp_form'] = ComponentForm(instance=self.object) + context['new_distri_num_form'] = DistributorNumberCreateForm() + context['new_param_form'] = ComponentParameterCreateForm() + context['distri_nums'] = DistributorNum.objects.filter(component=self.object).order_by( + 'distributor__name') + context['parameters'] = ComponentParameter.objects.filter(component=self.object).order_by( + 'parameter_type__parameter_name') + + return context + + def handle_submit_edit_component_post(self, request, **kwargs): + cform = ComponentForm(instance=self.object, data=request.POST, files=request.FILES) + if cform.is_valid(): + cform.save() + + context = self.get_context_data(**kwargs) + if not cform.is_valid(): + context['comp_form'] = cform + + return self.render_to_response(context) + + def handle_submit_delete_post(self, request, **kwargs): + delete_error = None + protected_stuff = None + + try: + self.object.delete() + except ProtectedError as pe: + delete_error = 'Component is protected' + protected_stuff = pe.protected_objects + except: + delete_error = 'Cannot delete component. Unknown error' + + if delete_error is None: + return redirect('parts-components') + else: + context = self.get_context_data(**kwargs) + context['delete_error'] = delete_error + context['protected_stuff'] = protected_stuff + return self.render_to_response(context) + + def handle_submit_new_distri_num_post(self, request, **kwargs): + form = DistributorNumberCreateForm(data=request.POST) + if form.is_valid(): + new_number = form.save(commit=False) + new_number.component = self.object + try: + new_number.save() + except IntegrityError as ie: + form.add_error('__all__', 'Number for given distributor already exists') + + context = self.get_context_data(**kwargs) + if not form.is_valid(): + context['new_distri_num_form'] = form + return self.render_to_response(context) + + def handle_submit_delete_distri_num_post(self, request, **kwargs): + form = DistributorNumberDeleteForm(data=request.POST) + if form.is_valid(): + form.cleaned_data['distributor_num'].delete() + + context = self.get_context_data(**kwargs) + return self.render_to_response(context) + + def handle_submit_delete_param_post(self, request, **kwargs): + form = ComponentParameterDeleteForm(data=request.POST) + if form.is_valid(): + form.cleaned_data['param_num'].delete() + + context = self.get_context_data(**kwargs) + return self.render_to_response(context) + + def handle_submit_new_param_post(self, request, **kwargs): + form = ComponentParameterCreateForm(data=request.POST) + + if form.is_valid(): + try: + form.save(self.object) + except IntegrityError: + form.add_error('__all__', 'This parameter is already set') + context = self.get_context_data(**kwargs) + if not form.is_valid(): + context['new_param_form'] = form + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + if 'submit-edit-component' in request.POST: + return self.handle_submit_edit_component_post(request, **kwargs) + elif 'submit-component-delete' in request.POST: + return self.handle_submit_delete_post(request, **kwargs) + elif 'submit-create-new-distri-num' in request.POST: + return self.handle_submit_new_distri_num_post(request, **kwargs) + elif 'submit-delete-distributor-num' in request.POST: + return self.handle_submit_delete_distri_num_post(request, **kwargs) + elif 'submit-delete-param' in request.POST: + return self.handle_submit_delete_param_post(request, **kwargs) + elif 'submit-create-new-param' in request.POST: + return self.handle_submit_new_param_post(request, **kwargs) + else: + return super().post(request, *args, **kwargs) + + diff --git a/shimatta_kenkyusho/parts/views/distributor_views.py b/shimatta_kenkyusho/parts/views/distributor_views.py new file mode 100644 index 0000000..5e56ad0 --- /dev/null +++ b/shimatta_kenkyusho/parts/views/distributor_views.py @@ -0,0 +1,120 @@ +from django.shortcuts import redirect +from django.views.generic import TemplateView, DetailView +from django.contrib.auth.mixins import LoginRequiredMixin +from ..models import Distributor +from django.core.paginator import Paginator +from django.db.models import ProtectedError +from ..forms import * +from django.db.models import Q +from .generic_views import BaseTemplateMixin + +class DistributorView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): + template_name = 'parts/distributors.html' + base_title = 'Distributors' + navbar_selected = 'Distributors' + default_page_size = 25 + + def search_distributors(self, search): + qs = Distributor.objects.all() + + if not search: + return qs + + search_fragments = search.strip().split() + + for search in search_fragments: + qs = qs.filter(Q(name__icontains = search) | Q(website__icontains = search)) + return qs + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + page_num = self.request.GET.get('page', default=1) + search_string = self.request.GET.get('search', default=None) + + queryset = self.search_distributors(search_string) + + paginator = Paginator(queryset, self.default_page_size) + + + context['search_string'] = search_string + context['distributors'] = paginator.get_page(page_num) + context['new_distri_form'] = DistributorForm() + + return context + + def handle_add_new_distributor(self, request): + + form = DistributorForm(data=request.POST, files=request.FILES) + if form.is_valid(): + form.save() + + context = self.get_context_data() + + if not form.is_valid(): + context['new_distri_form'] = form + + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + if 'submit-distri-add-new' in request.POST: + return self.handle_add_new_distributor(request) + + return super().post(request, *args, **kwargs) + + +class DistributorDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): + template_name = 'parts/distributors-detail.html' + model = Distributor + pk_url_kwarg = 'uuid' + base_title = '' + navbar_selected = 'Distributors' + + def get_context_data(self, **kwargs): + self.base_title = 'Distributor / '+self.object.name + context = super().get_context_data(**kwargs) + context['distributor'] = self.object + context['edit_form'] = DistributorForm(instance=self.object) + + return context + + def handle_delete_distributor(self, request): + delete_error = None + protected_objects = None + + # Try to delete this instance + try: + self.object.delete() + except ProtectedError as pe: + delete_error = 'Cannot delete this distributor. It is referenced by a component.' + protected_objects = pe.protected_objects + except: + delete_error = 'Cannot delete this distributor. Unknown error' + + if delete_error: + context = self.get_context_data() + context['delete_error'] = delete_error + context['protected_components'] = protected_objects + return self.render_to_response(context) + else: + return redirect('parts-distributors') + + def edit_distributor(self, request): + edit_form = DistributorForm(data=request.POST, files=request.FILES, instance=self.object) + + if edit_form.is_valid(): + edit_form.save() + + context = self.get_context_data() + if not edit_form.is_valid(): + context['edit_form'] = edit_form + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + + if 'submit-distri-delete' in request.POST: + return self.handle_delete_distributor(request) + elif 'submit-distri-edit' in request.POST: + return self.edit_distributor(request) + + return super().post(request, *args, **kwargs) diff --git a/shimatta_kenkyusho/parts/views/generic_views.py b/shimatta_kenkyusho/parts/views/generic_views.py new file mode 100644 index 0000000..22d7d9c --- /dev/null +++ b/shimatta_kenkyusho/parts/views/generic_views.py @@ -0,0 +1,121 @@ +from django.shortcuts import render, redirect +from django.contrib.auth import logout, login +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.generic import TemplateView +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpResponse +from ..navbar import NavBar +from ..forms import QrSearchForm + +class BaseTemplateMixin(): + 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 ChangePasswordView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): + template_name = 'parts/change-pw.html' + navbar_selected = 'Main' + base_title = 'Change Password' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['form'] = PasswordChangeForm(self.request.user) + return context + + def post(self, request, *args, **kwargs): + if 'submit-change-pw' not in request.POST: + return super().post(request, *args, **kwargs) + + form = PasswordChangeForm(request.user, data=request.POST) + + if form.is_valid(): + user = form.save() + update_session_auth_hash(request, user) + return redirect('parts-main') + + context = self.get_context_data(**kwargs) + if form.errors: + context['form'] = form + + return self.render_to_response(context) + + +class MainView(BaseTemplateMixin, TemplateView): + template_name = 'parts/main.html' + navbar_selected = 'Main' + base_title = 'Main' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['user'] = self.request.user + return context + + +def logout_view(request): + logout(request) + return redirect('parts-main') + +def login_view(request): + base_context = { + '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) + 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) + return redirect('parts-main') + else: + form = AuthForm() + + + context = { + 'base': base_context, + 'form': form, + } + + return render(request, 'parts/login.html', context) + + + +def health_check_view(_request) -> HttpResponse: + """ + Health checking view. Returns empty http response with HTTP status OK. + This will be used to check if the system is actually running correctly + """ + return HttpResponse(status=200) diff --git a/shimatta_kenkyusho/parts/views/manufacturer_views.py b/shimatta_kenkyusho/parts/views/manufacturer_views.py new file mode 100644 index 0000000..d647d77 --- /dev/null +++ b/shimatta_kenkyusho/parts/views/manufacturer_views.py @@ -0,0 +1,121 @@ +from django.shortcuts import redirect +from django.views.generic import TemplateView, DetailView +from django.contrib.auth.mixins import LoginRequiredMixin +from ..models import Manufacturer +from django.core.paginator import Paginator +from django.db.models import ProtectedError +from ..forms import * +from django.db.models import Q +from .generic_views import BaseTemplateMixin + +class ManufacturersView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): + template_name = 'parts/manufacturers.html' + base_title = 'Manufacturers' + navbar_selected = 'Manufacturers' + default_page_size = 25 + + def search_manufacturers(self, search): + qs = Manufacturer.objects.all() + + if not search: + return qs + + search_fragements = search.strip().split() + + for search in search_fragements: + qs = qs.filter(Q(name__icontains = search) | Q(website__icontains = search)) + + return qs + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + page_num = self.request.GET.get('page', default=1) + search_string = self.request.GET.get('search', default=None) + + queryset = self.search_manufacturers(search_string) + + paginator = Paginator(queryset, self.default_page_size) + + + context['search_string'] = search_string + context['manufacturers'] = paginator.get_page(page_num) + context['new_manufacturer_form'] = ManufacturerForm() + + return context + + def handle_add_new_manufacturer(self, request): + + form = ManufacturerForm(data=request.POST, files=request.FILES) + if form.is_valid(): + form.save() + + context = self.get_context_data() + + if not form.is_valid(): + context['new_manufacturer_form'] = form + + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + if 'submit-manufacturer-add-new' in request.POST: + return self.handle_add_new_manufacturer(request) + + return super().post(request, *args, **kwargs) + + +class ManufacturerDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): + template_name = 'parts/manufacturers-detail.html' + model = Manufacturer + pk_url_kwarg = 'uuid' + base_title = '' + navbar_selected = 'Manufacturers' + + def get_context_data(self, **kwargs): + self.base_title = 'Manufacturer / '+self.object.name + context = super().get_context_data(**kwargs) + context['manufacturer'] = self.object + context['edit_form'] = ManufacturerForm(instance=self.object) + + return context + + def handle_delete_manufacturer(self, request): + delete_error = None + protected_objects = None + + # Try to delete this instance + try: + self.object.delete() + except ProtectedError as pe: + delete_error = 'Cannot delete this distributor. It is referenced by a component.' + protected_objects = pe.protected_objects + except: + delete_error = 'Cannot delete this distributor. Unknown error' + + if delete_error: + context = self.get_context_data() + context['delete_error'] = delete_error + context['protected_components'] = protected_objects + return self.render_to_response(context) + else: + return redirect('parts-manufacturers') + + def edit_manufacturer(self, request): + edit_form = ManufacturerForm(data=request.POST, files=request.FILES, instance=self.object) + + if edit_form.is_valid(): + edit_form.save() + + context = self.get_context_data() + if not edit_form.is_valid(): + context['edit_form'] = edit_form + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + + if 'submit-manufacturer-delete' in request.POST: + return self.handle_delete_manufacturer(request) + elif 'submit-manufacturer-edit' in request.POST: + return self.edit_manufacturer(request) + + return super().post(request, *args, **kwargs) \ No newline at end of file diff --git a/shimatta_kenkyusho/parts/views/package_views.py b/shimatta_kenkyusho/parts/views/package_views.py new file mode 100644 index 0000000..37ad2e7 --- /dev/null +++ b/shimatta_kenkyusho/parts/views/package_views.py @@ -0,0 +1,128 @@ +from django.shortcuts import redirect +from django.views.generic import TemplateView, DetailView +from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.paginator import Paginator +from django.db.models import ProtectedError +from django.db.models import Q +from ..forms import * +from ..models import Package +from .generic_views import BaseTemplateMixin + +class PackageView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): + template_name = 'parts/packages.html' + base_title = 'Packages' + navbar_selected = 'Packages' + default_page_size = 25 + + def search_packages(self, search): + qs = Package.objects.all() + + if not search: + return qs + + search_fragments = search.strip().split() + for search in search_fragments: + if search.lower() == 'smd': + s_filter = Q(name__icontains = search) | Q(smd = True) + else: + try: + pin_count = int(search) + s_filter = Q(name__icontains = search) | Q(pin_count=pin_count) + except: + s_filter = Q(name__icontains = search) + + qs = qs.filter(s_filter) + return qs + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + page_num = self.request.GET.get('page', default=1) + search_string = self.request.GET.get('search', default=None) + + package_queryset = self.search_packages(search_string) + + paginator = Paginator(package_queryset, self.default_page_size) + + + context['search_string'] = search_string + context['packages'] = paginator.get_page(page_num) + context['new_pkg_form'] = PackageForm() + + return context + + def handle_add_new_package(self, request): + + form = PackageForm(data=request.POST, files=request.FILES) + if form.is_valid(): + form.save() + + context = self.get_context_data() + + if not form.is_valid(): + context['new_pkg_form'] = form + + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + if 'submit-pkg-add-new' in request.POST: + return self.handle_add_new_package(request) + + return super().post(request, *args, **kwargs) + + +class PackageDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): + template_name = 'parts/packages-detail.html' + model = Package + pk_url_kwarg = 'uuid' + base_title = '' + navbar_selected = 'Packages' + + def get_context_data(self, **kwargs): + self.base_title = 'Package / '+self.object.name + context = super().get_context_data(**kwargs) + context['package'] = self.object + context['edit_form'] = PackageForm(instance=self.object) + + return context + + def handle_delete_package(self, request): + delete_error = None + protected_objects = None + + # Try to delete this instance + try: + self.object.delete() + except ProtectedError as pe: + delete_error = 'Cannot delete this package. It is referenced by a component.' + protected_objects = pe.protected_objects + except: + delete_error = 'Cannot delete this package. Unknown error' + + if delete_error: + context = self.get_context_data() + context['delete_error'] = delete_error + context['protected_components'] = protected_objects + return self.render_to_response(context) + else: + return redirect('parts-packages') + + def edit_package(self, request): + edit_form = PackageForm(data=request.POST, files=request.FILES, instance=self.object) + + if edit_form.is_valid(): + edit_form.save() + + context = self.get_context_data() + if not edit_form.is_valid(): + context['edit_form'] = edit_form + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + + if 'submit-pkg-delete' in request.POST: + return self.handle_delete_package(request) + elif 'submit-pkg-edit' in request.POST: + return self.edit_package(request) + + return super().post(request, *args, **kwargs) \ No newline at end of file diff --git a/shimatta_kenkyusho/parts/views/storage_views.py b/shimatta_kenkyusho/parts/views/storage_views.py new file mode 100644 index 0000000..1e1e402 --- /dev/null +++ b/shimatta_kenkyusho/parts/views/storage_views.py @@ -0,0 +1,254 @@ +import uuid +from django.shortcuts import redirect +from django.urls import reverse +from django.views.generic import TemplateView, DetailView +from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.paginator import Paginator +from django.core.exceptions import ValidationError +from django.db.models import Q +from django.db.models.functions import Lower +from ..models import Storage, Stock +from ..forms import * +from .generic_views import BaseTemplateMixin + +class StockView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): + template_name = 'parts/stocks.html' + base_title = 'Stocks' + navbar_selected = 'Stocks' + default_pagination_size = 25 + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + storage_page = self.request.GET.get('storage_page') + if storage_page is None: + storage_page = 1 + low_stock_page = self.request.GET.get('low_stock_page') + if low_stock_page is None: + low_stock_page = 1 + + storage_paginator = Paginator(Storage.objects.filter(parent_storage=None), self.default_pagination_size) + low_stock_paginator = Paginator(Stock.get_under_watermark(), + self.default_pagination_size) + + 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.id + context['add_storage_form'] = add_stor_form + return context + + def handle_add_storage(self, request, **kwargs): + f = AddSubStorageForm(data=request.POST) + if f.is_valid(): + 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) + 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 + pk_url_kwarg = 'uuid' + base_title = '' + navbar_selected = 'Stocks' + default_pagination_size = 8 + + 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 search_stock_queryset(self, search): + stocks_in_storage = Stock.objects.filter(storage=self.object).order_by(Lower('component__name')) + + if search is None or search == '': + return stocks_in_storage + + if search.startswith('[comp_uuid]'): + search = search.replace('[comp_uuid]', '') + + + # Check if the searhc equals a UUID + test_uuid = None + try: + test_uuid = uuid.UUID(search) + except: + pass + + if test_uuid is not None: + stocks_in_storage = stocks_in_storage.filter(Q(component__id = test_uuid) | Q(id= test_uuid)) + else: + stocks_in_storage = stocks_in_storage.filter(Q(component__name__icontains = search) | + Q(component__package__name__icontains = search) | + Q(component__manufacturer__name__icontains = search)) + + return stocks_in_storage + + def get_context_data(self, **kwargs): + self.base_title = 'Stocks / ' + self.object.name + context = super().get_context_data(**kwargs) + context['breadcrumbs'] = self.get_breadcrumbs() + + storage_page = self.request.GET.get('storage_page', default=1) + storage_paginator = Paginator(Storage.objects.filter(parent_storage=self.object), self.default_pagination_size) + stock_search_input = self.request.GET.get('search') + + componente_stock_page = self.request.GET.get('stock_page', default=1) + + stock_paginator = Paginator(self.search_stock_queryset(stock_search_input), self.default_pagination_size) + + context['storages'] = storage_paginator.get_page(storage_page) + stocks = stock_paginator.get_page(componente_stock_page) + context['stocks'] = stocks + context['stock_search'] = stock_search_input + add_storage_form = AddSubStorageForm() + 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['verbose_name'].initial = self.object.verbose_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() + + return context + + def handle_add_storage_post(self, request, **kwargs): + f = AddSubStorageForm(data=request.POST) + if f.is_valid(): + sub_name = f.cleaned_data['storage_name'] + try: + Storage.objects.create(name=sub_name, + verbose_name=f.cleaned_data.get('verbose_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(): + try: + self.object.name = f.cleaned_data['storage_name'] + self.object.verbose_name = f.cleaned_data.get('verbose_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: + self.object.delete() + except: + context = self.get_context_data(**kwargs) + context['delete_storage_errors'] = ['Error deleting Storage '+str(self.object)] + return self.render_to_response(context) + + if parent is None: + 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 # TODO: Check error handling. This is clearly not working as intended :P + if 'stock_uuid' in request.POST: + f = DeleteStockForm(data=request.POST) + if f.is_valid(): + try: + s = Stock.objects.get(id=f.cleaned_data['stock_uuid']) + print(s.storage) + print(self.object) + if s.storage == self.object: + s.delete() + else: + del_error = 'Cannot delete stock from another storage.' + except: + del_error = 'Could not find requested stock in this storage.' + + context = self.get_context_data(**kwargs) + return self.render_to_response(context) + + def handle_update_watermark(self, request, **kwargs): + edit_form = EditWatermarkForm(data=request.POST) + if edit_form.is_valid(): + edit_form.save() + else: + pass # Todo: Handle error + + context = self.get_context_data(**kwargs) + return self.render_to_response(context) + + def handle_amount_change_post(self, request, increase, **kwargs): + edit_form = EditStockAmountForm(data=request.POST) + if edit_form.is_valid(): + edit_form.save(increase) + + context = self.get_context_data(**kwargs) + return self.render_to_response(context) + + def handle_add_stock_post(self, request, **kwargs): + f = AddStockForm(data=request.POST) + error_occured = False + + if f.is_valid(): + try: + f.save(self.object) + except Exception as ex: + f.add_error('', str(ex)) + error_occured = True + else: + error_occured = True + + context = self.get_context_data(**kwargs) + if error_occured: + context['add_stock_form'] = f + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + + 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: + return self.handle_del_stock_post(request, **kwargs) + elif 'submit-edit-watermark' in request.POST: + return self.handle_update_watermark(request, **kwargs) + elif 'submit-amount-reduce' in request.POST: + return self.handle_amount_change_post(request, False, **kwargs) + elif 'submit-amount-increase' in request.POST: + return self.handle_amount_change_post(request, True, **kwargs) + elif 'submit-add-stock' in request.POST: + return self.handle_add_stock_post(request, **kwargs) + + return super().post(request, *args, **kwargs) \ No newline at end of file diff --git a/shimatta_kenkyusho/shimatta_kenkyusho/urls.py b/shimatta_kenkyusho/shimatta_kenkyusho/urls.py index e8ac531..c01b976 100644 --- a/shimatta_kenkyusho/shimatta_kenkyusho/urls.py +++ b/shimatta_kenkyusho/shimatta_kenkyusho/urls.py @@ -14,14 +14,13 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, include -from django.conf.urls import url +from django.urls import path, include, re_path from django.conf.urls.static import static from django.conf import settings from parts import views as parts_views urlpatterns = [ - url(r'^admin/login/', parts_views.login_view), + re_path(r'^admin/login/', parts_views.login_view), path('admin/', admin.site.urls), path('api/v1/', include('api.urls'), name='api-root'), path('', include('parts.urls')), diff --git a/shimatta_kenkyusho/static/js/autocomplete-foreign-key-field.js b/shimatta_kenkyusho/static/js/autocomplete-foreign-key-field.js index 0ff2e41..f90e0d5 100644 --- a/shimatta_kenkyusho/static/js/autocomplete-foreign-key-field.js +++ b/shimatta_kenkyusho/static/js/autocomplete-foreign-key-field.js @@ -27,6 +27,17 @@ function initialize_autocompletion_foreign_key_field(search_element) { }); } + // select first match if any on enter + search_element.addEventListener('keydown', (e) => { + if (e.key === "Enter") { + e.preventDefault(); + first_search_result = search_element.parentElement.querySelector('li') + if (first_search_result) { + first_search_result.click() + } + } + }); + new AutocompleteCustomUi( base_id, base_id+'-ac-ul', function(search_query, autocomplete_obj) { api_ajax_request_without_send('GET', search_url+`?search=${encodeURIComponent(search_query)}`, function(method, url, json) { @@ -47,7 +58,7 @@ function initialize_autocompletion_foreign_key_field(search_element) { } } - autocomplete_obj.show_results(nodes, function(data) { + autocomplete_obj.show_results(nodes, function(data) { var name = data['result'][name_field_name]; if (image_field_name != '' && image_field_name != null) { var image = data['result'][image_field_name]; @@ -72,7 +83,7 @@ function initialize_autocompletion_foreign_key_field(search_element) { }); dflex_container.appendChild(span); - }) + }) }, function (){}); } ) diff --git a/shimatta_kenkyusho/static/js/autocomplete.js b/shimatta_kenkyusho/static/js/autocomplete.js index b99c0d0..c99be62 100644 --- a/shimatta_kenkyusho/static/js/autocomplete.js +++ b/shimatta_kenkyusho/static/js/autocomplete.js @@ -38,7 +38,9 @@ class AutocompleteCustomUi { this.query_callback = query_function.bind(this); document.getElementById(text_id).addEventListener("keyup", this.ac_delay(function(event) { - this.query_callback(document.getElementById(this.text_id).value, this); + if (event.key != 'Enter') { + this.query_callback(document.getElementById(this.text_id).value, this); + } }, autocomplete_query_delay_ms).bind(this)); this.dropdown_data = {}; diff --git a/shimatta_kenkyusho/templates/base.html b/shimatta_kenkyusho/templates/base.html index f7daf6b..dd94693 100644 --- a/shimatta_kenkyusho/templates/base.html +++ b/shimatta_kenkyusho/templates/base.html @@ -83,6 +83,14 @@ const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]') const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl)) + + {% block custom_scripts %} {% endblock custom_scripts %}