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)
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.
- User must have 'staff' status to view / edit
@@ -328,7 +337,7 @@ class UserSettingsList(SettingsList):
return queryset
class UserSettingsDetail(RetrieveUpdateAPI):
class UserSettingsDetail(TouchMixin, RetrieveUpdateAPI):
"""Detail view for an individual "user setting" object.
- User can only view / edit settings their own settings objects
@@ -9,25 +9,25 @@ import django.db.models.deletion
import taggit.managers
import uuid
from django.conf import settings
from django.db import migrations, models
from django.db import migrations, models, transaction
from moneyed import CURRENCIES
def set_default_currency(apps, schema_editor):
"""Migrate the currency setting from config.yml to db."""
from common.currency import currency_codes
from common.models import InvenTreeSetting
InvenTreeSetting = apps.get_model('common', 'InvenTreeSetting')
key = 'INVENTREE_DEFAULT_CURRENCY'
base_currency = InvenTree.config.get_setting('INVENTREE_BASE_CURRENCY', 'base_currency', 'USD')
base_currency = InvenTree.config.get_setting(
'INVENTREE_BASE_CURRENCY', 'base_currency', 'USD'
)
if base_currency not in currency_codes():
base_currency = currency_codes()[0] if currency_codes() else 'USD'
InvenTreeSetting.set_setting(
'INVENTREE_DEFAULT_CURRENCY', base_currency, None, create=True
)
if base_currency not in CURRENCIES:
base_currency = 'USD'
with transaction.atomic():
setting = InvenTreeSetting.objects.filter(key=key).first()
if setting:
setting.value = base_currency
setting.save()
else:
InvenTreeSetting(key=key, value=base_currency).save()
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).
Attributes:
@@ -217,6 +225,8 @@ class BaseInvenTreeSetting(models.Model):
CHECK_SETTING_KEY = False
objects = SettingsManager()
extra_unique_fields: list[str] = []
class Meta:
@@ -614,7 +624,6 @@ class BaseInvenTreeSetting(models.Model):
access_global_cache = False
# At this point, we need to query the database
filters = {
'key__iexact': key,
# Optionally filter by other keys
@@ -806,6 +815,11 @@ class BaseInvenTreeSetting(models.Model):
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
def name(self):
"""Return name for setting."""
@@ -152,6 +152,15 @@ class SettingsSerializer(InvenTreeModelSerializer):
})
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):
"""Serializer for the InvenTreeSetting model."""
+2 -1
View File
@@ -10,6 +10,7 @@ from rest_framework.views import APIView
import InvenTree.permissions
import machine.serializers as MachineSerializers
from common.api import TouchMixin
from InvenTree.filters import SEARCH_ORDER_FILTER
from InvenTree.mixins import ListCreateAPI, RetrieveUpdateAPI, RetrieveUpdateDestroyAPI
from machine import registry
@@ -108,7 +109,7 @@ class MachineSettingList(APIView):
return Response(results)
class MachineSettingDetail(RetrieveUpdateAPI):
class MachineSettingDetail(TouchMixin, RetrieveUpdateAPI):
"""Detail endpoint for a machine-specific setting.
- 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'),
),
]