diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 26ce0c47d7..952bd5fa63 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 514 +INVENTREE_API_VERSION = 515 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v515 -> 2026-07-03 : https://github.com/inventree/InvenTree/pull/12298 + - Change the file fields definition to binary (from uri) in the upload requests + v514 -> 2026-07-02 : https://github.com/inventree/InvenTree/pull/12294 - Adds "duplicate" field to the BuildOrder, Company, ManufacturerPart, SupplierPart and SalesOrderShipment API endpoints - Order duplication options: renames "order_id" field to "original", which now performs primary-key validation diff --git a/src/backend/InvenTree/InvenTree/fields.py b/src/backend/InvenTree/InvenTree/fields.py index b7c09dbe12..1254b1b57d 100644 --- a/src/backend/InvenTree/InvenTree/fields.py +++ b/src/backend/InvenTree/InvenTree/fields.py @@ -1,6 +1,7 @@ """Custom fields used in InvenTree.""" import sys +import uuid from decimal import Decimal from django import forms @@ -59,6 +60,34 @@ class InvenTreeURLField(models.URLField): super().__init__(**kwargs) +class InvenTreeUUIDField(models.UUIDField): + """UUIDField which is always stored as a char(32) column on MySQL / MariaDB. + + On MariaDB 10.7+, Django maps UUIDField to the native 'uuid' column type, + and writes 36-character (hyphenated) values to match. + However, databases migrated under older Django / MariaDB versions retain + their original char(32) columns, into which a 36-character value does not fit. + + To ensure consistent behavior across all supported database backends, + we force the legacy char(32) storage format on MySQL / MariaDB. + + Ref: https://docs.djangoproject.com/en/5.2/releases/5.0/#migrating-existing-uuidfield-on-mariadb-10-7 + """ + + def db_type(self, connection): + """Force a char(32) column type on MySQL / MariaDB backends.""" + if connection.vendor == 'mysql': + return 'char(32)' + return super().db_type(connection) + + def get_db_prep_value(self, value, connection, prepared=False): + """Store values in 32-character hex format on MySQL / MariaDB backends.""" + value = super().get_db_prep_value(value, connection, prepared) + if connection.vendor == 'mysql' and isinstance(value, uuid.UUID): + value = value.hex + return value + + def money_kwargs(**kwargs): """Returns the database settings for MoneyFields.""" from common.currency import currency_code_mappings diff --git a/src/backend/InvenTree/InvenTree/schema.py b/src/backend/InvenTree/InvenTree/schema.py index edea87bfef..ebd3bda9d8 100644 --- a/src/backend/InvenTree/InvenTree/schema.py +++ b/src/backend/InvenTree/InvenTree/schema.py @@ -16,6 +16,7 @@ from drf_spectacular.utils import ( extend_schema, extend_schema_view, ) +from rest_framework import serializers from rest_framework.pagination import LimitOffsetPagination from InvenTree.permissions import OASTokenMixin @@ -46,6 +47,20 @@ class ExtendedOAuth2Scheme(DjangoOAuthToolkitScheme): class ExtendedAutoSchema(AutoSchema): """Extend drf-spectacular to allow customizing the schema to match the actual API behavior.""" + def _map_serializer_field(self, field, direction, *args, **kwargs): + """Custom field mapping overrides, falling back to default behavior.""" + schema = super()._map_serializer_field(field, direction, *args, **kwargs) + + direction_value = getattr(direction, 'value', direction) + + # File and image fields in request schemas must be represented as binary + # payloads. In response schemas they are still rendered as URLs. + if direction_value == 'request' and isinstance(field, serializers.FileField): + schema['type'] = 'string' + schema['format'] = 'binary' + + return schema + def is_bulk_action(self, ref: str) -> bool: """Check the class of the current view for the bulk mixins.""" return ref in [c.__name__ for c in type(self.view).__mro__] diff --git a/src/backend/InvenTree/InvenTree/tests.py b/src/backend/InvenTree/InvenTree/tests.py index ea2f6d4171..7934f91324 100644 --- a/src/backend/InvenTree/InvenTree/tests.py +++ b/src/backend/InvenTree/InvenTree/tests.py @@ -22,6 +22,7 @@ from djmoney.contrib.exchange.exceptions import MissingRate from djmoney.contrib.exchange.models import Rate, convert_money from djmoney.money import Money from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode +from rest_framework import serializers from sesame.utils import get_user from stdimage.models import StdImageFieldFile @@ -1817,6 +1818,35 @@ class SchemaPostprocessingTest(TestCase): # required key removed when empty self.assertNotIn('required', schemas_out.get('SalesOrderShipment')) + def test_file_field_request_schema_binary(self): + """Verify only request file fields are exposed as binary.""" + auto_schema = object.__new__(schema.ExtendedAutoSchema) + + mapped_schemas = [ + {'type': 'string', 'format': 'uri', 'nullable': True}, + {'type': 'string', 'format': 'uri'}, + {'type': 'string', 'format': 'uri', 'nullable': True}, + ] + + with mock.patch( + 'drf_spectacular.openapi.AutoSchema._map_serializer_field', + side_effect=mapped_schemas, + ): + file_request = auto_schema._map_serializer_field( + serializers.FileField(allow_null=True), 'request' + ) + url_request = auto_schema._map_serializer_field( + serializers.URLField(), 'request' + ) + file_response = auto_schema._map_serializer_field( + serializers.FileField(allow_null=True), 'response' + ) + + self.assertEqual(file_request['format'], 'binary') + self.assertTrue(file_request['nullable']) + self.assertEqual(url_request['format'], 'uri') + self.assertEqual(file_response['format'], 'uri') + class URLCompatibilityTest(InvenTreeTestCase): """Unit test for legacy URL compatibility.""" diff --git a/src/backend/InvenTree/common/migrations/0046_alter_emailmessage_global_id_and_more.py b/src/backend/InvenTree/common/migrations/0046_alter_emailmessage_global_id_and_more.py new file mode 100644 index 0000000000..5ead5a53bf --- /dev/null +++ b/src/backend/InvenTree/common/migrations/0046_alter_emailmessage_global_id_and_more.py @@ -0,0 +1,59 @@ +"""Migration to store UUID primary keys as char(32) on MySQL / MariaDB. + +On MariaDB 10.7+, Django 5.x creates UUIDField columns with the native 'uuid' +type and writes 36-character (hyphenated) values. Databases migrated under older +Django / MariaDB versions retain char(32) columns, into which the new values +do not fit. + +See: https://github.com/inventree/InvenTree/issues/12270 +""" + +import uuid + +from django.db import migrations + +import InvenTree.fields + + +class Migration(migrations.Migration): + dependencies = [('common', '0045_projectcode_active')] + + operations = [ + migrations.AlterField( + model_name='emailmessage', + name='global_id', + field=InvenTree.fields.InvenTreeUUIDField( + default=uuid.uuid4, + editable=False, + help_text='Unique identifier for this message', + primary_key=True, + serialize=False, + unique=True, + verbose_name='Global ID', + ), + ), + migrations.AlterField( + model_name='emailthread', + name='global_id', + field=InvenTree.fields.InvenTreeUUIDField( + default=uuid.uuid4, + editable=False, + help_text='Unique identifier for this thread', + primary_key=True, + serialize=False, + verbose_name='Global ID', + ), + ), + migrations.AlterField( + model_name='webhookmessage', + name='message_id', + field=InvenTree.fields.InvenTreeUUIDField( + default=uuid.uuid4, + editable=False, + help_text='Unique identifier for this message', + primary_key=True, + serialize=False, + verbose_name='Message ID', + ), + ), + ] diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index b07dd08eeb..bb89a0622b 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -1584,7 +1584,7 @@ class WebhookMessage(models.Model): worked_on: Was the work on this message finished? """ - message_id = models.UUIDField( + message_id = InvenTree.fields.InvenTreeUUIDField( verbose_name=_('Message ID'), help_text=_('Unique identifier for this message'), primary_key=True, @@ -3266,7 +3266,7 @@ class EmailMessage(models.Model): TRACK_READ = 'track_read', _('Track Read') TRACK_CLICK = 'track_click', _('Track Click') - global_id = models.UUIDField( + global_id = InvenTree.fields.InvenTreeUUIDField( verbose_name=_('Global ID'), help_text=_('Unique identifier for this message'), primary_key=True, @@ -3374,7 +3374,7 @@ class EmailThread(InvenTree.models.InvenTreeMetadataModel): blank=True, help_text=_('Unique key for this thread (used to identify the thread)'), ) - global_id = models.UUIDField( + global_id = InvenTree.fields.InvenTreeUUIDField( verbose_name=_('Global ID'), help_text=_('Unique identifier for this thread'), primary_key=True, diff --git a/src/backend/InvenTree/machine/migrations/0002_alter_machineconfig_id.py b/src/backend/InvenTree/machine/migrations/0002_alter_machineconfig_id.py new file mode 100644 index 0000000000..5d3aac8bea --- /dev/null +++ b/src/backend/InvenTree/machine/migrations/0002_alter_machineconfig_id.py @@ -0,0 +1,28 @@ +"""Migration to store MachineConfig UUID primary keys as char(32) on MySQL / MariaDB. + +On MariaDB 10.7+, Django 5.x writes 36-character (hyphenated) UUID values, +but databases migrated under older Django / MariaDB versions retain char(32) +columns, causing machine creation to fail with "Data too long for column 'id'". + +See: https://github.com/inventree/InvenTree/issues/12270 +""" + +import uuid + +from django.db import migrations + +import InvenTree.fields + + +class Migration(migrations.Migration): + dependencies = [('machine', '0001_initial')] + + operations = [ + migrations.AlterField( + model_name='machineconfig', + name='id', + field=InvenTree.fields.InvenTreeUUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ] diff --git a/src/backend/InvenTree/machine/models.py b/src/backend/InvenTree/machine/models.py index b81b1c7a00..f1f1896a85 100755 --- a/src/backend/InvenTree/machine/models.py +++ b/src/backend/InvenTree/machine/models.py @@ -10,6 +10,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ import common.models +import InvenTree.fields from machine import registry from machine.machine_type import BaseMachineType @@ -17,7 +18,9 @@ from machine.machine_type import BaseMachineType class MachineConfig(models.Model): """A Machine objects represents a physical machine.""" - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + id = InvenTree.fields.InvenTreeUUIDField( + primary_key=True, default=uuid.uuid4, editable=False + ) name = models.CharField( unique=True,