feat(backend): mark setting when changed via API (#12982)

* mark setting when changed via API

might be helpful for https://github.com/inventree/InvenTree/issues/12767

* add migration

* fix migration

* use atomic instead

* move atomic

* move the transaction safety to be migration specific

* reduce diff

* make write safe

* remove migration_safe_query again

* use a simpler approach in the squashed migration

* try using a manager

* add update / updated_by to api

* update generic settings api to handle updated_by field
This commit is contained in:
Matthias Mair authored and GitHub committed 2026-10-13 20:44:58 +11:00
1 parent 76a21b7899
commit 7e89e93252
8 files changed
+174 -18

No files matched your search

+11 -2
View File
@@ -269,7 +269,16 @@ class GlobalSettingsList(SettingsList):
return super().list(request, *args, **kwargs) return super().list(request, *args, **kwargs)
class GlobalSettingsDetail(RetrieveUpdateAPI): class TouchMixin:
"""Mark a setting as touched when it is modified by the user."""
def perform_update(self, serializer):
"""Mark a setting as touched when it is modified by the user."""
serializer.instance.touched = True
super().perform_update(serializer)
class GlobalSettingsDetail(TouchMixin, RetrieveUpdateAPI):
"""Detail view for an individual "global setting" object. """Detail view for an individual "global setting" object.
- User must have 'staff' status to view / edit - User must have 'staff' status to view / edit
@@ -328,7 +337,7 @@ class UserSettingsList(SettingsList):
return queryset return queryset
class UserSettingsDetail(RetrieveUpdateAPI): class UserSettingsDetail(TouchMixin, RetrieveUpdateAPI):
"""Detail view for an individual "user setting" object. """Detail view for an individual "user setting" object.
- User can only view / edit settings their own settings objects - User can only view / edit settings their own settings objects
@@ -9,25 +9,25 @@ import django.db.models.deletion
import taggit.managers import taggit.managers
import uuid import uuid
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models, transaction
from moneyed import CURRENCIES from moneyed import CURRENCIES
def set_default_currency(apps, schema_editor): def set_default_currency(apps, schema_editor):
"""Migrate the currency setting from config.yml to db.""" """Migrate the currency setting from config.yml to db."""
from common.currency import currency_codes InvenTreeSetting = apps.get_model('common', 'InvenTreeSetting')
from common.models import InvenTreeSetting key = 'INVENTREE_DEFAULT_CURRENCY'
base_currency = InvenTree.config.get_setting('INVENTREE_BASE_CURRENCY', 'base_currency', 'USD')
base_currency = InvenTree.config.get_setting( if base_currency not in CURRENCIES:
'INVENTREE_BASE_CURRENCY', 'base_currency', 'USD' base_currency = 'USD'
) with transaction.atomic():
setting = InvenTreeSetting.objects.filter(key=key).first()
if base_currency not in currency_codes(): if setting:
base_currency = currency_codes()[0] if currency_codes() else 'USD' setting.value = base_currency
setting.save()
InvenTreeSetting.set_setting( else:
'INVENTREE_DEFAULT_CURRENCY', base_currency, None, create=True InvenTreeSetting(key=key, value=base_currency).save()
)
def set_currencies(apps, schema_editor): def set_currencies(apps, schema_editor):
@@ -0,0 +1,46 @@
# Generated by Django 5.2.17 on 2026-10-12 22:47
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0052_remove_notesimage_model_id_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='inventreesetting',
name='touched',
field=models.BooleanField(default=False, help_text='Indicates whether the setting has been modified by the user'),
),
migrations.AddField(
model_name='inventreesetting',
name='updated',
field=models.DateTimeField(blank=True, default=None, help_text='Timestamp of last update', null=True, verbose_name='Updated'),
),
migrations.AddField(
model_name='inventreesetting',
name='updated_by',
field=models.ForeignKey(blank=True, help_text='User who last updated this object', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated', to=settings.AUTH_USER_MODEL, verbose_name='Update By'),
),
migrations.AddField(
model_name='inventreeusersetting',
name='touched',
field=models.BooleanField(default=False, help_text='Indicates whether the setting has been modified by the user'),
),
migrations.AddField(
model_name='inventreeusersetting',
name='updated',
field=models.DateTimeField(blank=True, default=None, help_text='Timestamp of last update', null=True, verbose_name='Updated'),
),
migrations.AddField(
model_name='inventreeusersetting',
name='updated_by',
field=models.ForeignKey(blank=True, help_text='User who last updated this object', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated', to=settings.AUTH_USER_MODEL, verbose_name='Update By'),
),
]
+16 -2
View File
@@ -205,7 +205,15 @@ class ProjectCode(InvenTree.models.InvenTreeMetadataModel):
) )
class BaseInvenTreeSetting(models.Model): class SettingsManager(models.Manager):
"""Ensure that the 'touched' field is deferred in all queries."""
def get_queryset(self):
"""Defer the write-only 'touched' field."""
return super().get_queryset().defer('touched', 'updated_by', 'updated')
class BaseInvenTreeSetting(UpdatedUserMixin, models.Model):
"""An base InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values). """An base InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values).
Attributes: Attributes:
@@ -217,6 +225,8 @@ class BaseInvenTreeSetting(models.Model):
CHECK_SETTING_KEY = False CHECK_SETTING_KEY = False
objects = SettingsManager()
extra_unique_fields: list[str] = [] extra_unique_fields: list[str] = []
class Meta: class Meta:
@@ -614,7 +624,6 @@ class BaseInvenTreeSetting(models.Model):
access_global_cache = False access_global_cache = False
# At this point, we need to query the database # At this point, we need to query the database
filters = { filters = {
'key__iexact': key, 'key__iexact': key,
# Optionally filter by other keys # Optionally filter by other keys
@@ -806,6 +815,11 @@ class BaseInvenTreeSetting(models.Model):
max_length=2000, blank=True, unique=False, help_text=_('Settings value') max_length=2000, blank=True, unique=False, help_text=_('Settings value')
) )
touched = models.BooleanField(
default=False,
help_text=_('Indicates whether the setting has been modified by the user'),
)
@property @property
def name(self): def name(self):
"""Return name for setting.""" """Return name for setting."""
@@ -152,6 +152,15 @@ class SettingsSerializer(InvenTreeModelSerializer):
}) })
return ret return ret
def save(self, **kwargs):
"""Save the setting, adding the updated_by field."""
request = self.context.get('request', None)
if request is not None and hasattr(request, 'user'):
kwargs['updated_by'] = request.user
elif 'updated_by' in kwargs:
kwargs.pop('updated_by')
return super().save(**kwargs)
class GlobalSettingsSerializer(SettingsSerializer): class GlobalSettingsSerializer(SettingsSerializer):
"""Serializer for the InvenTreeSetting model.""" """Serializer for the InvenTreeSetting model."""
+2 -1
View File
@@ -10,6 +10,7 @@ from rest_framework.views import APIView
import InvenTree.permissions import InvenTree.permissions
import machine.serializers as MachineSerializers import machine.serializers as MachineSerializers
from common.api import TouchMixin
from InvenTree.filters import SEARCH_ORDER_FILTER from InvenTree.filters import SEARCH_ORDER_FILTER
from InvenTree.mixins import ListCreateAPI, RetrieveUpdateAPI, RetrieveUpdateDestroyAPI from InvenTree.mixins import ListCreateAPI, RetrieveUpdateAPI, RetrieveUpdateDestroyAPI
from machine import registry from machine import registry
@@ -108,7 +109,7 @@ class MachineSettingList(APIView):
return Response(results) return Response(results)
class MachineSettingDetail(RetrieveUpdateAPI): class MachineSettingDetail(TouchMixin, RetrieveUpdateAPI):
"""Detail endpoint for a machine-specific setting. """Detail endpoint for a machine-specific setting.
- GET: Get machine setting detail - GET: Get machine setting detail
@@ -0,0 +1,31 @@
# Generated by Django 5.2.17 on 2026-10-12 22:47
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machine', '0002_alter_machineconfig_id'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='machinesetting',
name='touched',
field=models.BooleanField(default=False, help_text='Indicates whether the setting has been modified by the user'),
),
migrations.AddField(
model_name='machinesetting',
name='updated',
field=models.DateTimeField(blank=True, default=None, help_text='Timestamp of last update', null=True, verbose_name='Updated'),
),
migrations.AddField(
model_name='machinesetting',
name='updated_by',
field=models.ForeignKey(blank=True, help_text='User who last updated this object', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated', to=settings.AUTH_USER_MODEL, verbose_name='Update By'),
),
]
@@ -0,0 +1,46 @@
# Generated by Django 5.2.17 on 2026-10-12 22:47
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('plugin', '0011_delete_notificationusersetting'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='pluginsetting',
name='touched',
field=models.BooleanField(default=False, help_text='Indicates whether the setting has been modified by the user'),
),
migrations.AddField(
model_name='pluginsetting',
name='updated',
field=models.DateTimeField(blank=True, default=None, help_text='Timestamp of last update', null=True, verbose_name='Updated'),
),
migrations.AddField(
model_name='pluginsetting',
name='updated_by',
field=models.ForeignKey(blank=True, help_text='User who last updated this object', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated', to=settings.AUTH_USER_MODEL, verbose_name='Update By'),
),
migrations.AddField(
model_name='pluginusersetting',
name='touched',
field=models.BooleanField(default=False, help_text='Indicates whether the setting has been modified by the user'),
),
migrations.AddField(
model_name='pluginusersetting',
name='updated',
field=models.DateTimeField(blank=True, default=None, help_text='Timestamp of last update', null=True, verbose_name='Updated'),
),
migrations.AddField(
model_name='pluginusersetting',
name='updated_by',
field=models.ForeignKey(blank=True, help_text='User who last updated this object', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated', to=settings.AUTH_USER_MODEL, verbose_name='Update By'),
),
]