Test Failed
Pull Request — master (#4023)
by W
03:56
created

ChangeRevisionFieldMixin.get_indexes()   A

Complexity

Conditions 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
from __future__ import absolute_import
17
import abc
18
import datetime
19
20
import bson
21
import six
22
import mongoengine as me
23
from oslo_config import cfg
24
25
from st2common.util import mongoescape
26
from st2common.models.base import DictSerializableClassMixin
27
from st2common.models.system.common import ResourceReference
28
from st2common.constants.types import ResourceType
29
30
__all__ = [
31
    'StormFoundationDB',
32
    'StormBaseDB',
33
34
    'EscapedDictField',
35
    'EscapedDynamicField',
36
    'TagField',
37
38
    'RefFieldMixin',
39
    'UIDFieldMixin',
40
    'TagsMixin',
41
    'ContentPackResourceMixin'
42
]
43
44
JSON_UNFRIENDLY_TYPES = (datetime.datetime, bson.ObjectId, me.EmbeddedDocument)
45
46
47
class StormFoundationDB(me.Document, DictSerializableClassMixin):
48
    """
49
    Base abstraction for a model entity. This foundation class should only be directly
50
    inherited from the application domain models.
51
    """
52
53
    # Variable representing a type of this resource
54
    RESOURCE_TYPE = ResourceType.UNKNOWN
55
56
    # We explicitly assign the manager so pylint know what type objects is
57
    objects = me.queryset.QuerySetManager()
58
59
    # Note: In mongoengine >= 0.10 "id" field is automatically declared on all
60
    # the documents and declaring it ourselves causes a lot of issues so we
61
    # don't do that
62
63
    # see http://docs.mongoengine.org/guide/defining-documents.html#abstract-classes
64
    meta = {
65
        'abstract': True
66
    }
67
68
    def __str__(self):
69
        attrs = list()
70
        for k in sorted(self._fields.keys()):
71
            v = getattr(self, k)
72
            v = '"%s"' % str(v) if type(v) in [str, six.text_type, datetime.datetime] else str(v)
73
            attrs.append('%s=%s' % (k, v))
74
        return '%s(%s)' % (self.__class__.__name__, ', '.join(attrs))
75
76
    def get_resource_type(self):
77
        return self.RESOURCE_TYPE
78
79
    def mask_secrets(self, value):
80
        """
81
        Process the model dictionary and mask secret values.
82
83
        :type value: ``dict``
84
        :param value: Document dictionary.
85
86
        :rtype: ``dict``
87
        """
88
        return value
89
90
    def to_serializable_dict(self, mask_secrets=False):
91
        """
92
        Serialize database model to a dictionary.
93
94
        :param mask_secrets: True to mask secrets in the resulting dict.
95
        :type mask_secrets: ``boolean``
96
97
        :rtype: ``dict``
98
        """
99
        serializable_dict = {}
100
        for k in sorted(six.iterkeys(self._fields)):
101
            v = getattr(self, k)
102
            v = str(v) if isinstance(v, JSON_UNFRIENDLY_TYPES) else v
103
            serializable_dict[k] = v
104
105
        if mask_secrets and cfg.CONF.log.mask_secrets:
106
            serializable_dict = self.mask_secrets(value=serializable_dict)
107
108
        return serializable_dict
109
110
111
class StormBaseDB(StormFoundationDB):
112
    """Abstraction for a user content model."""
113
114
    name = me.StringField(required=True, unique=True)
115
    description = me.StringField()
116
117
    # see http://docs.mongoengine.org/guide/defining-documents.html#abstract-classes
118
    meta = {
119
        'abstract': True
120
    }
121
122
123
class EscapedDictField(me.DictField):
124
125
    def to_mongo(self, value, use_db_field=True, fields=None):
126
        value = mongoescape.escape_chars(value)
127
        return super(EscapedDictField, self).to_mongo(value=value, use_db_field=use_db_field,
128
                                                      fields=fields)
129
130
    def to_python(self, value):
131
        value = super(EscapedDictField, self).to_python(value)
132
        return mongoescape.unescape_chars(value)
133
134
    def validate(self, value):
135
        if not isinstance(value, dict):
136
            self.error('Only dictionaries may be used in a DictField')
137
        if me.fields.key_not_string(value):
138
            self.error("Invalid dictionary key - documents must have only string keys")
139
        me.base.ComplexBaseField.validate(self, value)
140
141
142
class EscapedDynamicField(me.DynamicField):
143
144
    def to_mongo(self, value, use_db_field=True, fields=None):
145
        value = mongoescape.escape_chars(value)
146
        return super(EscapedDynamicField, self).to_mongo(value=value, use_db_field=use_db_field,
147
                                                         fields=fields)
148
149
    def to_python(self, value):
150
        value = super(EscapedDynamicField, self).to_python(value)
151
        return mongoescape.unescape_chars(value)
152
153
154
class TagField(me.EmbeddedDocument):
155
    """
156
    To be attached to a db model object for the purpose of providing supplemental
157
    information.
158
    """
159
    name = me.StringField(max_length=1024)
160
    value = me.StringField(max_length=1024)
161
162
163
class TagsMixin(object):
164
    """
165
    Mixin to include tags on an object.
166
    """
167
    tags = me.ListField(field=me.EmbeddedDocumentField(TagField))
168
169
    @classmethod
170
    def get_indices(cls):
171
        return ['tags.name', 'tags.value']
172
173
174
class RefFieldMixin(object):
175
    """
176
    Mixin class which adds "ref" field to the class inheriting from it.
177
    """
178
179
    ref = me.StringField(required=True, unique=True)
180
181
182
class UIDFieldMixin(object):
183
    """
184
    Mixin class which adds "uid" field to the class inheriting from it.
185
186
    UID field is a unique identifier which we can be used to unambiguously reference a resource in
187
    the system.
188
    """
189
190
    UID_SEPARATOR = ':'  # TODO: Move to constants
191
192
    RESOURCE_TYPE = abc.abstractproperty
193
    UID_FIELDS = abc.abstractproperty
194
195
    uid = me.StringField(required=True)
196
197
    @classmethod
198
    def get_indexes(cls):
199
        # Note: We use a special sparse index so we don't need to pre-populate "uid" for existing
200
        # models in the database before ensure_indexes() is called.
201
        # This field gets populated in the constructor which means it will be lazily assigned next
202
        # time the model is saved (e.g. once register-content is ran).
203
        indexes = [
204
            {
205
                'fields': ['uid'],
206
                'unique': True,
207
                'sparse': True
208
            }
209
        ]
210
        return indexes
211
212
    def get_uid(self):
213
        """
214
        Return an object UID constructed from the object properties / fields.
215
216
        :rtype: ``str``
217
        """
218
        parts = []
219
        parts.append(self.RESOURCE_TYPE)
220
221
        for field in self.UID_FIELDS:
222
            value = getattr(self, field, None) or ''
223
            parts.append(value)
224
225
        uid = self.UID_SEPARATOR.join(parts)
226
        return uid
227
228
    def get_uid_parts(self):
229
        """
230
        Return values for fields which make up the UID.
231
232
        :rtype: ``list``
233
        """
234
        parts = self.uid.split(self.UID_SEPARATOR)  # pylint: disable=no-member
235
        parts = [part for part in parts if part.strip()]
236
        return parts
237
238
    def has_valid_uid(self):
239
        """
240
        Return True if object contains a valid id (aka all parts contain a valid value).
241
242
        :rtype: ``bool``
243
        """
244
        parts = self.get_uid_parts()
245
        return len(parts) == len(self.UID_FIELDS) + 1
246
247
248
class ContentPackResourceMixin(object):
249
    """
250
    Mixin class provides utility methods for models which belong to a pack.
251
    """
252
253
    def get_pack_uid(self):
254
        """
255
        Return an UID of a pack this resource belongs to.
256
257
        :rtype ``str``
258
        """
259
        parts = [ResourceType.PACK, self.pack]
260
        uid = UIDFieldMixin.UID_SEPARATOR.join(parts)
261
        return uid
262
263
    def get_reference(self):
264
        """
265
        Retrieve referene object for this model.
266
267
        :rtype: :class:`ResourceReference`
268
        """
269
        if getattr(self, 'ref', None):
270
            ref = ResourceReference.from_string_reference(ref=self.ref)
271
        else:
272
            ref = ResourceReference(pack=self.pack, name=self.name)
273
274
        return ref
275
276
277
class ChangeRevisionFieldMixin(object):
278
279
    rev = me.IntField(required=True, default=1)
280
281
    @classmethod
282
    def get_indexes(cls):
283
        return [
284
            {
285
                'fields': ['id', 'rev'],
286
                'unique': True
287
            }
288
        ]
289