From 5a2d223cd097bbe16a577ec08d87aa18d717cd1d Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Thu, 8 Oct 2026 10:26:25 +0200 Subject: [PATCH] use mixin instead of repeating patterns (#12977) --- src/backend/InvenTree/InvenTree/admin.py | 33 ++-------- src/backend/InvenTree/common/admin.py | 67 +++----------------- src/backend/InvenTree/common/admin_shared.py | 25 ++++++++ src/backend/InvenTree/machine/admin.py | 7 +- src/backend/InvenTree/plugin/admin.py | 13 +--- src/backend/InvenTree/stock/admin.py | 16 +---- 6 files changed, 46 insertions(+), 115 deletions(-) create mode 100644 src/backend/InvenTree/common/admin_shared.py diff --git a/src/backend/InvenTree/InvenTree/admin.py b/src/backend/InvenTree/InvenTree/admin.py index 637e77a10f..11da00afa6 100644 --- a/src/backend/InvenTree/InvenTree/admin.py +++ b/src/backend/InvenTree/InvenTree/admin.py @@ -1,7 +1,6 @@ """Admin classes.""" from django.contrib import admin -from django.http.request import HttpRequest from django.utils import timezone from allauth.usersessions.admin import UserSessionAdmin @@ -11,14 +10,12 @@ from django_q.models import Schedule from djmoney.contrib.exchange.admin import RateAdmin from djmoney.contrib.exchange.models import Rate +from common.admin_shared import NoAddAdminMixin, NoEditAdminMixin, ReadOnlyAdminMixin -class CustomRateAdmin(RateAdmin): + +class CustomRateAdmin(NoAddAdminMixin, RateAdmin): """Admin interface for the Rate class.""" - def has_add_permission(self, request: HttpRequest) -> bool: - """Disable the 'add' permission for Rate objects.""" - return False - admin.site.unregister(Rate) admin.site.register(Rate, CustomRateAdmin) @@ -34,39 +31,19 @@ def run_schedule_now(modeladmin, request, queryset): run_schedule_now.short_description = 'Run selected tasks now' -class ReadOnlyScheduleAdmin(ScheduleAdmin): +class ReadOnlyScheduleAdmin(ReadOnlyAdminMixin, ScheduleAdmin): """Read-only admin interface for django-q Schedule objects.""" actions = [run_schedule_now] - def has_add_permission(self, request: HttpRequest) -> bool: - """Prevent adding new Schedule objects.""" - return False - - def has_change_permission(self, request: HttpRequest, obj=None) -> bool: - """Prevent changing existing Schedule objects.""" - return False - - def has_delete_permission(self, request: HttpRequest, obj=None) -> bool: - """Prevent deleting Schedule objects.""" - return False - admin.site.unregister(Schedule) admin.site.register(Schedule, ReadOnlyScheduleAdmin) -class InvenTreeUserSessionAdmin(UserSessionAdmin): +class InvenTreeUserSessionAdmin(NoEditAdminMixin, UserSessionAdmin): """Admin interface for UserSession - view and delete only, no add or edit.""" - def has_add_permission(self, request: HttpRequest) -> bool: - """Prevent creating sessions via admin.""" - return False - - def has_change_permission(self, request: HttpRequest, obj=None) -> bool: - """Prevent editing sessions via admin.""" - return False - admin.site.unregister(UserSession) admin.site.register(UserSession, InvenTreeUserSessionAdmin) diff --git a/src/backend/InvenTree/common/admin.py b/src/backend/InvenTree/common/admin.py index b41889cd9e..b5ec01a58a 100644 --- a/src/backend/InvenTree/common/admin.py +++ b/src/backend/InvenTree/common/admin.py @@ -4,6 +4,7 @@ from django.contrib import admin import common.models import common.validators +from common.admin_shared import NoEditAdminMixin, ReadOnlyAdminMixin @admin.register(common.models.ParameterTemplate) @@ -89,7 +90,7 @@ class AttachmentAdmin(admin.ModelAdmin): @admin.register(common.models.DataOutput) -class DataOutputAdmin(admin.ModelAdmin): +class DataOutputAdmin(NoEditAdminMixin, admin.ModelAdmin): """Admin interface for DataOutput objects - view and delete only.""" list_display = ('user', 'created', 'output_type', 'output') @@ -106,17 +107,9 @@ class DataOutputAdmin(admin.ModelAdmin): autocomplete_fields = ('user',) - def has_add_permission(self, request): - """Prevent addition of new DataOutput objects via the admin interface.""" - return False - - def has_change_permission(self, request, obj=None): - """Prevent modification of DataOutput objects via the admin interface.""" - return False - @admin.register(common.models.BarcodeScanResult) -class BarcodeScanResultAdmin(admin.ModelAdmin): +class BarcodeScanResultAdmin(NoEditAdminMixin, admin.ModelAdmin): """Admin interface for BarcodeScanResult objects - read-only audit log.""" list_display = ('data', 'timestamp', 'user', 'endpoint', 'result') @@ -127,14 +120,6 @@ class BarcodeScanResultAdmin(admin.ModelAdmin): autocomplete_fields = ('user',) - def has_add_permission(self, request): - """Prevent addition of new BarcodeScanResult objects via the admin interface.""" - return False - - def has_change_permission(self, request, obj=None): - """Prevent modification of BarcodeScanResult objects via the admin interface.""" - return False - @admin.register(common.models.ProjectCode) class ProjectCodeAdmin(admin.ModelAdmin): @@ -196,24 +181,16 @@ class WebhookAdmin(admin.ModelAdmin): @admin.register(common.models.NotificationEntry) -class NotificationEntryAdmin(admin.ModelAdmin): +class NotificationEntryAdmin(NoEditAdminMixin, admin.ModelAdmin): """Admin settings for NotificationEntry - view and delete only.""" list_display = ('key', 'uid', 'updated') search_fields = ('key', 'uid') - def has_add_permission(self, request): - """Prevent addition of new NotificationEntry objects via the admin interface.""" - return False - - def has_change_permission(self, request, obj=None): - """Prevent modification of NotificationEntry objects via the admin interface.""" - return False - @admin.register(common.models.NotificationMessage) -class NotificationMessageAdmin(admin.ModelAdmin): +class NotificationMessageAdmin(NoEditAdminMixin, admin.ModelAdmin): """Admin settings for NotificationMessage - view and delete only.""" list_display = ( @@ -232,46 +209,18 @@ class NotificationMessageAdmin(admin.ModelAdmin): autocomplete_fields = ('user',) - def has_add_permission(self, request): - """Prevent addition of new NotificationMessage objects via the admin interface.""" - return False - - def has_change_permission(self, request, obj=None): - """Prevent modification of NotificationMessage objects via the admin interface.""" - return False - @admin.register(common.models.NewsFeedEntry) -class NewsFeedEntryAdmin(admin.ModelAdmin): +class NewsFeedEntryAdmin(NoEditAdminMixin, admin.ModelAdmin): """Admin settings for NewsFeedEntry - view and delete only.""" list_display = ('title', 'author', 'published', 'summary') search_fields = ('title', 'author', 'summary') - def has_add_permission(self, request): - """Prevent addition of new NewsFeedEntry objects via the admin interface.""" - return False - def has_change_permission(self, request, obj=None): - """Prevent modification of NewsFeedEntry objects via the admin interface.""" - return False - - -class ReadOnlyAdmin(admin.ModelAdmin): - """Base admin class that prevents all modifications.""" - - def has_add_permission(self, request): - """Prevent addition of new objects via the admin interface.""" - return False - - def has_change_permission(self, request, obj=None): - """Prevent modification of objects via the admin interface.""" - return False - - def has_delete_permission(self, request, obj=None): - """Prevent deletion of objects via the admin interface.""" - return False +class ReadOnlyAdmin(ReadOnlyAdminMixin, admin.ModelAdmin): + """Admin class that makes the model read-only.""" admin.site.register(common.models.WebhookMessage, ReadOnlyAdmin) diff --git a/src/backend/InvenTree/common/admin_shared.py b/src/backend/InvenTree/common/admin_shared.py new file mode 100644 index 0000000000..45e0075d8c --- /dev/null +++ b/src/backend/InvenTree/common/admin_shared.py @@ -0,0 +1,25 @@ +"""Helpers for controlling admin interface permissions.""" + + +class NoAddAdminMixin: + """Mixin to prevent addition of new objects via the admin interface.""" + + def has_add_permission(self, request, obj=None): + """Prevent addition of new objects via the admin interface.""" + return False + + +class NoEditAdminMixin(NoAddAdminMixin): + """Base admin class that prevents editing of objects.""" + + def has_change_permission(self, request, obj=None): + """Prevent modification of objects via the admin interface.""" + return False + + +class ReadOnlyAdminMixin(NoEditAdminMixin): + """Base admin class that prevents all modifications.""" + + def has_delete_permission(self, request, obj=None): + """Prevent deletion of objects via the admin interface.""" + return False diff --git a/src/backend/InvenTree/machine/admin.py b/src/backend/InvenTree/machine/admin.py index 79487d951b..be8cdac9ae 100755 --- a/src/backend/InvenTree/machine/admin.py +++ b/src/backend/InvenTree/machine/admin.py @@ -2,20 +2,17 @@ from django.contrib import admin +from common.admin_shared import NoAddAdminMixin from machine import models -class MachineSettingInline(admin.TabularInline): +class MachineSettingInline(NoAddAdminMixin, admin.TabularInline): """Inline admin class for MachineSetting.""" model = models.MachineSetting readonly_fields = ['key', 'config_type'] - def has_add_permission(self, request, obj): - """The machine settings should not be meddled with manually.""" - return False - @admin.register(models.MachineConfig) class MachineConfigAdmin(admin.ModelAdmin): diff --git a/src/backend/InvenTree/plugin/admin.py b/src/backend/InvenTree/plugin/admin.py index 7162f46df6..ff88df7de3 100644 --- a/src/backend/InvenTree/plugin/admin.py +++ b/src/backend/InvenTree/plugin/admin.py @@ -2,6 +2,7 @@ from django.contrib import admin +from common.admin_shared import NoAddAdminMixin from plugin import models from plugin.registry import registry as pl_registry @@ -34,29 +35,21 @@ def plugin_deactivate(modeladmin, request, queryset): plugin_update(queryset, False) -class PluginSettingInline(admin.TabularInline): +class PluginSettingInline(NoAddAdminMixin, admin.TabularInline): """Inline admin class for PluginSetting.""" model = models.PluginSetting readonly_fields = ['key'] - def has_add_permission(self, request, obj): - """The plugin settings should not be meddled with manually.""" - return False - -class PluginUserSettingInline(admin.TabularInline): +class PluginUserSettingInline(NoAddAdminMixin, admin.TabularInline): """Inline admin class for PluginUserSetting.""" model = models.PluginUserSetting readonly_fields = ['key'] - def has_add_permission(self, request, obj): - """The plugin user settings should not be meddled with manually.""" - return False - class PluginConfigAdmin(admin.ModelAdmin): """Custom admin with restricted id fields.""" diff --git a/src/backend/InvenTree/stock/admin.py b/src/backend/InvenTree/stock/admin.py index 15d3b8b1bd..5326f5cc33 100644 --- a/src/backend/InvenTree/stock/admin.py +++ b/src/backend/InvenTree/stock/admin.py @@ -3,6 +3,8 @@ from django.contrib import admin from django.db.models import Count +from common.admin_shared import ReadOnlyAdminMixin + from .models import ( StockItem, StockItemTestResult, @@ -77,7 +79,7 @@ class StockItemAdmin(admin.ModelAdmin): @admin.register(StockItemTracking) -class StockTrackingAdmin(admin.ModelAdmin): +class StockTrackingAdmin(ReadOnlyAdminMixin, admin.ModelAdmin): """Admin class for StockTracking - read-only to preserve audit trail integrity.""" list_display = ('item', 'date', 'label') @@ -86,18 +88,6 @@ class StockTrackingAdmin(admin.ModelAdmin): autocomplete_fields = ['item'] - def has_add_permission(self, request): - """Prevent addition of new tracking entries via the admin interface.""" - return False - - def has_change_permission(self, request, obj=None): - """Prevent modification of tracking entries via the admin interface.""" - return False - - def has_delete_permission(self, request, obj=None): - """Prevent deletion of tracking entries via the admin interface.""" - return False - @admin.register(StockItemTestResult) class StockItemTestResultAdmin(admin.ModelAdmin):