2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 12:06:44 +00:00

Set part category (#3134)

* Refactor function to enable / disable submit button on modal forms

* Category selection now just uses the AP

* Remove unused forms / views

* JS linting fixes

* remove outdated unit test
This commit is contained in:
Oliver 2022-06-06 13:00:30 +10:00 committed by GitHub
parent fe8f111a63
commit 2b1d8f5b79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 91 additions and 185 deletions

View File

@ -3,15 +3,12 @@
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from mptt.fields import TreeNodeChoiceField
from common.forms import MatchItemForm from common.forms import MatchItemForm
from InvenTree.fields import RoundingDecimalFormField from InvenTree.fields import RoundingDecimalFormField
from InvenTree.forms import HelperForm from InvenTree.forms import HelperForm
from InvenTree.helpers import clean_decimal from InvenTree.helpers import clean_decimal
from .models import (Part, PartCategory, PartInternalPriceBreak, from .models import Part, PartInternalPriceBreak, PartSellPriceBreak
PartSellPriceBreak)
class PartImageDownloadForm(HelperForm): class PartImageDownloadForm(HelperForm):
@ -53,12 +50,6 @@ class BomMatchItemForm(MatchItemForm):
return super().get_special_field(col_guess, row, file_manager) return super().get_special_field(col_guess, row, file_manager)
class SetPartCategoryForm(forms.Form):
"""Form for setting the category of multiple Part objects."""
part_category = TreeNodeChoiceField(queryset=PartCategory.objects.all(), required=True, help_text=_('Select part category'))
class PartPriceForm(forms.Form): class PartPriceForm(forms.Form):
"""Simple form for viewing part pricing information.""" """Simple form for viewing part pricing information."""

View File

@ -165,7 +165,7 @@
<div class='btn-group' role='group'> <div class='btn-group' role='group'>
<div class='btn-group' role='group'> <div class='btn-group' role='group'>
<button id='part-options' class='btn btn-primary dropdown-toggle' type='button' data-bs-toggle="dropdown"> <button id='part-options' class='btn btn-primary dropdown-toggle' type='button' data-bs-toggle="dropdown">
{% trans "Options" %} <span class='fas fa-tools' title='{% trans "Options" %}'></span>
</button> </button>
<ul class='dropdown-menu'> <ul class='dropdown-menu'>
{% if roles.part.change %} {% if roles.part.change %}
@ -378,7 +378,6 @@
{% else %}category: "null", {% else %}category: "null",
{% endif %} {% endif %}
}, },
buttons: ['#part-options'],
checkbox: true, checkbox: true,
gridView: true, gridView: true,
}, },

View File

@ -1,43 +0,0 @@
{% extends "modal_form.html" %}
{% load i18n %}
{% block form %}
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
{% load crispy_forms_tags %}
<label class='control-label'>Parts</label>
<p class='help-block'>{% trans "Set category for the following parts" %}</p>
<table class='table table-striped'>
<tr>
<th>{% trans "Part" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Category" %}</th>
<th>
</tr>
{% for part in parts %}
<tr id='part_row_{{ part.id }}'>
<input type='hidden' name='part_id_{{ part.id }}' value='1'/>
<td>
{% include "hover_image.html" with image=part.image hover=False %}
{{ part.full_name }}
</td>
<td>
{{ part.description }}
</td>
<td>
{{ part.category.pathstring }}
</td>
<td>
<button class='btn btn-outline-secondary btn-remove' onClick='removeRowFromModalForm()' title='{% trans "Remove part" %}' type='button'>
<span row='part_row_{{ part.id }}' class='fas fa-trash-alt icon-red'></span>
</button>
</td>
</tr>
{% endfor %}
</table>
{% crispy form %}
</form>
{% endblock %}

View File

@ -138,23 +138,3 @@ class PartQRTest(PartViewTestCase):
response = self.client.get(reverse('part-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.get(reverse('part-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
class CategoryTest(PartViewTestCase):
"""Tests for PartCategory related views."""
def test_set_category(self):
"""Test that the "SetCategory" view works."""
url = reverse('part-set-category')
response = self.client.get(url, {'parts[]': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
data = {
'part_id_10': True,
'part_id_1': True,
'part_category': 5
}
response = self.client.post(url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)

View File

@ -56,9 +56,6 @@ part_urls = [
# Part category # Part category
re_path(r'^category/', include(category_urls)), re_path(r'^category/', include(category_urls)),
# Change category for multiple parts
re_path(r'^set-category/?', views.PartSetCategory.as_view(), name='part-set-category'),
# Individual part using IPN as slug # Individual part using IPN as slug
re_path(r'^(?P<slug>[-\w]+)/', views.PartDetailFromIPN.as_view(), name='part-detail-from-ipn'), re_path(r'^(?P<slug>[-\w]+)/', views.PartDetailFromIPN.as_view(), name='part-detail-from-ipn'),

View File

@ -8,7 +8,6 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import transaction
from django.shortcuts import HttpResponseRedirect, get_object_or_404 from django.shortcuts import HttpResponseRedirect, get_object_or_404
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -67,80 +66,6 @@ class PartIndex(InvenTreeRoleMixin, ListView):
return context return context
class PartSetCategory(AjaxUpdateView):
"""View for settings the part category for multiple parts at once."""
ajax_template_name = 'part/set_category.html'
ajax_form_title = _('Set Part Category')
form_class = part_forms.SetPartCategoryForm
role_required = 'part.change'
category = None
parts = []
def get(self, request, *args, **kwargs):
"""Respond to a GET request to this view."""
self.request = request
if 'parts[]' in request.GET:
self.parts = Part.objects.filter(id__in=request.GET.getlist('parts[]'))
else:
self.parts = []
return self.renderJsonResponse(request, form=self.get_form(), context=self.get_context_data())
def post(self, request, *args, **kwargs):
"""Respond to a POST request to this view."""
self.parts = []
for item in request.POST:
if item.startswith('part_id_'):
pk = item.replace('part_id_', '')
try:
part = Part.objects.get(pk=pk)
except (Part.DoesNotExist, ValueError):
continue
self.parts.append(part)
self.category = None
if 'part_category' in request.POST:
pk = request.POST['part_category']
try:
self.category = PartCategory.objects.get(pk=pk)
except (PartCategory.DoesNotExist, ValueError):
self.category = None
valid = self.category is not None
data = {
'form_valid': valid,
'success': _('Set category for {n} parts').format(n=len(self.parts))
}
if valid:
with transaction.atomic():
for part in self.parts:
part.category = self.category
part.save()
return self.renderJsonResponse(request, data=data, form=self.get_form(), context=self.get_context_data())
def get_context_data(self):
"""Return context data for rendering in the form."""
ctx = {}
ctx['parts'] = self.parts
ctx['categories'] = PartCategory.objects.all()
ctx['category'] = self.category
return ctx
class PartImport(FileManagementFormView): class PartImport(FileManagementFormView):
"""Part: Upload file, match to fields and import parts(using multi-Step form)""" """Part: Upload file, match to fields and import parts(using multi-Step form)"""
permission_required = 'part.add' permission_required = 'part.add'

View File

@ -642,7 +642,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
addRemoveCallback(opts.modal, `#button-row-remove-${response.pk}`); addRemoveCallback(opts.modal, `#button-row-remove-${response.pk}`);
// Re-enable the "submit" button // Re-enable the "submit" button
$(opts.modal).find('#modal-form-submit').prop('disabled', false); enableSubmitButton(opts, true);
// Reload the parent BOM table // Reload the parent BOM table
reloadParentTable(); reloadParentTable();

View File

@ -596,7 +596,7 @@ function constructFormBody(fields, options) {
// Immediately disable the "submit" button, // Immediately disable the "submit" button,
// to prevent the form being submitted multiple times! // to prevent the form being submitted multiple times!
$(options.modal).find('#modal-form-submit').prop('disabled', true); enableSubmitButton(options, false);
// Run custom code before normal form submission // Run custom code before normal form submission
if (options.beforeSubmit) { if (options.beforeSubmit) {
@ -639,13 +639,13 @@ function insertConfirmButton(options) {
$(options.modal).find('#modal-footer-buttons').append(html); $(options.modal).find('#modal-footer-buttons').append(html);
// Disable the 'submit' button // Disable the 'submit' button
$(options.modal).find('#modal-form-submit').prop('disabled', true); enableSubmitButton(options, true);
// Trigger event // Trigger event
$(options.modal).find('#modal-confirm').change(function() { $(options.modal).find('#modal-confirm').change(function() {
var enabled = this.checked; var enabled = this.checked;
$(options.modal).find('#modal-form-submit').prop('disabled', !enabled); enableSubmitButton(options, !enabled);
}); });
} }
@ -1063,7 +1063,7 @@ function handleFormSuccess(response, options) {
// Reset the status of the "submit" button // Reset the status of the "submit" button
if (options.modal) { if (options.modal) {
$(options.modal).find('#modal-form-submit').prop('disabled', false); enableSubmitButton(options, true);
} }
// Remove any error flags from the form // Remove any error flags from the form
@ -1228,7 +1228,7 @@ function handleFormErrors(errors, fields={}, options={}) {
// Reset the status of the "submit" button // Reset the status of the "submit" button
if (options.modal) { if (options.modal) {
$(options.modal).find('#modal-form-submit').prop('disabled', false); enableSubmitButton(options, true);
} }
// Remove any existing error messages from the form // Remove any existing error messages from the form

View File

@ -11,10 +11,10 @@
clearFieldOptions, clearFieldOptions,
closeModal, closeModal,
enableField, enableField,
enableSubmitButton,
getFieldValue, getFieldValue,
reloadFieldOptions, reloadFieldOptions,
showModalImage, showModalImage,
removeRowFromModalForm,
showQuestionDialog, showQuestionDialog,
showModalSpinner, showModalSpinner,
*/ */
@ -146,6 +146,24 @@ function createNewModal(options={}) {
} }
/*
* Convenience function to enable (or disable) the "submit" button on a modal form
*/
function enableSubmitButton(options, enable=true) {
if (!options || !options.modal) {
console.warn('enableSubmitButton() called without modal reference');
return;
}
if (enable) {
$(options.modal).find('#modal-form-submit').prop('disabled', false);
} else {
$(options.modal).find('#modal-form-submit').prop('disabled', true);
}
}
function makeOption(text, value, title) { function makeOption(text, value, title) {
/* Format an option for a select element /* Format an option for a select element
*/ */
@ -536,18 +554,6 @@ function modalSubmit(modal, callback) {
} }
function removeRowFromModalForm(e) {
/* Remove a row from a table in a modal form */
e = e || window.event;
var src = e.target || e.srcElement;
var row = $(src).attr('row');
$('#' + row).remove();
}
function renderErrorMessage(xhr) { function renderErrorMessage(xhr) {
var html = '<b>' + xhr.statusText + '</b><br>'; var html = '<b>' + xhr.statusText + '</b><br>';

View File

@ -8,7 +8,6 @@
imageHoverIcon, imageHoverIcon,
inventreeGet, inventreeGet,
inventreePut, inventreePut,
launchModalForm,
linkButtonsToSelection, linkButtonsToSelection,
loadTableFilters, loadTableFilters,
makeIconBadge, makeIconBadge,
@ -1604,6 +1603,7 @@ function loadPartTable(table, url, options={}) {
/* Button callbacks for part table buttons */ /* Button callbacks for part table buttons */
// Callback function for the "order parts" button
$('#multi-part-order').click(function() { $('#multi-part-order').click(function() {
var selections = getTableData(table); var selections = getTableData(table);
@ -1613,31 +1613,82 @@ function loadPartTable(table, url, options={}) {
parts.push(part); parts.push(part);
}); });
orderParts( orderParts(parts, {});
parts,
{
}
);
}); });
// Callback function for the "set category" button
$('#multi-part-category').click(function() { $('#multi-part-category').click(function() {
var selections = $(table).bootstrapTable('getSelections'); var selections = getTableData(table);
var parts = []; var parts = [];
selections.forEach(function(item) { selections.forEach(function(item) {
parts.push(item.pk); parts.push(item.pk);
}); });
launchModalForm('/part/set-category/', { var html = `
data: { <div class='alert alert-block alert-info'>
parts: parts, {% trans "Set the part category for the selected parts" %}
</div>
`;
constructFormBody({}, {
title: '{% trans "Set Part Category" %}',
preFormContent: html,
fields: {
category: {
label: '{% trans "Category" %}',
help_text: '{% trans "Select Part Category" %}',
required: true,
type: 'related field',
model: 'partcategory',
api_url: '{% url "api-part-category-list" %}',
}
},
onSubmit: function(fields, opts) {
var category = getFormFieldValue('category', fields['category'], opts);
if (category == null) {
handleFormErrors(
{
'category': ['{% trans "Category is required" %}']
},
opts.fields,
opts
);
return;
}
// Set the category for each part in sequence
function setCategory() {
if (parts.length > 0) {
var part = parts.shift();
inventreePut(
`/api/part/${part}/`,
{
category: category,
},
{
method: 'PATCH',
complete: setCategory,
}
);
} else {
// We are done!
$(opts.modal).modal('hide');
$(table).bootstrapTable('refresh');
}
};
// Start the ball rolling
showModalSpinner(opts.modal);
setCategory();
}, },
reload: true,
}); });
}); });
// Callback function for the "print label" button
$('#multi-part-print-label').click(function() { $('#multi-part-print-label').click(function() {
var selections = getTableData(table); var selections = getTableData(table);