mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 15:15:42 +00:00 
			
		
		
		
	Merge branch 'master' of https://github.com/inventree/inventree
This commit is contained in:
		
							
								
								
									
										2
									
								
								.github/workflows/sponsors.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/sponsors.yml
									
									
									
									
										vendored
									
									
								
							@@ -6,6 +6,8 @@ on:
 | 
			
		||||
jobs:
 | 
			
		||||
  deploy:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    if: github.repository_owner == 'Inventree'
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout Code
 | 
			
		||||
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ from rest_framework.utils import model_meta
 | 
			
		||||
import InvenTree.permissions
 | 
			
		||||
import users.models
 | 
			
		||||
from InvenTree.helpers import str2bool
 | 
			
		||||
from InvenTree.serializers import DependentField
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger('inventree')
 | 
			
		||||
 | 
			
		||||
@@ -242,6 +243,10 @@ class InvenTreeMetadata(SimpleMetadata):
 | 
			
		||||
 | 
			
		||||
        We take the regular DRF metadata and add our own unique flavor
 | 
			
		||||
        """
 | 
			
		||||
        # Try to add the child property to the dependent field to be used by the super call
 | 
			
		||||
        if self.label_lookup[field] == 'dependent field':
 | 
			
		||||
            field.get_child(raise_exception=True)
 | 
			
		||||
 | 
			
		||||
        # Run super method first
 | 
			
		||||
        field_info = super().get_field_info(field)
 | 
			
		||||
 | 
			
		||||
@@ -275,4 +280,11 @@ class InvenTreeMetadata(SimpleMetadata):
 | 
			
		||||
                else:
 | 
			
		||||
                    field_info['api_url'] = model.get_api_url()
 | 
			
		||||
 | 
			
		||||
        # Add more metadata about dependent fields
 | 
			
		||||
        if field_info['type'] == 'dependent field':
 | 
			
		||||
            field_info['depends_on'] = field.depends_on
 | 
			
		||||
 | 
			
		||||
        return field_info
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
InvenTreeMetadata.label_lookup[DependentField] = "dependent field"
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
from collections import OrderedDict
 | 
			
		||||
from copy import deepcopy
 | 
			
		||||
from decimal import Decimal
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
@@ -94,6 +95,93 @@ class InvenTreeCurrencySerializer(serializers.ChoiceField):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DependentField(serializers.Field):
 | 
			
		||||
    """A dependent field can be used to dynamically return child fields based on the value of other fields."""
 | 
			
		||||
    child = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, depends_on, field_serializer, **kwargs):
 | 
			
		||||
        """A dependent field can be used to dynamically return child fields based on the value of other fields.
 | 
			
		||||
 | 
			
		||||
        Example:
 | 
			
		||||
        This example adds two fields. If the client selects integer, an integer field will be shown, but if he
 | 
			
		||||
        selects char, an char field will be shown. For any other value, nothing will be shown.
 | 
			
		||||
 | 
			
		||||
        class TestSerializer(serializers.Serializer):
 | 
			
		||||
            select_type = serializers.ChoiceField(choices=[
 | 
			
		||||
                ("integer", "Integer"),
 | 
			
		||||
                ("char", "Char"),
 | 
			
		||||
            ])
 | 
			
		||||
            my_field = DependentField(depends_on=["select_type"], field_serializer="get_my_field")
 | 
			
		||||
 | 
			
		||||
            def get_my_field(self, fields):
 | 
			
		||||
                if fields["select_type"] == "integer":
 | 
			
		||||
                    return serializers.IntegerField()
 | 
			
		||||
                if fields["select_type"] == "char":
 | 
			
		||||
                    return serializers.CharField()
 | 
			
		||||
        """
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        self.depends_on = depends_on
 | 
			
		||||
        self.field_serializer = field_serializer
 | 
			
		||||
 | 
			
		||||
    def get_child(self, raise_exception=False):
 | 
			
		||||
        """This method tries to extract the child based on the provided data in the request by the client."""
 | 
			
		||||
        data = deepcopy(self.context["request"].data)
 | 
			
		||||
 | 
			
		||||
        def visit_parent(node):
 | 
			
		||||
            """Recursively extract the data for the parent field/serializer in reverse."""
 | 
			
		||||
            nonlocal data
 | 
			
		||||
 | 
			
		||||
            if node.parent:
 | 
			
		||||
                visit_parent(node.parent)
 | 
			
		||||
 | 
			
		||||
            # only do for composite fields and stop right before the current field
 | 
			
		||||
            if hasattr(node, "child") and node is not self and isinstance(data, dict):
 | 
			
		||||
                data = data.get(node.field_name, None)
 | 
			
		||||
        visit_parent(self)
 | 
			
		||||
 | 
			
		||||
        # ensure that data is a dictionary and that a parent exists
 | 
			
		||||
        if not isinstance(data, dict) or self.parent is None:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # check if the request data contains the dependent fields, otherwise skip getting the child
 | 
			
		||||
        for f in self.depends_on:
 | 
			
		||||
            if not data.get(f, None):
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
        # partially validate the data for options requests that set raise_exception while calling .get_child(...)
 | 
			
		||||
        if raise_exception:
 | 
			
		||||
            validation_data = {k: v for k, v in data.items() if k in self.depends_on}
 | 
			
		||||
            serializer = self.parent.__class__(context=self.context, data=validation_data, partial=True)
 | 
			
		||||
            serializer.is_valid(raise_exception=raise_exception)
 | 
			
		||||
 | 
			
		||||
        # try to get the field serializer
 | 
			
		||||
        field_serializer = getattr(self.parent, self.field_serializer)
 | 
			
		||||
        child = field_serializer(data)
 | 
			
		||||
 | 
			
		||||
        if not child:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.child = child
 | 
			
		||||
        self.child.bind(field_name='', parent=self)
 | 
			
		||||
 | 
			
		||||
    def to_internal_value(self, data):
 | 
			
		||||
        """This method tries to convert the data to an internal representation based on the defined to_internal_value method on the child."""
 | 
			
		||||
        self.get_child()
 | 
			
		||||
        if self.child:
 | 
			
		||||
            return self.child.to_internal_value(data)
 | 
			
		||||
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def to_representation(self, value):
 | 
			
		||||
        """This method tries to convert the data to representation based on the defined to_representation method on the child."""
 | 
			
		||||
        self.get_child()
 | 
			
		||||
        if self.child:
 | 
			
		||||
            return self.child.to_representation(value)
 | 
			
		||||
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvenTreeModelSerializer(serializers.ModelSerializer):
 | 
			
		||||
    """Inherits the standard Django ModelSerializer class, but also ensures that the underlying model class data are checked on validation."""
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1874,6 +1874,12 @@ class InvenTreeSetting(BaseInvenTreeSetting):
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'DISPLAY_FULL_NAMES': {
 | 
			
		||||
            'name': _('Display Users full names'),
 | 
			
		||||
            'description': _('Display Users full names instead of usernames'),
 | 
			
		||||
            'default': False,
 | 
			
		||||
            'validator': bool
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    typ = 'inventree'
 | 
			
		||||
 
 | 
			
		||||
@@ -159,7 +159,8 @@ class LabelPrintMixin(LabelFilterMixin):
 | 
			
		||||
        # Check the request to determine if the user has selected a label printing plugin
 | 
			
		||||
        plugin = self.get_plugin(self.request)
 | 
			
		||||
 | 
			
		||||
        serializer = plugin.get_printing_options_serializer(self.request)
 | 
			
		||||
        kwargs.setdefault('context', self.get_serializer_context())
 | 
			
		||||
        serializer = plugin.get_printing_options_serializer(self.request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # if no serializer is defined, return an empty serializer
 | 
			
		||||
        if not serializer:
 | 
			
		||||
@@ -226,7 +227,7 @@ class LabelPrintMixin(LabelFilterMixin):
 | 
			
		||||
            raise ValidationError('Label has invalid dimensions')
 | 
			
		||||
 | 
			
		||||
        # if the plugin returns a serializer, validate the data
 | 
			
		||||
        if serializer := plugin.get_printing_options_serializer(request, data=request.data):
 | 
			
		||||
        if serializer := plugin.get_printing_options_serializer(request, data=request.data, context=self.get_serializer_context()):
 | 
			
		||||
            serializer.is_valid(raise_exception=True)
 | 
			
		||||
 | 
			
		||||
        # At this point, we offload the label(s) to the selected plugin.
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE" icon="fa-info-circle" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE_TITLE" icon="fa-info-circle" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="INVENTREE_RESTRICT_ABOUT" icon="fa-info-circle" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="DISPLAY_FULL_NAMES" icon="fa-font" %}
 | 
			
		||||
        <tr><td colspan='5'></td></tr>
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="INVENTREE_UPDATE_CHECK_INTERVAL" icon="fa-calendar-alt" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="INVENTREE_DOWNLOAD_FROM_URL" icon="fa-cloud-download-alt" %}
 | 
			
		||||
 
 | 
			
		||||
@@ -298,7 +298,7 @@ function constructDeleteForm(fields, options) {
 | 
			
		||||
 * - closeText: Text for the "close" button
 | 
			
		||||
 * - fields: list of fields to display, with the following options
 | 
			
		||||
 *      - filters: API query filters
 | 
			
		||||
 *      - onEdit: callback when field is edited
 | 
			
		||||
 *      - onEdit: callback or array of callbacks which get fired when field is edited
 | 
			
		||||
 *      - secondary: Define a secondary modal form for this field
 | 
			
		||||
 *      - label: Specify custom label
 | 
			
		||||
 *      - help_text: Specify custom help_text
 | 
			
		||||
@@ -493,6 +493,32 @@ function constructFormBody(fields, options) {
 | 
			
		||||
        html += options.header_html;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // process every field by recursively walking down nested fields
 | 
			
		||||
    const processField = (name, field, optionsField) => {
 | 
			
		||||
        if (typeof optionsField !== "object") return;
 | 
			
		||||
 | 
			
		||||
        if (field.type === "nested object" && optionsField.children) {
 | 
			
		||||
            for (const [k, v] of Object.entries(field.children)) {
 | 
			
		||||
                processField(`${name}__${k}`, v, optionsField.children[k]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (field.type === "dependent field") {
 | 
			
		||||
            if(field.child) {
 | 
			
		||||
                // copy child attribute from parameters to options
 | 
			
		||||
                optionsField.child = field.child;
 | 
			
		||||
 | 
			
		||||
                processField(name, field.child, optionsField.child);
 | 
			
		||||
            } else {
 | 
			
		||||
                delete optionsField.child;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const [k,v] of Object.entries(fields)) {
 | 
			
		||||
        processField(k, v, options.fields[k]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Client must provide set of fields to be displayed,
 | 
			
		||||
    // otherwise *all* fields will be displayed
 | 
			
		||||
    var displayed_fields = options.fields || fields;
 | 
			
		||||
@@ -599,14 +625,6 @@ function constructFormBody(fields, options) {
 | 
			
		||||
 | 
			
		||||
        var field = fields[field_name];
 | 
			
		||||
 | 
			
		||||
        switch (field.type) {
 | 
			
		||||
        // Skip field types which are simply not supported
 | 
			
		||||
        case 'nested object':
 | 
			
		||||
            continue;
 | 
			
		||||
        default:
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        html += constructField(field_name, field, options);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -810,7 +828,7 @@ function insertSecondaryButtons(options) {
 | 
			
		||||
/*
 | 
			
		||||
 * Extract all specified form values as a single object
 | 
			
		||||
 */
 | 
			
		||||
function extractFormData(fields, options) {
 | 
			
		||||
function extractFormData(fields, options, includeLocal = true) {
 | 
			
		||||
 | 
			
		||||
    var data = {};
 | 
			
		||||
 | 
			
		||||
@@ -823,6 +841,7 @@ function extractFormData(fields, options) {
 | 
			
		||||
        if (!field) continue;
 | 
			
		||||
 | 
			
		||||
        if (field.type == 'candy') continue;
 | 
			
		||||
        if (!includeLocal && field.localOnly) continue;
 | 
			
		||||
 | 
			
		||||
        data[name] = getFormFieldValue(name, field, options);
 | 
			
		||||
    }
 | 
			
		||||
@@ -1031,6 +1050,17 @@ function updateFieldValue(name, value, field, options) {
 | 
			
		||||
        }
 | 
			
		||||
        // TODO - Specify an actual value!
 | 
			
		||||
        break;
 | 
			
		||||
    case 'nested object':
 | 
			
		||||
        for (const [k, v] of Object.entries(value)) {
 | 
			
		||||
            if (!(k in field.children)) continue;
 | 
			
		||||
            updateFieldValue(`${name}__${k}`, v, field.children[k], options);
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
    case 'dependent field':
 | 
			
		||||
        if (field.child) {
 | 
			
		||||
            updateFieldValue(name, value, field.child, options);
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
    case 'file upload':
 | 
			
		||||
    case 'image upload':
 | 
			
		||||
        break;
 | 
			
		||||
@@ -1165,6 +1195,17 @@ function getFormFieldValue(name, field={}, options={}) {
 | 
			
		||||
    case 'email':
 | 
			
		||||
        value = sanitizeInputString(el.val());
 | 
			
		||||
        break;
 | 
			
		||||
    case 'nested object':
 | 
			
		||||
        value = {};
 | 
			
		||||
        for (const [name, subField] of Object.entries(field.children)) {
 | 
			
		||||
            value[name] = getFormFieldValue(subField.name, subField, options);
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
    case 'dependent field':
 | 
			
		||||
        if(!field.child) return undefined;
 | 
			
		||||
 | 
			
		||||
        value = getFormFieldValue(name, field.child, options);
 | 
			
		||||
        break;
 | 
			
		||||
    default:
 | 
			
		||||
        value = el.val();
 | 
			
		||||
        break;
 | 
			
		||||
@@ -1449,19 +1490,28 @@ function handleFormErrors(errors, fields={}, options={}) {
 | 
			
		||||
        var field = fields[field_name] || {};
 | 
			
		||||
        var field_errors = errors[field_name];
 | 
			
		||||
 | 
			
		||||
        if ((field.type == 'nested object') && ('children' in field)) {
 | 
			
		||||
        // for nested objects with children and dependent fields with a child defined, extract nested errors
 | 
			
		||||
        if (((field.type == 'nested object') && ('children' in field)) || ((field.type == 'dependent field') && ('child' in field))) {
 | 
			
		||||
            // Handle multi-level nested errors
 | 
			
		||||
            const handleNestedError = (parent_name, sub_field_errors) => {
 | 
			
		||||
                for (const sub_field in sub_field_errors) {
 | 
			
		||||
                    const sub_sub_field_name = `${parent_name}__${sub_field}`;
 | 
			
		||||
                    const sub_sub_field_errors = sub_field_errors[sub_field];
 | 
			
		||||
 | 
			
		||||
            for (var sub_field in field_errors) {
 | 
			
		||||
                var sub_field_name = `${field_name}__${sub_field}`;
 | 
			
		||||
                var sub_field_errors = field_errors[sub_field];
 | 
			
		||||
 | 
			
		||||
                if (!first_error_field && sub_field_errors && isFieldVisible(sub_field_name, options)) {
 | 
			
		||||
                    first_error_field = sub_field_name;
 | 
			
		||||
                    if (!first_error_field && sub_sub_field_errors && isFieldVisible(sub_sub_field_name, options)) {
 | 
			
		||||
                        first_error_field = sub_sub_field_name;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                addFieldErrorMessage(sub_field_name, sub_field_errors, options);
 | 
			
		||||
                    // if the error is an object, its a nested object, recursively handle the errors
 | 
			
		||||
                    if (typeof sub_sub_field_errors === "object" && !Array.isArray(sub_sub_field_errors)) {
 | 
			
		||||
                        handleNestedError(sub_sub_field_name, sub_sub_field_errors)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        addFieldErrorMessage(sub_sub_field_name, sub_sub_field_errors, options);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            handleNestedError(field_name, field_errors);
 | 
			
		||||
        } else if ((field.type == 'field') && ('child' in field)) {
 | 
			
		||||
            // This is a "nested" array field
 | 
			
		||||
            handleNestedArrayErrors(errors, field_name, options);
 | 
			
		||||
@@ -1556,7 +1606,7 @@ function addFieldCallbacks(fields, options) {
 | 
			
		||||
 | 
			
		||||
        var field = fields[name];
 | 
			
		||||
 | 
			
		||||
        if (!field || !field.onEdit) continue;
 | 
			
		||||
        if (!field || field.type === "candy") continue;
 | 
			
		||||
 | 
			
		||||
        addFieldCallback(name, field, options);
 | 
			
		||||
    }
 | 
			
		||||
@@ -1564,17 +1614,36 @@ function addFieldCallbacks(fields, options) {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function addFieldCallback(name, field, options) {
 | 
			
		||||
    const el = getFormFieldElement(name, options);
 | 
			
		||||
 | 
			
		||||
    var el = getFormFieldElement(name, options);
 | 
			
		||||
 | 
			
		||||
    if (field.onEdit) {
 | 
			
		||||
        el.change(function() {
 | 
			
		||||
 | 
			
		||||
            var value = getFormFieldValue(name, field, options);
 | 
			
		||||
            let onEditHandlers = field.onEdit;
 | 
			
		||||
 | 
			
		||||
        field.onEdit(value, name, field, options);
 | 
			
		||||
            if (!Array.isArray(onEditHandlers)) {
 | 
			
		||||
                onEditHandlers = [onEditHandlers];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (const onEdit of onEditHandlers) {
 | 
			
		||||
                onEdit(value, name, field, options);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // attach field callback for nested fields
 | 
			
		||||
    if(field.type === "nested object") {
 | 
			
		||||
        for (const [c_name, c_field] of Object.entries(field.children)) {
 | 
			
		||||
            addFieldCallback(`${name}__${c_name}`, c_field, options);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if(field.type === "dependent field" && field.child) {
 | 
			
		||||
        addFieldCallback(name, field.child, options);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function addClearCallbacks(fields, options) {
 | 
			
		||||
 | 
			
		||||
@@ -1727,6 +1796,11 @@ function initializeRelatedFields(fields, options={}) {
 | 
			
		||||
 | 
			
		||||
        if (!field || field.hidden) continue;
 | 
			
		||||
 | 
			
		||||
        initializeRelatedFieldsRecursively(field, fields, options);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function initializeRelatedFieldsRecursively(field, fields, options) {
 | 
			
		||||
    switch (field.type) {
 | 
			
		||||
    case 'related field':
 | 
			
		||||
        initializeRelatedField(field, fields, options);
 | 
			
		||||
@@ -1734,11 +1808,22 @@ function initializeRelatedFields(fields, options={}) {
 | 
			
		||||
    case 'choice':
 | 
			
		||||
        initializeChoiceField(field, fields, options);
 | 
			
		||||
        break;
 | 
			
		||||
    case 'nested object':
 | 
			
		||||
        for (const [c_name, c_field] of Object.entries(field.children)) {
 | 
			
		||||
            if(!c_field.name) c_field.name = `${field.name}__${c_name}`;
 | 
			
		||||
            initializeRelatedFieldsRecursively(c_field, field.children, options);
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
    case 'dependent field':
 | 
			
		||||
        if (field.child) {
 | 
			
		||||
            if(!field.child.name) field.child.name = field.name;
 | 
			
		||||
            initializeRelatedFieldsRecursively(field.child, fields, options);
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
    default:
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
@@ -2346,7 +2431,7 @@ function constructField(name, parameters, options={}) {
 | 
			
		||||
    html += `<div id='div_id_${field_name}' class='${form_classes}' ${hover_title} ${css}>`;
 | 
			
		||||
 | 
			
		||||
    // Add a label
 | 
			
		||||
    if (!options.hideLabels) {
 | 
			
		||||
    if (!options.hideLabels && parameters.type !== "nested object" && parameters.type !== "dependent field") {
 | 
			
		||||
        html += constructLabel(name, parameters);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -2501,6 +2586,12 @@ function constructInput(name, parameters, options={}) {
 | 
			
		||||
    case 'raw':
 | 
			
		||||
        func = constructRawInput;
 | 
			
		||||
        break;
 | 
			
		||||
    case 'nested object':
 | 
			
		||||
        func = constructNestedObject;
 | 
			
		||||
        break;
 | 
			
		||||
    case 'dependent field':
 | 
			
		||||
        func = constructDependentField;
 | 
			
		||||
        break;
 | 
			
		||||
    default:
 | 
			
		||||
        // Unsupported field type!
 | 
			
		||||
        break;
 | 
			
		||||
@@ -2780,6 +2871,129 @@ function constructRawInput(name, parameters) {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Construct a nested object input
 | 
			
		||||
 */
 | 
			
		||||
function constructNestedObject(name, parameters, options) {
 | 
			
		||||
    let html = `
 | 
			
		||||
        <div id="div_id_${name}" class='panel form-panel' style="margin-bottom: 0; padding-bottom: 0;">
 | 
			
		||||
            <div class='panel-heading form-panel-heading'>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <h6 style='display: inline;'>${parameters.label}</h6>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class='panel-content form-panel-content' id="id_${name}">
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    parameters.field_names = [];
 | 
			
		||||
 | 
			
		||||
    for (const [key, field] of Object.entries(parameters.children)) {
 | 
			
		||||
        const subFieldName = `${name}__${key}`;
 | 
			
		||||
        field.name = subFieldName;
 | 
			
		||||
        parameters.field_names.push(subFieldName);
 | 
			
		||||
 | 
			
		||||
        html += constructField(subFieldName, field, options);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    html += "</div></div>";
 | 
			
		||||
 | 
			
		||||
    return html;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getFieldByNestedPath(name, fields) {
 | 
			
		||||
    if (typeof name === "string") {
 | 
			
		||||
        name = name.split("__");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (name.length === 0) return fields;
 | 
			
		||||
 | 
			
		||||
    if (fields.type === "nested object") fields = fields.children;
 | 
			
		||||
 | 
			
		||||
    if (!(name[0] in fields)) return null;
 | 
			
		||||
    let field = fields[name[0]];
 | 
			
		||||
 | 
			
		||||
    if (field.type === "dependent field" && field.child) {
 | 
			
		||||
        field = field.child;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return getFieldByNestedPath(name.slice(1), field);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Construct a dependent field input
 | 
			
		||||
 */
 | 
			
		||||
function constructDependentField(name, parameters, options) {
 | 
			
		||||
    // add onEdit handler to all fields this dependent field depends on
 | 
			
		||||
    for (let d_field_name of parameters.depends_on) {
 | 
			
		||||
        const d_field = getFieldByNestedPath([...name.split("__").slice(0, -1), d_field_name], options.fields);
 | 
			
		||||
        if (!d_field) continue;
 | 
			
		||||
 | 
			
		||||
        const onEdit = (value, name, field, options) => {
 | 
			
		||||
            if(value === undefined) return;
 | 
			
		||||
 | 
			
		||||
            // extract the current form data to include in OPTIONS request
 | 
			
		||||
            const data = extractFormData(options.fields, options, false)
 | 
			
		||||
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                url: options.url,
 | 
			
		||||
                type: "OPTIONS",
 | 
			
		||||
                data: JSON.stringify(data),
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                dataType: "json",
 | 
			
		||||
                accepts: { json: "application/json" },
 | 
			
		||||
                success: (res) => {
 | 
			
		||||
                    const fields = res.actions[options.method];
 | 
			
		||||
 | 
			
		||||
                    // merge already entered values in the newly constructed form
 | 
			
		||||
                    options.data = extractFormData(options.fields, options);
 | 
			
		||||
 | 
			
		||||
                    // remove old submit handlers
 | 
			
		||||
                    $(options.modal).off('click', '#modal-form-submit');
 | 
			
		||||
 | 
			
		||||
                    if (options.method === "POST") {
 | 
			
		||||
                        constructCreateForm(fields, options);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (options.method === "PUT" || options.method === "PATCH") {
 | 
			
		||||
                        constructChangeForm(fields, options);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (options.method === "DELETE") {
 | 
			
		||||
                        constructDeleteForm(fields, options);
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                error: (xhr) => showApiError(xhr, options.url)
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // attach on edit handler
 | 
			
		||||
        const originalOnEdit = d_field.onEdit;
 | 
			
		||||
        d_field.onEdit = [onEdit];
 | 
			
		||||
 | 
			
		||||
        if(typeof originalOnEdit === "function") {
 | 
			
		||||
            d_field.onEdit.push(originalOnEdit);
 | 
			
		||||
        } else if (Array.isArray(originalOnEdit)) {
 | 
			
		||||
            // push old onEdit handlers, but omit the old
 | 
			
		||||
            d_field.onEdit.push(...originalOnEdit.filter(h => h !== d_field._currentDependentFieldOnEdit));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // track current onEdit handler function
 | 
			
		||||
        d_field._currentDependentFieldOnEdit = onEdit;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // child is not specified already, return a dummy div with id so no errors can happen
 | 
			
		||||
    if (!parameters.child) {
 | 
			
		||||
        return `<div id="id_${name}" hidden></div>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // copy label to child if not already provided
 | 
			
		||||
    if(!parameters.child.label) {
 | 
			
		||||
        parameters.child.label = parameters.label;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // construct the provided child field
 | 
			
		||||
    return constructField(name, parameters.child, options);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Construct a 'help text' div based on the field parameters
 | 
			
		||||
 
 | 
			
		||||
@@ -137,6 +137,11 @@ function printLabels(options) {
 | 
			
		||||
 | 
			
		||||
        // update form
 | 
			
		||||
        updateForm(formOptions);
 | 
			
		||||
 | 
			
		||||
        // workaround to fix a bug where one cannot scroll after changing the plugin
 | 
			
		||||
        // without opening and closing the select box again manually
 | 
			
		||||
        $("#id__plugin").select2("open");
 | 
			
		||||
        $("#id__plugin").select2("close");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const printingFormOptions = {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import logging
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from django.contrib.auth import get_user_model
 | 
			
		||||
from django.contrib.auth.models import Group, Permission
 | 
			
		||||
from django.contrib.auth.models import Group, Permission, User
 | 
			
		||||
from django.contrib.contenttypes.fields import GenericForeignKey
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
@@ -21,6 +21,7 @@ from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from rest_framework.authtoken.models import Token as AuthToken
 | 
			
		||||
 | 
			
		||||
import common.models as common_models
 | 
			
		||||
import InvenTree.helpers
 | 
			
		||||
import InvenTree.models
 | 
			
		||||
from InvenTree.ready import canAppAccessDatabase
 | 
			
		||||
@@ -28,6 +29,22 @@ from InvenTree.ready import canAppAccessDatabase
 | 
			
		||||
logger = logging.getLogger("inventree")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#  OVERRIDE START
 | 
			
		||||
# Overrides Django User model __str__ with a custom function to be able to change
 | 
			
		||||
# string representation of a user
 | 
			
		||||
def user_model_str(self):
 | 
			
		||||
    """Function to override the default Django User __str__"""
 | 
			
		||||
 | 
			
		||||
    if common_models.InvenTreeSetting.get_setting('DISPLAY_FULL_NAMES'):
 | 
			
		||||
        if self.first_name or self.last_name:
 | 
			
		||||
            return f'{self.first_name} {self.last_name}'
 | 
			
		||||
    return self.username
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
User.add_to_class("__str__", user_model_str)  # Overriding User.__str__
 | 
			
		||||
#  OVERRIDE END
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def default_token():
 | 
			
		||||
    """Generate a default value for the token"""
 | 
			
		||||
    return ApiToken.generate_key()
 | 
			
		||||
@@ -785,10 +802,16 @@ class Owner(models.Model):
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        """Defines the owner string representation."""
 | 
			
		||||
        return f'{self.owner} ({self.owner_type.name})'
 | 
			
		||||
        if self.owner_type.name == 'user' and common_models.InvenTreeSetting.get_setting('DISPLAY_FULL_NAMES'):
 | 
			
		||||
            display_name = self.owner.get_full_name()
 | 
			
		||||
        else:
 | 
			
		||||
            display_name = str(self.owner)
 | 
			
		||||
        return f'{display_name} ({self.owner_type.name})'
 | 
			
		||||
 | 
			
		||||
    def name(self):
 | 
			
		||||
        """Return the 'name' of this owner."""
 | 
			
		||||
        if self.owner_type.name == 'user' and common_models.InvenTreeSetting.get_setting('DISPLAY_FULL_NAMES'):
 | 
			
		||||
            return self.owner.get_full_name()
 | 
			
		||||
        return str(self.owner)
 | 
			
		||||
 | 
			
		||||
    def label(self):
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user