@sw-tsdk/plugin-connector 3.13.1 → 3.13.2-next.3dfd44a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -18
- package/lib/commands/connector/build.js +168 -44
- package/lib/commands/connector/build.js.map +1 -1
- package/lib/commands/connector/sign.js +108 -12
- package/lib/commands/connector/sign.js.map +1 -1
- package/lib/commands/connector/validate.js +110 -10
- package/lib/commands/connector/validate.js.map +1 -1
- package/lib/commands/migrator/convert.d.ts +3 -0
- package/lib/commands/migrator/convert.js +201 -20
- package/lib/commands/migrator/convert.js.map +1 -1
- package/lib/templates/migrator-runners/plugin_override.txt +76 -4
- package/lib/templates/migrator-runners/runner_override.txt +30 -0
- package/lib/templates/migrator-runners/script_override.txt +77 -5
- package/lib/templates/swimlane/__init__.py +18 -0
- package/lib/templates/swimlane/core/__init__.py +0 -0
- package/lib/templates/swimlane/core/adapters/__init__.py +10 -0
- package/lib/templates/swimlane/core/adapters/app.py +59 -0
- package/lib/templates/swimlane/core/adapters/app_revision.py +49 -0
- package/lib/templates/swimlane/core/adapters/helper.py +84 -0
- package/lib/templates/swimlane/core/adapters/record.py +468 -0
- package/lib/templates/swimlane/core/adapters/record_revision.py +43 -0
- package/lib/templates/swimlane/core/adapters/report.py +65 -0
- package/lib/templates/swimlane/core/adapters/task.py +58 -0
- package/lib/templates/swimlane/core/adapters/usergroup.py +183 -0
- package/lib/templates/swimlane/core/bulk.py +48 -0
- package/lib/templates/swimlane/core/cache.py +165 -0
- package/lib/templates/swimlane/core/client.py +466 -0
- package/lib/templates/swimlane/core/cursor.py +100 -0
- package/lib/templates/swimlane/core/fields/__init__.py +46 -0
- package/lib/templates/swimlane/core/fields/attachment.py +82 -0
- package/lib/templates/swimlane/core/fields/base/__init__.py +15 -0
- package/lib/templates/swimlane/core/fields/base/cursor.py +90 -0
- package/lib/templates/swimlane/core/fields/base/field.py +149 -0
- package/lib/templates/swimlane/core/fields/base/multiselect.py +116 -0
- package/lib/templates/swimlane/core/fields/comment.py +48 -0
- package/lib/templates/swimlane/core/fields/datetime.py +112 -0
- package/lib/templates/swimlane/core/fields/history.py +28 -0
- package/lib/templates/swimlane/core/fields/list.py +266 -0
- package/lib/templates/swimlane/core/fields/number.py +38 -0
- package/lib/templates/swimlane/core/fields/reference.py +169 -0
- package/lib/templates/swimlane/core/fields/text.py +30 -0
- package/lib/templates/swimlane/core/fields/tracking.py +10 -0
- package/lib/templates/swimlane/core/fields/usergroup.py +137 -0
- package/lib/templates/swimlane/core/fields/valueslist.py +70 -0
- package/lib/templates/swimlane/core/resolver.py +46 -0
- package/lib/templates/swimlane/core/resources/__init__.py +0 -0
- package/lib/templates/swimlane/core/resources/app.py +136 -0
- package/lib/templates/swimlane/core/resources/app_revision.py +43 -0
- package/lib/templates/swimlane/core/resources/attachment.py +64 -0
- package/lib/templates/swimlane/core/resources/base.py +55 -0
- package/lib/templates/swimlane/core/resources/comment.py +33 -0
- package/lib/templates/swimlane/core/resources/record.py +499 -0
- package/lib/templates/swimlane/core/resources/record_revision.py +44 -0
- package/lib/templates/swimlane/core/resources/report.py +259 -0
- package/lib/templates/swimlane/core/resources/revision_base.py +69 -0
- package/lib/templates/swimlane/core/resources/task.py +16 -0
- package/lib/templates/swimlane/core/resources/usergroup.py +166 -0
- package/lib/templates/swimlane/core/search.py +31 -0
- package/lib/templates/swimlane/core/wrappedsession.py +12 -0
- package/lib/templates/swimlane/exceptions.py +191 -0
- package/lib/templates/swimlane/utils/__init__.py +132 -0
- package/lib/templates/swimlane/utils/date_validator.py +4 -0
- package/lib/templates/swimlane/utils/list_validator.py +7 -0
- package/lib/templates/swimlane/utils/str_validator.py +10 -0
- package/lib/templates/swimlane/utils/version.py +101 -0
- package/lib/transformers/base-transformer.js +61 -14
- package/lib/transformers/base-transformer.js.map +1 -1
- package/lib/transformers/connector-generator.d.ts +104 -2
- package/lib/transformers/connector-generator.js +1234 -51
- package/lib/transformers/connector-generator.js.map +1 -1
- package/lib/types/migrator-types.d.ts +22 -0
- package/lib/types/migrator-types.js.map +1 -1
- package/oclif.manifest.json +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from collections.abc import defaultdict
|
|
3
|
+
except ImportError:
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from numbers import Number
|
|
7
|
+
|
|
8
|
+
import six
|
|
9
|
+
from shortid import ShortId
|
|
10
|
+
|
|
11
|
+
from swimlane.core.fields.base import FieldCursor
|
|
12
|
+
from swimlane.exceptions import ValidationError
|
|
13
|
+
from .base import CursorField
|
|
14
|
+
|
|
15
|
+
SID = ShortId()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _ListFieldCursor(FieldCursor):
|
|
19
|
+
"""Base class for Text and Numeric FieldCursors emulating a basic list"""
|
|
20
|
+
|
|
21
|
+
def _validate_list(self, target):
|
|
22
|
+
"""Validate a list against field validation rules"""
|
|
23
|
+
# Check list length restrictions
|
|
24
|
+
min_items = self._field.field_definition.get('minItems')
|
|
25
|
+
max_items = self._field.field_definition.get('maxItems')
|
|
26
|
+
|
|
27
|
+
if min_items is not None:
|
|
28
|
+
if len(target) < min_items:
|
|
29
|
+
raise ValidationError(
|
|
30
|
+
self._record,
|
|
31
|
+
'Field "{}" must have a minimum of {} item(s)'.format(self._field.name, min_items)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if max_items is not None:
|
|
35
|
+
if len(target) > max_items:
|
|
36
|
+
raise ValidationError(
|
|
37
|
+
self._record,
|
|
38
|
+
'Field "{}" can only have a maximum of {} item(s)'.format(self._field.name, max_items)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Individual item validation
|
|
42
|
+
for item in target:
|
|
43
|
+
self._validate_item(item)
|
|
44
|
+
|
|
45
|
+
def _validate_item(self, item):
|
|
46
|
+
"""Validate individual item against field rules. Defaults to no-op"""
|
|
47
|
+
|
|
48
|
+
def __getattr__(self, item):
|
|
49
|
+
"""Fallback to any builtin list methods on self._elements for any undefined methods called on cursor
|
|
50
|
+
|
|
51
|
+
List methods are wrapped in a function ensuring the updated value passes field validation before actually
|
|
52
|
+
applying to the internal value
|
|
53
|
+
|
|
54
|
+
Wrapper function creates a copy of the current elements in the cursor, calls whatever list method was executed
|
|
55
|
+
on the copy, then validates the updated list against field rules.
|
|
56
|
+
|
|
57
|
+
If validation fails, raise ValidationError, otherwise update the cursor elements and field value to the modified
|
|
58
|
+
list copy
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
# Check for method on list class
|
|
62
|
+
func = getattr(list, item)
|
|
63
|
+
|
|
64
|
+
# Create a copy of elements if function exists to be the actual target of the method call
|
|
65
|
+
elements_copy = self._elements[:]
|
|
66
|
+
|
|
67
|
+
# Wrap in function adding validation after the method is executed
|
|
68
|
+
def wrapper(*args, **kwargs):
|
|
69
|
+
# Execute method against the copied elements
|
|
70
|
+
result = func(elements_copy, *args, **kwargs)
|
|
71
|
+
|
|
72
|
+
# Validate elements copy after any potential modification
|
|
73
|
+
self._validate_list(elements_copy)
|
|
74
|
+
|
|
75
|
+
# Update internal cursor elements to modified copy and sync with field
|
|
76
|
+
self._elements = elements_copy
|
|
77
|
+
self._sync_field()
|
|
78
|
+
|
|
79
|
+
# Return in case of methods retrieving values instead of modifying state
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
return wrapper
|
|
83
|
+
except AttributeError:
|
|
84
|
+
# Raise separate AttributeError with correct class reference instead of list
|
|
85
|
+
raise AttributeError('{} object has no attribute {}'.format(self.__class__, item))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TextListFieldCursor(_ListFieldCursor):
|
|
89
|
+
"""Cursor for Text ListField"""
|
|
90
|
+
|
|
91
|
+
def _validate_item(self, item):
|
|
92
|
+
"""Validate char/word count"""
|
|
93
|
+
if not isinstance(item, six.string_types):
|
|
94
|
+
raise ValidationError(
|
|
95
|
+
self._record,
|
|
96
|
+
'Text list field items must be strings, not "{}"'.format(item.__class__)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
words = item.split(' ')
|
|
100
|
+
|
|
101
|
+
item_length_type = self._field.field_definition.get('itemLengthType')
|
|
102
|
+
item_max_length = self._field.field_definition.get('itemMaxLength')
|
|
103
|
+
item_min_length = self._field.field_definition.get('itemMinLength')
|
|
104
|
+
if item_length_type is not None:
|
|
105
|
+
# Min/max word count
|
|
106
|
+
if item_length_type == 'words':
|
|
107
|
+
if item_max_length is not None:
|
|
108
|
+
if len(words) > item_max_length:
|
|
109
|
+
raise ValidationError(
|
|
110
|
+
self._record,
|
|
111
|
+
'Field "{}" items cannot contain more than {} words'.format(
|
|
112
|
+
self._field.name,
|
|
113
|
+
item_max_length
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
if item_min_length is not None:
|
|
117
|
+
if len(words) < item_min_length:
|
|
118
|
+
raise ValidationError(
|
|
119
|
+
self._record,
|
|
120
|
+
'Field "{}" items must contain at least {} words'.format(
|
|
121
|
+
self._field.name,
|
|
122
|
+
item_min_length
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Min/max char count of full item
|
|
127
|
+
else:
|
|
128
|
+
if item_max_length is not None:
|
|
129
|
+
if len(item) > item_max_length:
|
|
130
|
+
raise ValidationError(
|
|
131
|
+
self._record,
|
|
132
|
+
'Field "{}" items cannot contain more than {} characters'.format(
|
|
133
|
+
self._field.name,
|
|
134
|
+
item_max_length
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
if item_min_length is not None:
|
|
138
|
+
if len(item) < item_min_length:
|
|
139
|
+
raise ValidationError(
|
|
140
|
+
self._record,
|
|
141
|
+
'Field "{}" items must contain at least {} characters'.format(
|
|
142
|
+
self._field.name,
|
|
143
|
+
item_min_length
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class NumericListFieldCursor(_ListFieldCursor):
|
|
149
|
+
"""Cursor for Numeric ListField"""
|
|
150
|
+
|
|
151
|
+
def _validate_item(self, item):
|
|
152
|
+
if not isinstance(item, Number):
|
|
153
|
+
raise ValidationError(
|
|
154
|
+
self._record,
|
|
155
|
+
'Numeric list field items must be numbers, not "{}"'.format(item.__class__)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# range restrictions
|
|
159
|
+
item_max = self._field.field_definition.get('itemMax')
|
|
160
|
+
item_min = self._field.field_definition.get('itemMin')
|
|
161
|
+
|
|
162
|
+
if item_max is not None:
|
|
163
|
+
if item > item_max:
|
|
164
|
+
raise ValidationError(
|
|
165
|
+
self._record,
|
|
166
|
+
'Field "{}" items cannot be greater than {}'.format(
|
|
167
|
+
self._field.name,
|
|
168
|
+
item_max
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if item_min is not None:
|
|
173
|
+
if item < item_min:
|
|
174
|
+
raise ValidationError(
|
|
175
|
+
self._record,
|
|
176
|
+
'Field "{}" items cannot be less than {}'.format(
|
|
177
|
+
self._field.name,
|
|
178
|
+
item_min
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ListField(CursorField):
|
|
184
|
+
"""Text and Numeric List field"""
|
|
185
|
+
|
|
186
|
+
field_type = (
|
|
187
|
+
'Core.Models.Fields.List.ListField, Core',
|
|
188
|
+
'Core.Models.Fields.ListField, Core',
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
_type_map = {
|
|
192
|
+
'numeric': {
|
|
193
|
+
'list_item_type': 'Core.Models.Record.ListItem`1[[System.Double, mscorlib]], Core',
|
|
194
|
+
'cursor_class': NumericListFieldCursor
|
|
195
|
+
},
|
|
196
|
+
'text': {
|
|
197
|
+
'list_item_type': 'Core.Models.Record.ListItem`1[[System.String, mscorlib]], Core',
|
|
198
|
+
'cursor_class': TextListFieldCursor
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def __init__(self, *args, **kwargs):
|
|
203
|
+
super(ListField, self).__init__(*args, **kwargs)
|
|
204
|
+
self.cursor_class = self._type_map[self.input_type]['cursor_class']
|
|
205
|
+
|
|
206
|
+
# dict of value -> list(ids) for each value from record raw data
|
|
207
|
+
# Set by set_swimlane during record init from app.records.get() or after record.save()
|
|
208
|
+
self._initial_value_to_ids_map = defaultdict(list)
|
|
209
|
+
|
|
210
|
+
def set_swimlane(self, value):
|
|
211
|
+
"""Convert from list of dicts with values to list of values
|
|
212
|
+
|
|
213
|
+
Cache list items with their ID pairs to restore existing IDs to unmodified values to prevent workflow
|
|
214
|
+
evaluating on each save for any already existing values
|
|
215
|
+
"""
|
|
216
|
+
value = value or []
|
|
217
|
+
|
|
218
|
+
self._initial_value_to_ids_map = defaultdict(list)
|
|
219
|
+
for item in value:
|
|
220
|
+
self._initial_value_to_ids_map[item['value']].append(item['id'])
|
|
221
|
+
|
|
222
|
+
return super(ListField, self).set_swimlane([d['value'] for d in value])
|
|
223
|
+
|
|
224
|
+
def set_python(self, value):
|
|
225
|
+
"""Validate using cursor for consistency between direct set of values vs modification of cursor values"""
|
|
226
|
+
if not isinstance(value, (list, type(None))):
|
|
227
|
+
raise ValidationError(
|
|
228
|
+
self.record,
|
|
229
|
+
'Field "{}" must be set to a list, not "{}"'.format(
|
|
230
|
+
self.name,
|
|
231
|
+
value.__class__
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
value = value or []
|
|
235
|
+
self.cursor._validate_list(value)
|
|
236
|
+
return super(ListField, self).set_python(value)
|
|
237
|
+
|
|
238
|
+
def cast_to_swimlane(self, value):
|
|
239
|
+
"""Restore swimlane format, attempting to keep initial IDs for any previously existing values"""
|
|
240
|
+
value = super(ListField, self).cast_to_swimlane(value)
|
|
241
|
+
|
|
242
|
+
if not value:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
# Copy initial values to pop IDs out as each value is hydrated back to server format, without modifying initial
|
|
246
|
+
# cache of value -> list(ids) map
|
|
247
|
+
value_ids = deepcopy(self._initial_value_to_ids_map)
|
|
248
|
+
|
|
249
|
+
return [self._build_list_item(item, value_ids[item].pop(0) if value_ids[item] else None) for item in value]
|
|
250
|
+
|
|
251
|
+
def cast_to_bulk_modify(self, value):
|
|
252
|
+
"""List fields use raw list values for bulk modify"""
|
|
253
|
+
self.validate_value(value)
|
|
254
|
+
return value
|
|
255
|
+
|
|
256
|
+
def _build_list_item(self, item_value, id_=None):
|
|
257
|
+
"""Return a dict with ID and $type for API representation of value
|
|
258
|
+
|
|
259
|
+
Uses id_ param if provided, defaults to new random ID
|
|
260
|
+
"""
|
|
261
|
+
return {
|
|
262
|
+
'$type': self._type_map[self.input_type]['list_item_type'],
|
|
263
|
+
'id': id_ or SID.generate(),
|
|
264
|
+
'value': item_value
|
|
265
|
+
}
|
|
266
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import numbers
|
|
2
|
+
|
|
3
|
+
from swimlane.exceptions import ValidationError
|
|
4
|
+
from .base import Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NumberField(Field):
|
|
8
|
+
|
|
9
|
+
field_type = (
|
|
10
|
+
'Core.Models.Fields.NumericField, Core',
|
|
11
|
+
'Core.Models.Fields.Numeric.NumericField, Core'
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
supported_types = [numbers.Number]
|
|
15
|
+
|
|
16
|
+
def __init__(self, *args, **kwargs):
|
|
17
|
+
super(NumberField, self).__init__(*args, **kwargs)
|
|
18
|
+
|
|
19
|
+
self.min = self.field_definition.get('min')
|
|
20
|
+
self.max = self.field_definition.get('max')
|
|
21
|
+
|
|
22
|
+
def validate_value(self, value):
|
|
23
|
+
super(NumberField, self).validate_value(value)
|
|
24
|
+
|
|
25
|
+
if value is not None:
|
|
26
|
+
if self.min is not None and value < self.min:
|
|
27
|
+
raise ValidationError(self.record, 'Field "{}" minimum value "{}", received "{}"'.format(
|
|
28
|
+
self.name,
|
|
29
|
+
self.min,
|
|
30
|
+
value
|
|
31
|
+
))
|
|
32
|
+
|
|
33
|
+
if self.max is not None and value > self.max:
|
|
34
|
+
raise ValidationError(self.record, 'Field "{}" maximum value "{}", received "{}"'.format(
|
|
35
|
+
self.name,
|
|
36
|
+
self.max,
|
|
37
|
+
value
|
|
38
|
+
))
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import six
|
|
3
|
+
from sortedcontainers import SortedDict
|
|
4
|
+
from swimlane.core.fields.base import CursorField, FieldCursor
|
|
5
|
+
from swimlane.core.resources.record import Record
|
|
6
|
+
from swimlane.exceptions import ValidationError, SwimlaneHTTP400Error
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReferenceCursor(FieldCursor):
|
|
12
|
+
"""Handles lazy retrieval of target records"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *args, **kwargs):
|
|
15
|
+
super(ReferenceCursor, self).__init__(*args, **kwargs)
|
|
16
|
+
self._elements = self._elements or SortedDict()
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def target_app(self):
|
|
20
|
+
"""Make field's target_app available on cursor"""
|
|
21
|
+
return self._field.target_app
|
|
22
|
+
|
|
23
|
+
def __getitem__(self, item):
|
|
24
|
+
record_id, record = [kv for kv in self._elements.items()][item]
|
|
25
|
+
if record is self._field._unset:
|
|
26
|
+
try:
|
|
27
|
+
records_get = self.target_app.records.get(id=record_id)
|
|
28
|
+
self._elements[record_id] = records_get
|
|
29
|
+
return records_get
|
|
30
|
+
except SwimlaneHTTP400Error:
|
|
31
|
+
logger.debug('Received 400 response retrieving record "{}", ignoring assumed orphaned record')
|
|
32
|
+
else:
|
|
33
|
+
return record
|
|
34
|
+
|
|
35
|
+
def __iter__(self):
|
|
36
|
+
for record_id, record in six.iteritems(self._elements):
|
|
37
|
+
if record is self._field._unset:
|
|
38
|
+
try:
|
|
39
|
+
records_get = self.target_app.records.get(id=record_id)
|
|
40
|
+
self._elements[record_id] = records_get
|
|
41
|
+
yield records_get
|
|
42
|
+
except SwimlaneHTTP400Error:
|
|
43
|
+
logger.debug('Received 400 response retrieving record "{}", ignoring assumed orphaned record')
|
|
44
|
+
else:
|
|
45
|
+
yield record
|
|
46
|
+
|
|
47
|
+
def add(self, record):
|
|
48
|
+
"""Add a reference to the provided record"""
|
|
49
|
+
self._field.validate_value(record)
|
|
50
|
+
self._elements[record.id] = record
|
|
51
|
+
self._sync_field()
|
|
52
|
+
|
|
53
|
+
def remove(self, record):
|
|
54
|
+
"""Remove a reference to the provided record"""
|
|
55
|
+
self._field.validate_value(record)
|
|
56
|
+
del self._elements[record.id]
|
|
57
|
+
self._sync_field()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ReferenceField(CursorField):
|
|
61
|
+
|
|
62
|
+
field_type = 'Core.Models.Fields.Reference.ReferenceField, Core'
|
|
63
|
+
supported_types = (Record,)
|
|
64
|
+
cursor_class = ReferenceCursor
|
|
65
|
+
|
|
66
|
+
def __init__(self, *args, **kwargs):
|
|
67
|
+
super(ReferenceField, self).__init__(*args, **kwargs)
|
|
68
|
+
self.__target_app_id = self.field_definition['targetId']
|
|
69
|
+
self.__target_app = None
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def target_app(self):
|
|
73
|
+
"""Defer target app retrieval until requested"""
|
|
74
|
+
if self.__target_app is None:
|
|
75
|
+
self.__target_app = self._swimlane.apps.get(id=self.__target_app_id)
|
|
76
|
+
|
|
77
|
+
return self.__target_app
|
|
78
|
+
|
|
79
|
+
def validate_value(self, value):
|
|
80
|
+
"""Validate provided record is a part of the appropriate target app for the field"""
|
|
81
|
+
if value not in (None, self._unset):
|
|
82
|
+
|
|
83
|
+
if value.app != self.target_app:
|
|
84
|
+
raise ValidationError(
|
|
85
|
+
self.record,
|
|
86
|
+
'Reference field "{}" has target app "{}", cannot reference record "{}" from app "{}"'.format(
|
|
87
|
+
self.name,
|
|
88
|
+
self.target_app,
|
|
89
|
+
value,
|
|
90
|
+
value.app
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _set(self, value):
|
|
95
|
+
self._cursor = None
|
|
96
|
+
self._value = value or None
|
|
97
|
+
|
|
98
|
+
def set_swimlane(self, value):
|
|
99
|
+
"""Store record ids in separate location for later use, but ignore initial value"""
|
|
100
|
+
|
|
101
|
+
# Values come in as a list of record ids or None
|
|
102
|
+
value = value or []
|
|
103
|
+
if isinstance(value, dict):
|
|
104
|
+
value = value["_v"] if "_v" in value else []
|
|
105
|
+
|
|
106
|
+
records = SortedDict()
|
|
107
|
+
|
|
108
|
+
for record_id in value:
|
|
109
|
+
records[record_id] = self._unset
|
|
110
|
+
|
|
111
|
+
return super(ReferenceField, self).set_swimlane(records)
|
|
112
|
+
|
|
113
|
+
def set_python(self, value):
|
|
114
|
+
"""With changes to Lazy instrumentation _evaluation returns SortedDictionary, unfortunately this
|
|
115
|
+
is still public method that can be accessed in the old way, therefore it was split in two.
|
|
116
|
+
"""
|
|
117
|
+
if isinstance(value, SortedDict):
|
|
118
|
+
self.set_python_raw(value)
|
|
119
|
+
else:
|
|
120
|
+
self.set_python_old(value)
|
|
121
|
+
|
|
122
|
+
def set_python_old(self, value):
|
|
123
|
+
"""Expect list of record instances, convert to a SortedDict for internal representation"""
|
|
124
|
+
if not self.multiselect:
|
|
125
|
+
if value and not isinstance(value, list):
|
|
126
|
+
value = [value]
|
|
127
|
+
|
|
128
|
+
value = value or []
|
|
129
|
+
|
|
130
|
+
records = SortedDict()
|
|
131
|
+
|
|
132
|
+
for record in value:
|
|
133
|
+
self.validate_value(record)
|
|
134
|
+
records[record.id] = record
|
|
135
|
+
|
|
136
|
+
self._set(records)
|
|
137
|
+
self.record._raw['values'][self.id] = self.get_swimlane()
|
|
138
|
+
|
|
139
|
+
def get_swimlane(self):
|
|
140
|
+
"""Return list of record ids"""
|
|
141
|
+
value = super(ReferenceField, self).get_swimlane()
|
|
142
|
+
if value:
|
|
143
|
+
ids = list(value.keys())
|
|
144
|
+
return ids
|
|
145
|
+
|
|
146
|
+
def get_item(self):
|
|
147
|
+
"""Return cursor if multi-select, direct value if single-select"""
|
|
148
|
+
cursor = super(ReferenceField, self).get_python()
|
|
149
|
+
if self.multiselect:
|
|
150
|
+
return cursor
|
|
151
|
+
else:
|
|
152
|
+
try:
|
|
153
|
+
return cursor[0]
|
|
154
|
+
except IndexError:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def get_batch_representation(self):
|
|
158
|
+
"""Return best batch process representation of field value"""
|
|
159
|
+
return self.get_swimlane()
|
|
160
|
+
|
|
161
|
+
def cast_to_report(self, value):
|
|
162
|
+
return value.id
|
|
163
|
+
|
|
164
|
+
def for_json(self):
|
|
165
|
+
return self.get_swimlane()
|
|
166
|
+
|
|
167
|
+
def set_python_raw(self, value):
|
|
168
|
+
self._set(value)
|
|
169
|
+
self.record._raw['values'][self.id] = self.get_swimlane()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import six
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from .base import Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TextField(Field):
|
|
8
|
+
|
|
9
|
+
field_type = (
|
|
10
|
+
'Core.Models.Fields.TextField, Core',
|
|
11
|
+
'Core.Models.Fields.Text.TextField, Core'
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
supported_types = six.string_types
|
|
15
|
+
|
|
16
|
+
def set_python(self, value):
|
|
17
|
+
"""Set field internal value from the python representation of field value"""
|
|
18
|
+
|
|
19
|
+
# json field handling
|
|
20
|
+
if self.input_type == "json":
|
|
21
|
+
if value is not None and not isinstance(value, self.supported_types):
|
|
22
|
+
value = json.dumps(value, indent=4)
|
|
23
|
+
|
|
24
|
+
# hook exists to stringify before validation
|
|
25
|
+
|
|
26
|
+
# set to string if not string or unicode
|
|
27
|
+
if value is not None and not isinstance(value, self.supported_types) or isinstance(value, int):
|
|
28
|
+
value = str(value)
|
|
29
|
+
return super(TextField, self).set_python(value)
|
|
30
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from swimlane.core.resources.usergroup import UserGroup, User
|
|
2
|
+
from swimlane.exceptions import ValidationError
|
|
3
|
+
from .base import MultiSelectField
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UserGroupField(MultiSelectField):
|
|
7
|
+
"""Manages getting/setting users from record User/Group fields"""
|
|
8
|
+
|
|
9
|
+
field_type = (
|
|
10
|
+
'Core.Models.Fields.UserGroupField, Core',
|
|
11
|
+
'Core.Models.Fields.UserGroup.UserGroupField, Core'
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
supported_types = [UserGroup]
|
|
15
|
+
|
|
16
|
+
def __init__(self, *args, **kwargs):
|
|
17
|
+
super(UserGroupField, self).__init__(*args, **kwargs)
|
|
18
|
+
|
|
19
|
+
members = self.field_definition.get('members', [])
|
|
20
|
+
|
|
21
|
+
self._allowed_user_ids = set([r['id'] for r in members if r['selectionType'] == 'users'])
|
|
22
|
+
self._allowed_member_ids = set([r['id'] for r in members if r['selectionType'] == 'members'])
|
|
23
|
+
|
|
24
|
+
self._allowed_group_ids = set([r['id'] for r in members if r['selectionType'] == 'groups'])
|
|
25
|
+
self._allowed_subgroup_ids = set([r['id'] for r in members if r['selectionType'] == 'subGroups'])
|
|
26
|
+
|
|
27
|
+
self._show_all_users = self.field_definition['showAllUsers']
|
|
28
|
+
self._show_all_groups = self.field_definition['showAllGroups']
|
|
29
|
+
|
|
30
|
+
def validate_value(self, value):
|
|
31
|
+
"""Validate new user/group value against any User/Group restrictions
|
|
32
|
+
|
|
33
|
+
Attempts to resolve generic UserGroup instances if necessary to respect special "Everyone" group, and
|
|
34
|
+
"All Users" + "All Groups" options
|
|
35
|
+
"""
|
|
36
|
+
super(UserGroupField, self).validate_value(value)
|
|
37
|
+
|
|
38
|
+
if value is not None:
|
|
39
|
+
# Ignore validation if all users + groups are allowed
|
|
40
|
+
if self._show_all_groups and self._show_all_users:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# Try to directly check allowed ids against user/group id first to avoid having to resolve generic
|
|
44
|
+
# UserGroup with an additional request
|
|
45
|
+
if value.id in self._allowed_user_ids | self._allowed_group_ids:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
# Resolve to check Users vs Groups separately
|
|
49
|
+
value = value.resolve()
|
|
50
|
+
|
|
51
|
+
if isinstance(value, User):
|
|
52
|
+
self._validate_user(value)
|
|
53
|
+
else:
|
|
54
|
+
self._validate_group(value)
|
|
55
|
+
|
|
56
|
+
def _validate_user(self, user):
|
|
57
|
+
"""Validate a User instance against allowed user IDs or membership in a group"""
|
|
58
|
+
if user._raw['groups'] == []:
|
|
59
|
+
result = self._swimlane.request('get', "user/{0}".format(user.id))
|
|
60
|
+
user._raw['groups'] = result.json()['groups']
|
|
61
|
+
|
|
62
|
+
# All users allowed
|
|
63
|
+
if self._show_all_users:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# User specifically allowed
|
|
67
|
+
if user.id in self._allowed_user_ids:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
# User allowed by group membership
|
|
71
|
+
user_member_group_ids = set([g['id'] for g in user._raw['groups']])
|
|
72
|
+
if user_member_group_ids & self._allowed_member_ids:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
raise ValidationError(
|
|
76
|
+
self.record,
|
|
77
|
+
'User "{}" is not a valid selection for field "{}"'.format(
|
|
78
|
+
user,
|
|
79
|
+
self.name
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def _validate_group(self, group):
|
|
84
|
+
"""Validate a Group instance against allowed group IDs or subgroup of a parent group"""
|
|
85
|
+
# All groups allowed
|
|
86
|
+
if self._show_all_groups:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Group specifically allowed
|
|
90
|
+
if group.id in self._allowed_group_ids:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# Group allowed by subgroup membership
|
|
94
|
+
for parent_group_id in self._allowed_subgroup_ids:
|
|
95
|
+
# Get each group, and check subgroup ids
|
|
96
|
+
parent_group = self._swimlane.groups.get(id=parent_group_id)
|
|
97
|
+
parent_group_child_ids = set([g['id'] for g in parent_group._raw['groups']])
|
|
98
|
+
if group.id in parent_group_child_ids:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
raise ValidationError(
|
|
102
|
+
self.record,
|
|
103
|
+
'Group "{}" is not a valid selection for field "{}"'.format(
|
|
104
|
+
group,
|
|
105
|
+
self.name
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def get_batch_representation(self):
|
|
110
|
+
"""Return best batch process representation of field value"""
|
|
111
|
+
return self.get_swimlane()
|
|
112
|
+
|
|
113
|
+
def set_swimlane(self, value):
|
|
114
|
+
"""Workaround for reports returning an empty usergroup field as a single element list with no id/name"""
|
|
115
|
+
if value == [{"$type": "Core.Models.Utilities.UserGroupSelection, Core"}]:
|
|
116
|
+
value = []
|
|
117
|
+
|
|
118
|
+
return super(UserGroupField, self).set_swimlane(value)
|
|
119
|
+
|
|
120
|
+
def cast_to_python(self, value):
|
|
121
|
+
"""Convert JSON definition to UserGroup object"""
|
|
122
|
+
# v2.x does not provide a distinction between users and groups at the field selection level, can only return
|
|
123
|
+
# UserGroup instances instead of specific User or Group instances
|
|
124
|
+
if value is not None:
|
|
125
|
+
value = UserGroup(self._swimlane, value)
|
|
126
|
+
|
|
127
|
+
return value
|
|
128
|
+
|
|
129
|
+
def cast_to_swimlane(self, value):
|
|
130
|
+
"""Dump UserGroup back to JSON representation"""
|
|
131
|
+
if value is not None:
|
|
132
|
+
if not isinstance(value, UserGroup):
|
|
133
|
+
raise TypeError(
|
|
134
|
+
'Expected UserGroup, received "{}" instead'.format(value))
|
|
135
|
+
value = value.as_usergroup_selection()
|
|
136
|
+
|
|
137
|
+
return value
|