• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

inventree / InvenTree / 5890077779

17 Aug 2023 11:04AM UTC coverage: 88.555% (-0.02%) from 88.571%
5890077779

push

github

web-flow
Fix plugin pickeling (#5412) (#5457)

(cherry picked from commit 1fe382e31)

Co-authored-by: Lukas <76838159+wolflu05@users.noreply.github.com>

3 of 3 new or added lines in 1 file covered. (100.0%)

26850 of 30320 relevant lines covered (88.56%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.14
/InvenTree/plugin/models.py
1
"""Plugin model definitions."""
2

3
import inspect
4
import warnings
5

6
from django.conf import settings
7
from django.contrib import admin
8
from django.contrib.auth.models import User
9
from django.db import models
10
from django.utils.translation import gettext_lazy as _
11

12
import common.models
13
import InvenTree.models
14
from plugin import InvenTreePlugin, registry
15

16

17
class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
18
    """A PluginConfig object holds settings for plugins.
19

20
    Attributes:
21
        key: slug of the plugin (this must be unique across all installed plugins!)
22
        name: PluginName of the plugin - serves for a manual double check  if the right plugin is used
23
        active: Should the plugin be loaded?
24
    """
25

26
    class Meta:
1✔
27
        """Meta for PluginConfig."""
28
        verbose_name = _("Plugin Configuration")
29
        verbose_name_plural = _("Plugin Configurations")
30

31
    key = models.CharField(
32
        unique=True,
33
        max_length=255,
34
        verbose_name=_('Key'),
35
        help_text=_('Key of plugin'),
36
    )
37

38
    name = models.CharField(
39
        null=True,
40
        blank=True,
41
        max_length=255,
42
        verbose_name=_('Name'),
43
        help_text=_('PluginName of the plugin'),
44
    )
45

46
    active = models.BooleanField(
47
        default=False,
48
        verbose_name=_('Active'),
49
        help_text=_('Is the plugin active'),
50
    )
51

52
    def __str__(self) -> str:
53
        """Nice name for printing."""
54
        name = f'{self.name} - {self.key}'
1✔
55
        if not self.active:
1✔
56
            name += '(not active)'
1✔
57
        return name
1✔
58

59
    # extra attributes from the registry
60
    def mixins(self):
1✔
61
        """Returns all registered mixins."""
62
        try:
63
            if inspect.isclass(self.plugin):
64
                return self.plugin.get_registered_mixins(self, with_base=True, with_cls=False)
65
            return self.plugin.get_registered_mixins(with_base=True, with_cls=False)
66
        except (AttributeError, ValueError):  # pragma: no cover
67
            return {}
68

69
    # functions
70

71
    def __init__(self, *args, **kwargs):
72
        """Override to set original state of the plugin-config instance."""
73
        super().__init__(*args, **kwargs)
1✔
74
        self.__org_active = self.active
1✔
75

76
        # Append settings from registry
77
        plugin = registry.plugins_full.get(self.key, None)
1✔
78

79
        def get_plugin_meta(name):
1✔
80
            """Return a meta-value associated with this plugin"""
81

82
            # Ignore if the plugin config is not defined
83
            if not plugin:
84
                return None
85

86
            # Ignore if the plugin is not active
87
            if not self.active:
88
                return None
89

90
            result = getattr(plugin, name, None)
91

92
            if result is not None:
93
                result = str(result)
94

95
            return result
96

97
        self.meta = {
98
            key: get_plugin_meta(key) for key in ['slug', 'human_name', 'description', 'author',
99
                                                  'pub_date', 'version', 'website', 'license',
100
                                                  'package_path', 'settings_url', ]
101
        }
102

103
        # Save plugin
104
        self.plugin: InvenTreePlugin = plugin
105

106
    def __getstate__(self):
107
        """Customize pickeling behaviour."""
108
        state = super().__getstate__()
1✔
109
        state.pop("plugin", None)  # plugin cannot be pickelt in some circumstances when used with drf views, remove it (#5408)
1✔
110
        return state
1✔
111

112
    def save(self, force_insert=False, force_update=False, *args, **kwargs):
1✔
113
        """Extend save method to reload plugins if the 'active' status changes."""
114
        reload = kwargs.pop('no_reload', False)  # check if no_reload flag is set
115

116
        ret = super().save(force_insert, force_update, *args, **kwargs)
117

118
        if self.is_builtin():
119
            # Force active if builtin
120
            self.active = True
121

122
        if not reload:
123
            if (self.active is False and self.__org_active is True) or \
124
               (self.active is True and self.__org_active is False):
125
                if settings.PLUGIN_TESTING:
126
                    warnings.warn('A reload was triggered', stacklevel=2)
127
                registry.reload_plugins()
128

129
        return ret
130

131
    @admin.display(boolean=True, description=_('Sample plugin'))
132
    def is_sample(self) -> bool:
133
        """Is this plugin a sample app?"""
134

135
        if not self.plugin:
1✔
136
            return False
×
137

138
        return self.plugin.check_is_sample()
1✔
139

140
    @admin.display(boolean=True, description=_('Builtin Plugin'))
1✔
141
    def is_builtin(self) -> bool:
1✔
142
        """Return True if this is a 'builtin' plugin"""
143

144
        if not self.plugin:
145
            return False
146

147
        return self.plugin.check_is_builtin()
148

149

150
class PluginSetting(common.models.BaseInvenTreeSetting):
151
    """This model represents settings for individual plugins."""
152

153
    typ = 'plugin'
1✔
154
    extra_unique_fields = ['plugin']
1✔
155

156
    class Meta:
1✔
157
        """Meta for PluginSetting."""
158
        unique_together = [
159
            ('plugin', 'key'),
160
        ]
161

162
    plugin = models.ForeignKey(
163
        PluginConfig,
164
        related_name='settings',
165
        null=False,
166
        verbose_name=_('Plugin'),
167
        on_delete=models.CASCADE,
168
    )
169

170
    @classmethod
171
    def get_setting_definition(cls, key, **kwargs):
172
        """In the BaseInvenTreeSetting class, we have a class attribute named 'SETTINGS', which is a dict object that fully defines all the setting parameters.
173

174
        Here, unlike the BaseInvenTreeSetting, we do not know the definitions of all settings
175
        'ahead of time' (as they are defined externally in the plugins).
176

177
        Settings can be provided by the caller, as kwargs['settings'].
178

179
        If not provided, we'll look at the plugin registry to see what settings are available,
180
        (if the plugin is specified!)
181
        """
182
        if 'settings' not in kwargs:
1✔
183

184
            plugin = kwargs.pop('plugin', None)
1✔
185

186
            if plugin:
1✔
187
                mixin_settings = getattr(registry, 'mixins_settings')
1✔
188
                if mixin_settings:
1✔
189
                    kwargs['settings'] = mixin_settings.get(plugin.key, {})
1✔
190

191
        return super().get_setting_definition(key, **kwargs)
1✔
192

193

194
class NotificationUserSetting(common.models.BaseInvenTreeSetting):
1✔
195
    """This model represents notification settings for a user."""
196

197
    typ = 'notification'
198
    extra_unique_fields = ['method', 'user']
199

200
    class Meta:
201
        """Meta for NotificationUserSetting."""
202
        unique_together = [
1✔
203
            ('method', 'user', 'key'),
1✔
204
        ]
205

206
    @classmethod
1✔
207
    def get_setting_definition(cls, key, **kwargs):
1✔
208
        """Override setting_definition to use notification settings."""
209
        from common.notifications import storage
210

211
        kwargs['settings'] = storage.user_settings
212

213
        return super().get_setting_definition(key, **kwargs)
214

215
    method = models.CharField(
216
        max_length=255,
217
        verbose_name=_('Method'),
218
    )
219

220
    user = models.ForeignKey(
221
        User,
222
        on_delete=models.CASCADE,
223
        blank=True, null=True,
224
        verbose_name=_('User'),
225
        help_text=_('User'),
226
    )
227

228
    def __str__(self) -> str:
229
        """Nice name of printing."""
230
        return f'{self.key} (for {self.user}): {self.value}'
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc