File: managers.py

package info (click to toggle)
django-dynamic-preferences 1.17.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 476 kB
  • sloc: python: 3,040; makefile: 3
file content (249 lines) | stat: -rw-r--r-- 8,484 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
try:
    from collections.abc import Mapping
except ImportError:
    from collections import Mapping

from .settings import preferences_settings
from .exceptions import CachedValueNotFound, DoesNotExist
from .signals import preference_updated


class PreferencesManager(Mapping):

    """Handle retrieving / caching of preferences"""

    def __init__(self, model, registry, **kwargs):
        self.model = model
        self.registry = registry
        self.instance = kwargs.get("instance")

    @property
    def queryset(self):
        qs = self.model.objects.all()
        if self.instance:
            qs = qs.filter(instance=self.instance)
        return qs

    @property
    def cache(self):
        from django.core.cache import caches

        return caches[preferences_settings.CACHE_NAME]

    def __getitem__(self, key):
        return self.get(key)

    def __setitem__(self, key, value):
        section, name = self.parse_lookup(key)
        preference = self.registry.get(section=section, name=name, fallback=False)
        preference.validate(value)
        self.update_db_pref(section=section, name=name, value=value)

    def __repr__(self):
        return repr(self.all())

    def __iter__(self):
        return self.all().__iter__()

    def __len__(self):
        return len(self.all())

    def by_name(self):
        """Return a dictionary with preferences identifiers and values, but without the section name in the identifier"""
        return {
            key.split(preferences_settings.SECTION_KEY_SEPARATOR)[-1]: value
            for key, value in self.all().items()
        }

    def get_by_name(self, name):
        return self.get(self.registry.get_by_name(name).identifier())

    def get_cache_key(self, section, name):
        """Return the cache key corresponding to a given preference"""
        if not self.instance:
            return "dynamic_preferences_{0}_{1}_{2}".format(
                self.model.__name__, section, name
            )
        return "dynamic_preferences_{0}_{1}_{2}_{3}".format(
            self.model.__name__, self.instance.pk, section, name, self.instance.pk
        )

    def from_cache(self, section, name):
        """Return a preference raw_value from cache"""
        cached_value = self.cache.get(
            self.get_cache_key(section, name), CachedValueNotFound
        )

        if cached_value is CachedValueNotFound:
            raise CachedValueNotFound

        if cached_value == preferences_settings.CACHE_NONE_VALUE:
            cached_value = None
        return self.registry.get(section=section, name=name).serializer.deserialize(
            cached_value
        )

    def many_from_cache(self, preferences):
        """
        Return cached value for given preferences
        missing preferences will be skipped
        """
        keys = {p: self.get_cache_key(p.section.name, p.name) for p in preferences}
        cached = self.cache.get_many(list(keys.values()))

        for k, v in cached.items():
            # we replace dummy cached values by None here, if needed
            if v == preferences_settings.CACHE_NONE_VALUE:
                cached[k] = None

        # we have to remap returned value since the underlying cached keys
        # are not usable for an end user
        return {
            p.identifier(): p.serializer.deserialize(cached[k])
            for p, k in keys.items()
            if k in cached
        }

    def to_cache(self, *prefs):
        """
        Update/create the cache value for the given preference model instances
        """
        update_dict = {}
        for pref in prefs:
            key = self.get_cache_key(pref.section, pref.name)
            value = pref.raw_value
            if value is None or value == "":
                # some cache backends refuse to cache None or empty values
                # resulting in more DB queries, so we cache an arbitrary value
                # to ensure the cache is hot (even with empty values)
                value = preferences_settings.CACHE_NONE_VALUE
            update_dict[key] = value

        self.cache.set_many(update_dict)

    def pref_obj(self, section, name):
        return self.registry.get(section=section, name=name)

    def parse_lookup(self, lookup):
        try:
            section, name = lookup.split(preferences_settings.SECTION_KEY_SEPARATOR)
        except ValueError:
            name = lookup
            section = None
        return section, name

    def get(self, key, no_cache=False):
        """Return the value of a single preference using a dotted path key
        :arg no_cache: if true, the cache is bypassed
        """
        section, name = self.parse_lookup(key)
        preference = self.registry.get(section=section, name=name, fallback=False)
        if no_cache or not preferences_settings.ENABLE_CACHE:
            return self.get_db_pref(section=section, name=name).value

        try:
            return self.from_cache(section, name)
        except CachedValueNotFound:
            pass

        db_pref = self.get_db_pref(section=section, name=name)
        self.to_cache(db_pref)
        return db_pref.value

    def get_db_pref(self, section, name):
        try:
            pref = self.queryset.get(section=section, name=name)
        except self.model.DoesNotExist:
            pref_obj = self.pref_obj(section=section, name=name)
            pref = self.create_db_pref(
                section=section, name=name, value=pref_obj.get("default")
            )

        return pref

    def update_db_pref(self, section, name, value):
        try:
            db_pref = self.queryset.get(section=section, name=name)
            old_value = db_pref.value
            db_pref.value = value
            db_pref.save()
            preference_updated.send(
                sender=self.__class__,
                section=section,
                name=name,
                old_value=old_value,
                new_value=value,
                instance=db_pref
            )
        except self.model.DoesNotExist:
            return self.create_db_pref(section, name, value)

        return db_pref

    def create_db_pref(self, section, name, value):
        kwargs = {
            "section": section,
            "name": name,
        }
        if self.instance:
            kwargs["instance"] = self.instance

        # this is a just a shortcut to get the raw, serialized value
        # so we can pass it to get_or_create
        m = self.model(**kwargs)
        m.value = value
        raw_value = m.raw_value

        db_pref, created = self.model.objects.get_or_create(**kwargs)
        if created and db_pref.raw_value != raw_value:
            db_pref.raw_value = raw_value
            db_pref.save()

        return db_pref

    def all(self):
        """Return a dictionary containing all preferences by section
        Loaded from cache or from db in case of cold cache
        """
        if not preferences_settings.ENABLE_CACHE:
            return self.load_from_db()

        preferences = self.registry.preferences()

        # first we hit the cache once for all existing preferences
        a = self.many_from_cache(preferences)
        if len(a) == len(preferences):
            return a  # avoid database hit if not necessary

        # then we fill those that miss, but exist in the database
        # (just hit the database for all of them, filtering is complicated, and
        # in most cases you'd need to grab the majority of them anyway)
        a.update(self.load_from_db(cache=True))
        return a

    def load_from_db(self, cache=False):
        """Return a dictionary of preferences by section directly from DB"""
        a = {}
        db_prefs = {p.preference.identifier(): p for p in self.queryset}
        cache_prefs = []

        for preference in self.registry.preferences():
            try:
                db_pref = db_prefs[preference.identifier()]
            except KeyError:
                db_pref = self.create_db_pref(
                    section=preference.section.name,
                    name=preference.name,
                    value=preference.get("default"),
                )
            else:
                # cache if create_db_pref() hasn't already done so
                if cache:
                    cache_prefs.append(db_pref)

            a[preference.identifier()] = db_pref.value

        if cache_prefs:
            self.to_cache(*cache_prefs)

        return a