mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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:
parent
fe8f111a63
commit
2b1d8f5b79
@ -3,15 +3,12 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from mptt.fields import TreeNodeChoiceField
|
||||
|
||||
from common.forms import MatchItemForm
|
||||
from InvenTree.fields import RoundingDecimalFormField
|
||||
from InvenTree.forms import HelperForm
|
||||
from InvenTree.helpers import clean_decimal
|
||||
|
||||
from .models import (Part, PartCategory, PartInternalPriceBreak,
|
||||
PartSellPriceBreak)
|
||||
from .models import Part, PartInternalPriceBreak, PartSellPriceBreak
|
||||
|
||||
|
||||
class PartImageDownloadForm(HelperForm):
|
||||
@ -53,12 +50,6 @@ class BomMatchItemForm(MatchItemForm):
|
||||
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):
|
||||
"""Simple form for viewing part pricing information."""
|
||||
|
||||
|
@ -165,7 +165,7 @@
|
||||
<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">
|
||||
{% trans "Options" %}
|
||||
<span class='fas fa-tools' title='{% trans "Options" %}'></span>
|
||||
</button>
|
||||
<ul class='dropdown-menu'>
|
||||
{% if roles.part.change %}
|
||||
@ -378,7 +378,6 @@
|
||||
{% else %}category: "null",
|
||||
{% endif %}
|
||||
},
|
||||
buttons: ['#part-options'],
|
||||
checkbox: true,
|
||||
gridView: true,
|
||||
},
|
||||
|
@ -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 %}
|
@ -138,23 +138,3 @@ class PartQRTest(PartViewTestCase):
|
||||
response = self.client.get(reverse('part-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
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)
|
||||
|
@ -56,9 +56,6 @@ part_urls = [
|
||||
# Part category
|
||||
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
|
||||
re_path(r'^(?P<slug>[-\w]+)/', views.PartDetailFromIPN.as_view(), name='part-detail-from-ipn'),
|
||||
|
||||
|
@ -8,7 +8,6 @@ from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import transaction
|
||||
from django.shortcuts import HttpResponseRedirect, get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -67,80 +66,6 @@ class PartIndex(InvenTreeRoleMixin, ListView):
|
||||
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):
|
||||
"""Part: Upload file, match to fields and import parts(using multi-Step form)"""
|
||||
permission_required = 'part.add'
|
||||
|
@ -642,7 +642,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
|
||||
addRemoveCallback(opts.modal, `#button-row-remove-${response.pk}`);
|
||||
|
||||
// Re-enable the "submit" button
|
||||
$(opts.modal).find('#modal-form-submit').prop('disabled', false);
|
||||
enableSubmitButton(opts, true);
|
||||
|
||||
// Reload the parent BOM table
|
||||
reloadParentTable();
|
||||
|
@ -596,7 +596,7 @@ function constructFormBody(fields, options) {
|
||||
|
||||
// Immediately disable the "submit" button,
|
||||
// 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
|
||||
if (options.beforeSubmit) {
|
||||
@ -639,13 +639,13 @@ function insertConfirmButton(options) {
|
||||
$(options.modal).find('#modal-footer-buttons').append(html);
|
||||
|
||||
// Disable the 'submit' button
|
||||
$(options.modal).find('#modal-form-submit').prop('disabled', true);
|
||||
enableSubmitButton(options, true);
|
||||
|
||||
// Trigger event
|
||||
$(options.modal).find('#modal-confirm').change(function() {
|
||||
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
|
||||
if (options.modal) {
|
||||
$(options.modal).find('#modal-form-submit').prop('disabled', false);
|
||||
enableSubmitButton(options, true);
|
||||
}
|
||||
|
||||
// Remove any error flags from the form
|
||||
@ -1228,7 +1228,7 @@ function handleFormErrors(errors, fields={}, options={}) {
|
||||
|
||||
// Reset the status of the "submit" button
|
||||
if (options.modal) {
|
||||
$(options.modal).find('#modal-form-submit').prop('disabled', false);
|
||||
enableSubmitButton(options, true);
|
||||
}
|
||||
|
||||
// Remove any existing error messages from the form
|
||||
|
@ -11,10 +11,10 @@
|
||||
clearFieldOptions,
|
||||
closeModal,
|
||||
enableField,
|
||||
enableSubmitButton,
|
||||
getFieldValue,
|
||||
reloadFieldOptions,
|
||||
showModalImage,
|
||||
removeRowFromModalForm,
|
||||
showQuestionDialog,
|
||||
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) {
|
||||
/* 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) {
|
||||
|
||||
var html = '<b>' + xhr.statusText + '</b><br>';
|
||||
|
@ -8,7 +8,6 @@
|
||||
imageHoverIcon,
|
||||
inventreeGet,
|
||||
inventreePut,
|
||||
launchModalForm,
|
||||
linkButtonsToSelection,
|
||||
loadTableFilters,
|
||||
makeIconBadge,
|
||||
@ -1604,6 +1603,7 @@ function loadPartTable(table, url, options={}) {
|
||||
|
||||
/* Button callbacks for part table buttons */
|
||||
|
||||
// Callback function for the "order parts" button
|
||||
$('#multi-part-order').click(function() {
|
||||
var selections = getTableData(table);
|
||||
|
||||
@ -1613,31 +1613,82 @@ function loadPartTable(table, url, options={}) {
|
||||
parts.push(part);
|
||||
});
|
||||
|
||||
orderParts(
|
||||
parts,
|
||||
{
|
||||
|
||||
}
|
||||
);
|
||||
orderParts(parts, {});
|
||||
});
|
||||
|
||||
// Callback function for the "set category" button
|
||||
$('#multi-part-category').click(function() {
|
||||
var selections = $(table).bootstrapTable('getSelections');
|
||||
|
||||
var selections = getTableData(table);
|
||||
var parts = [];
|
||||
|
||||
selections.forEach(function(item) {
|
||||
parts.push(item.pk);
|
||||
});
|
||||
|
||||
launchModalForm('/part/set-category/', {
|
||||
data: {
|
||||
parts: parts,
|
||||
var html = `
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% 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() {
|
||||
var selections = getTableData(table);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user