Coverage for users/models/preferences.py: 21%

65 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1from django.db import models 

2from django.utils.translation import gettext_lazy as _ 

3 

4from netbox.config import get_config 

5from utilities.data import flatten_dict 

6 

7__all__ = ( 

8 'UserConfig', 

9) 

10 

11 

12class UserConfig(models.Model): 

13 """ 

14 This model stores arbitrary user-specific preferences in a JSON data structure. 

15 """ 

16 user = models.OneToOneField( 

17 to='users.User', 

18 on_delete=models.CASCADE, 

19 related_name='config' 

20 ) 

21 data = models.JSONField( 

22 default=dict 

23 ) 

24 

25 _netbox_private = True 

26 

27 class Meta: 

28 ordering = ['user'] 

29 verbose_name = _('user preferences') 

30 verbose_name_plural = _('user preferences') 

31 

32 def get(self, path, default=None): 

33 """ 

34 Retrieve a configuration parameter specified by its dotted path. Example: 

35 

36 userconfig.get('foo.bar.baz') 

37 

38 :param path: Dotted path to the configuration key. For example, 'foo.bar' returns self.data['foo']['bar']. 

39 :param default: Default value to return for a nonexistent key (default: None). 

40 """ 

41 d = self.data 

42 keys = path.split('.') 

43 

44 # Iterate down the hierarchy, returning the default value if any invalid key is encountered 

45 try: 

46 for key in keys: 

47 d = d[key] 

48 return d 

49 except (TypeError, KeyError): 

50 pass 

51 

52 # If the key is not found in the user's config, check for an application-wide default 

53 config = get_config() 

54 d = config.DEFAULT_USER_PREFERENCES 

55 try: 

56 for key in keys: 

57 d = d[key] 

58 return d 

59 except (TypeError, KeyError): 

60 pass 

61 

62 # Finally, return the specified default value (if any) 

63 return default 

64 

65 def all(self): 

66 """ 

67 Return a dictionary of all defined keys and their values. 

68 """ 

69 return flatten_dict(self.data) 

70 

71 def set(self, path, value, commit=False): 

72 """ 

73 Define or overwrite a configuration parameter. Example: 

74 

75 userconfig.set('foo.bar.baz', 123) 

76 

77 Leaf nodes (those which are not dictionaries of other nodes) cannot be overwritten as dictionaries. Similarly, 

78 branch nodes (dictionaries) cannot be overwritten as single values. (A TypeError exception will be raised.) In 

79 both cases, the existing key must first be cleared. This safeguard is in place to help avoid inadvertently 

80 overwriting the wrong key. 

81 

82 :param path: Dotted path to the configuration key. For example, 'foo.bar' sets self.data['foo']['bar']. 

83 :param value: The value to be written. This can be any type supported by JSON. 

84 :param commit: If true, the UserConfig instance will be saved once the new value has been applied. 

85 """ 

86 d = self.data 

87 keys = path.split('.') 

88 

89 # Iterate through the hierarchy to find the key we're setting. Raise TypeError if we encounter any 

90 # interim leaf nodes (keys which do not contain dictionaries). 

91 for i, key in enumerate(keys[:-1]): 

92 if key in d and type(d[key]) is dict: 

93 d = d[key] 

94 elif key in d: 

95 err_path = '.'.join(path.split('.')[:i + 1]) 

96 raise TypeError( 

97 _("Key '{path}' is a leaf node; cannot assign new keys").format(path=err_path) 

98 ) 

99 else: 

100 d = d.setdefault(key, {}) 

101 

102 # Set a key based on the last item in the path. Raise TypeError if attempting to overwrite a non-leaf node. 

103 key = keys[-1] 

104 if key in d and type(d[key]) is dict: 

105 if type(value) is dict: 

106 d[key].update(value) 

107 else: 

108 raise TypeError( 

109 _("Key '{path}' is a dictionary; cannot assign a non-dictionary value").format(path=path) 

110 ) 

111 else: 

112 d[key] = value 

113 

114 if commit: 

115 self.save() 

116 set.alters_data = True 

117 

118 def clear(self, path, commit=False): 

119 """ 

120 Delete a configuration parameter specified by its dotted path. The key and any child keys will be deleted. 

121 Example: 

122 

123 userconfig.clear('foo.bar.baz') 

124 

125 Invalid keys will be ignored silently. 

126 

127 :param path: Dotted path to the configuration key. For example, 'foo.bar' deletes self.data['foo']['bar']. 

128 :param commit: If true, the UserConfig instance will be saved once the new value has been applied. 

129 """ 

130 d = self.data 

131 keys = path.split('.') 

132 

133 for key in keys[:-1]: 

134 if key not in d: 

135 break 

136 if type(d[key]) is dict: 

137 d = d[key] 

138 

139 key = keys[-1] 

140 d.pop(key, None) # Avoid a KeyError on invalid keys 

141 

142 if commit: 

143 self.save() 

144 clear.alters_data = True