@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.
Files changed (74) hide show
  1. package/README.md +18 -18
  2. package/lib/commands/connector/build.js +168 -44
  3. package/lib/commands/connector/build.js.map +1 -1
  4. package/lib/commands/connector/sign.js +108 -12
  5. package/lib/commands/connector/sign.js.map +1 -1
  6. package/lib/commands/connector/validate.js +110 -10
  7. package/lib/commands/connector/validate.js.map +1 -1
  8. package/lib/commands/migrator/convert.d.ts +3 -0
  9. package/lib/commands/migrator/convert.js +201 -20
  10. package/lib/commands/migrator/convert.js.map +1 -1
  11. package/lib/templates/migrator-runners/plugin_override.txt +76 -4
  12. package/lib/templates/migrator-runners/runner_override.txt +30 -0
  13. package/lib/templates/migrator-runners/script_override.txt +77 -5
  14. package/lib/templates/swimlane/__init__.py +18 -0
  15. package/lib/templates/swimlane/core/__init__.py +0 -0
  16. package/lib/templates/swimlane/core/adapters/__init__.py +10 -0
  17. package/lib/templates/swimlane/core/adapters/app.py +59 -0
  18. package/lib/templates/swimlane/core/adapters/app_revision.py +49 -0
  19. package/lib/templates/swimlane/core/adapters/helper.py +84 -0
  20. package/lib/templates/swimlane/core/adapters/record.py +468 -0
  21. package/lib/templates/swimlane/core/adapters/record_revision.py +43 -0
  22. package/lib/templates/swimlane/core/adapters/report.py +65 -0
  23. package/lib/templates/swimlane/core/adapters/task.py +58 -0
  24. package/lib/templates/swimlane/core/adapters/usergroup.py +183 -0
  25. package/lib/templates/swimlane/core/bulk.py +48 -0
  26. package/lib/templates/swimlane/core/cache.py +165 -0
  27. package/lib/templates/swimlane/core/client.py +466 -0
  28. package/lib/templates/swimlane/core/cursor.py +100 -0
  29. package/lib/templates/swimlane/core/fields/__init__.py +46 -0
  30. package/lib/templates/swimlane/core/fields/attachment.py +82 -0
  31. package/lib/templates/swimlane/core/fields/base/__init__.py +15 -0
  32. package/lib/templates/swimlane/core/fields/base/cursor.py +90 -0
  33. package/lib/templates/swimlane/core/fields/base/field.py +149 -0
  34. package/lib/templates/swimlane/core/fields/base/multiselect.py +116 -0
  35. package/lib/templates/swimlane/core/fields/comment.py +48 -0
  36. package/lib/templates/swimlane/core/fields/datetime.py +112 -0
  37. package/lib/templates/swimlane/core/fields/history.py +28 -0
  38. package/lib/templates/swimlane/core/fields/list.py +266 -0
  39. package/lib/templates/swimlane/core/fields/number.py +38 -0
  40. package/lib/templates/swimlane/core/fields/reference.py +169 -0
  41. package/lib/templates/swimlane/core/fields/text.py +30 -0
  42. package/lib/templates/swimlane/core/fields/tracking.py +10 -0
  43. package/lib/templates/swimlane/core/fields/usergroup.py +137 -0
  44. package/lib/templates/swimlane/core/fields/valueslist.py +70 -0
  45. package/lib/templates/swimlane/core/resolver.py +46 -0
  46. package/lib/templates/swimlane/core/resources/__init__.py +0 -0
  47. package/lib/templates/swimlane/core/resources/app.py +136 -0
  48. package/lib/templates/swimlane/core/resources/app_revision.py +43 -0
  49. package/lib/templates/swimlane/core/resources/attachment.py +64 -0
  50. package/lib/templates/swimlane/core/resources/base.py +55 -0
  51. package/lib/templates/swimlane/core/resources/comment.py +33 -0
  52. package/lib/templates/swimlane/core/resources/record.py +499 -0
  53. package/lib/templates/swimlane/core/resources/record_revision.py +44 -0
  54. package/lib/templates/swimlane/core/resources/report.py +259 -0
  55. package/lib/templates/swimlane/core/resources/revision_base.py +69 -0
  56. package/lib/templates/swimlane/core/resources/task.py +16 -0
  57. package/lib/templates/swimlane/core/resources/usergroup.py +166 -0
  58. package/lib/templates/swimlane/core/search.py +31 -0
  59. package/lib/templates/swimlane/core/wrappedsession.py +12 -0
  60. package/lib/templates/swimlane/exceptions.py +191 -0
  61. package/lib/templates/swimlane/utils/__init__.py +132 -0
  62. package/lib/templates/swimlane/utils/date_validator.py +4 -0
  63. package/lib/templates/swimlane/utils/list_validator.py +7 -0
  64. package/lib/templates/swimlane/utils/str_validator.py +10 -0
  65. package/lib/templates/swimlane/utils/version.py +101 -0
  66. package/lib/transformers/base-transformer.js +61 -14
  67. package/lib/transformers/base-transformer.js.map +1 -1
  68. package/lib/transformers/connector-generator.d.ts +104 -2
  69. package/lib/transformers/connector-generator.js +1234 -51
  70. package/lib/transformers/connector-generator.js.map +1 -1
  71. package/lib/types/migrator-types.d.ts +22 -0
  72. package/lib/types/migrator-types.js.map +1 -1
  73. package/oclif.manifest.json +1 -1
  74. 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,10 @@
1
+ from .base import Field, ReadOnly
2
+
3
+
4
+ class TrackingField(ReadOnly, Field):
5
+
6
+ field_type = (
7
+ 'Core.Models.Fields.TrackingField, Core',
8
+ 'Core.Models.Fields.Tracking.TrackingField, Core'
9
+ )
10
+ bulk_modify_support = False
@@ -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