@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,15 @@
|
|
|
1
|
+
"""Base classes used to build field abstractions"""
|
|
2
|
+
from .cursor import CursorField, FieldCursor
|
|
3
|
+
from .field import Field
|
|
4
|
+
from .multiselect import MultiSelectField, MultiSelectCursor
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ReadOnly(Field):
|
|
8
|
+
"""Mixin explicitly disabling setting value via python"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, *args, **kwargs):
|
|
11
|
+
super(ReadOnly, self).__init__(*args, **kwargs)
|
|
12
|
+
|
|
13
|
+
self.readonly = True
|
|
14
|
+
|
|
15
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import weakref
|
|
2
|
+
|
|
3
|
+
from swimlane.core.cursor import Cursor
|
|
4
|
+
from swimlane.core.resolver import SwimlaneResolver
|
|
5
|
+
from .field import Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FieldCursor(Cursor, SwimlaneResolver):
|
|
9
|
+
"""Base class for encapsulating a field instance's complex logic
|
|
10
|
+
|
|
11
|
+
Useful in abstracting away extra request(s), lazy evaluation, pagination, intensive calculations, etc.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, field, initial_elements=None):
|
|
15
|
+
SwimlaneResolver.__init__(self, field.record._swimlane)
|
|
16
|
+
Cursor.__init__(self)
|
|
17
|
+
|
|
18
|
+
self._elements = initial_elements or self._elements
|
|
19
|
+
|
|
20
|
+
self.__field_name = field.name
|
|
21
|
+
self.__record_ref = weakref.ref(field.record)
|
|
22
|
+
self.__field_ref = weakref.ref(field)
|
|
23
|
+
|
|
24
|
+
def __repr__(self):
|
|
25
|
+
# pylint: disable=missing-format-attribute
|
|
26
|
+
return '<{self.__class__.__name__}: {self._record!r}["{self._field.name}"] ({length})>'.format(
|
|
27
|
+
self=self,
|
|
28
|
+
length=len(self)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def __eq__(self, other):
|
|
32
|
+
return isinstance(other, self.__class__) and other._record.id == self._record.id
|
|
33
|
+
|
|
34
|
+
def _sync_field(self):
|
|
35
|
+
"""Set source field value to current cursor value"""
|
|
36
|
+
self._field.set_python(self._evaluate())
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def _record(self):
|
|
40
|
+
return self.__record_ref()
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def _field(self):
|
|
44
|
+
field = self.__field_ref()
|
|
45
|
+
# Occurs when a record is saved and reinitialized, creating new Field instances and losing the existing weakref
|
|
46
|
+
# Update weakref to point to new Field instance of the same name
|
|
47
|
+
if field is None:
|
|
48
|
+
field = self._record.get_field(self.__field_name)
|
|
49
|
+
self.__field_ref = weakref.ref(field)
|
|
50
|
+
return field
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CursorField(Field):
|
|
54
|
+
"""Returns a proxy-like FieldCursor instance to support additional functionality"""
|
|
55
|
+
|
|
56
|
+
cursor_class = None
|
|
57
|
+
|
|
58
|
+
def __init__(self, *args, **kwargs):
|
|
59
|
+
super(CursorField, self).__init__(*args, **kwargs)
|
|
60
|
+
|
|
61
|
+
self._cursor = None
|
|
62
|
+
|
|
63
|
+
def get_initial_elements(self):
|
|
64
|
+
"""Return initial elements to be passed with cursor instantiation"""
|
|
65
|
+
return self._get()
|
|
66
|
+
|
|
67
|
+
def _set(self, value):
|
|
68
|
+
self._cursor = None
|
|
69
|
+
super(CursorField, self)._set(value)
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def cursor(self):
|
|
73
|
+
"""Cache and return cursor_class instance"""
|
|
74
|
+
if self._cursor is None:
|
|
75
|
+
# pylint: disable=not-callable
|
|
76
|
+
self._cursor = self.cursor_class(self, self.get_initial_elements())
|
|
77
|
+
|
|
78
|
+
return self._cursor
|
|
79
|
+
|
|
80
|
+
def get_python(self):
|
|
81
|
+
"""Create, cache, and return the appropriate cursor instance"""
|
|
82
|
+
return self.cursor
|
|
83
|
+
|
|
84
|
+
def for_json(self):
|
|
85
|
+
"""Return list of all cursor items, calling .for_json() if available for best representations"""
|
|
86
|
+
cursor = super(CursorField, self).for_json()
|
|
87
|
+
if cursor is not None:
|
|
88
|
+
# Lambda called immediately, false-positive from pylint on closure scope warning
|
|
89
|
+
# pylint: disable=cell-var-from-loop
|
|
90
|
+
return [getattr(item, 'for_json', lambda: item)() for item in cursor]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import weakref
|
|
2
|
+
|
|
3
|
+
from swimlane.core.resolver import SwimlaneResolver
|
|
4
|
+
from swimlane.exceptions import ValidationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Field(SwimlaneResolver):
|
|
8
|
+
"""Base class for abstracting Swimlane complex types"""
|
|
9
|
+
|
|
10
|
+
field_type = None
|
|
11
|
+
|
|
12
|
+
# Sentinel representing a field that has no current value
|
|
13
|
+
_unset = object()
|
|
14
|
+
|
|
15
|
+
# List of supported types, leave blank to disable type validation
|
|
16
|
+
supported_types = []
|
|
17
|
+
|
|
18
|
+
# Checks if bulk modify is supported for field
|
|
19
|
+
bulk_modify_support = True
|
|
20
|
+
|
|
21
|
+
def __init__(self, name, record):
|
|
22
|
+
"""Value not included during instantiation to prevent ambiguity between python and swimlane representations"""
|
|
23
|
+
super(Field, self).__init__(record._swimlane)
|
|
24
|
+
|
|
25
|
+
self.name = name
|
|
26
|
+
self.__record_ref = weakref.ref(record)
|
|
27
|
+
self._value = self._unset
|
|
28
|
+
|
|
29
|
+
self.field_definition = self.record.app.get_field_definition_by_name(self.name)
|
|
30
|
+
self.key = self.field_definition.get('key')
|
|
31
|
+
self.id = self.field_definition['id']
|
|
32
|
+
self.input_type = self.field_definition.get('inputType')
|
|
33
|
+
self.required = self.field_definition.get('required', False)
|
|
34
|
+
self.readonly = bool(self.field_definition.get('formula', self.field_definition.get('readOnly', False)))
|
|
35
|
+
self.multiselect = self.field_definition.get('selectionType', 'single') == 'multi'
|
|
36
|
+
|
|
37
|
+
def __repr__(self):
|
|
38
|
+
return '<{class_name}: {py!r}>'.format(class_name=self.__class__.__name__, py=self.get_python())
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def record(self):
|
|
42
|
+
"""Resolve weak reference to parent record"""
|
|
43
|
+
return self.__record_ref()
|
|
44
|
+
|
|
45
|
+
def _get(self):
|
|
46
|
+
"""Default getter used for both representations unless overridden"""
|
|
47
|
+
return self._value
|
|
48
|
+
|
|
49
|
+
def get_item(self):
|
|
50
|
+
"""Return best python representation of field value for get attribute method"""
|
|
51
|
+
return self.get_python()
|
|
52
|
+
|
|
53
|
+
def get_python(self):
|
|
54
|
+
"""Return best python representation of field value"""
|
|
55
|
+
return self._get()
|
|
56
|
+
|
|
57
|
+
def get_batch_representation(self):
|
|
58
|
+
"""Return best batch process representation of field value"""
|
|
59
|
+
return self._get()
|
|
60
|
+
|
|
61
|
+
def get_swimlane(self):
|
|
62
|
+
"""Return best swimlane representation of field value"""
|
|
63
|
+
return self.cast_to_swimlane(self._get())
|
|
64
|
+
|
|
65
|
+
def get_report(self, value):
|
|
66
|
+
"""Return provided field Python value formatted for use in report filter"""
|
|
67
|
+
if self.multiselect:
|
|
68
|
+
value = value or []
|
|
69
|
+
try:
|
|
70
|
+
list_vars = vars(value)
|
|
71
|
+
if "multiselect" in list_vars:
|
|
72
|
+
pass
|
|
73
|
+
except :
|
|
74
|
+
if not isinstance(value, list) :
|
|
75
|
+
raise TypeError("Value Expected a list, but got something else.")
|
|
76
|
+
children = []
|
|
77
|
+
|
|
78
|
+
for child in value:
|
|
79
|
+
id = self.cast_to_report(child)
|
|
80
|
+
if id:
|
|
81
|
+
children.append(id)
|
|
82
|
+
|
|
83
|
+
return children
|
|
84
|
+
|
|
85
|
+
return self.cast_to_report(value)
|
|
86
|
+
|
|
87
|
+
def get_bulk_modify(self, value):
|
|
88
|
+
"""Return value in format for bulk modify"""
|
|
89
|
+
if self.multiselect:
|
|
90
|
+
value = value or []
|
|
91
|
+
return [self.cast_to_bulk_modify(child) for child in value]
|
|
92
|
+
|
|
93
|
+
return self.cast_to_bulk_modify(value)
|
|
94
|
+
|
|
95
|
+
def cast_to_python(self, value):
|
|
96
|
+
"""Called during set_swimlane, should accept a single raw value as provided from API
|
|
97
|
+
|
|
98
|
+
Defaults to no-op
|
|
99
|
+
"""
|
|
100
|
+
return value
|
|
101
|
+
|
|
102
|
+
def cast_to_swimlane(self, value):
|
|
103
|
+
"""Called during get_swimlane, should accept a python value and return swimlane representation
|
|
104
|
+
|
|
105
|
+
Defaults to no-op
|
|
106
|
+
"""
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
def cast_to_report(self, value):
|
|
110
|
+
"""Cast single value to report format, defaults to cast_to_swimlane(value)"""
|
|
111
|
+
return self.cast_to_swimlane(value)
|
|
112
|
+
|
|
113
|
+
def cast_to_bulk_modify(self, value):
|
|
114
|
+
"""Cast single value to bulk modify format, defaults to cast_to_report with added validation"""
|
|
115
|
+
self.validate_value(value)
|
|
116
|
+
return self.cast_to_report(value)
|
|
117
|
+
|
|
118
|
+
def validate_value(self, value):
|
|
119
|
+
"""Validate value is an acceptable type during set_python operation"""
|
|
120
|
+
if self.readonly and not self._swimlane._write_to_read_only:
|
|
121
|
+
raise ValidationError(self.record, 'Cannot set readonly field "{}"'.format(self.name))
|
|
122
|
+
if value not in (None, self._unset):
|
|
123
|
+
if self.supported_types and not isinstance(value, tuple(self.supported_types)):
|
|
124
|
+
raise ValidationError(self.record, 'Field "{}" expects one of {}, got "{}" instead'.format(
|
|
125
|
+
self.name,
|
|
126
|
+
', '.join([repr(t.__name__) for t in self.supported_types]),
|
|
127
|
+
type(value).__name__)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def _set(self, value):
|
|
131
|
+
"""Default setter used for both representations unless overridden"""
|
|
132
|
+
self._value = value
|
|
133
|
+
self.record._raw['values'][self.id] = self.get_swimlane()
|
|
134
|
+
|
|
135
|
+
def set_python(self, value):
|
|
136
|
+
"""Set field internal value from the python representation of field value"""
|
|
137
|
+
self.validate_value(value)
|
|
138
|
+
return self._set(value)
|
|
139
|
+
|
|
140
|
+
def set_swimlane(self, value):
|
|
141
|
+
"""Set field internal value from the swimlane representation of field value"""
|
|
142
|
+
return self._set(self.cast_to_python(value))
|
|
143
|
+
|
|
144
|
+
def for_json(self):
|
|
145
|
+
"""Return json.dump()-compatible representation of field value
|
|
146
|
+
|
|
147
|
+
.. versionadded:: 4.1.0
|
|
148
|
+
"""
|
|
149
|
+
return self.get_python()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from sortedcontainers import SortedSet
|
|
2
|
+
|
|
3
|
+
from swimlane.core.resources.usergroup import User
|
|
4
|
+
|
|
5
|
+
from .cursor import CursorField, FieldCursor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MultiSelectCursor(FieldCursor):
|
|
9
|
+
"""Cursor allowing setting and unsetting values on a MultiSelectField
|
|
10
|
+
|
|
11
|
+
Respects parent field's validation
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *args, **kwargs):
|
|
15
|
+
super(MultiSelectCursor, self).__init__(*args, **kwargs)
|
|
16
|
+
|
|
17
|
+
self._elements = filter(lambda e: e is not None, self._elements)
|
|
18
|
+
self._elements = SortedSet(self._elements)
|
|
19
|
+
|
|
20
|
+
def select(self, element):
|
|
21
|
+
"""Add an element to the set of selected elements
|
|
22
|
+
|
|
23
|
+
Proxy to internal set.add and sync field
|
|
24
|
+
"""
|
|
25
|
+
self._field.validate_value(element)
|
|
26
|
+
self._elements.add(element)
|
|
27
|
+
self._sync_field()
|
|
28
|
+
|
|
29
|
+
def deselect(self, element):
|
|
30
|
+
"""Remove an element from the set of selected elements
|
|
31
|
+
|
|
32
|
+
Proxy to internal set.remove and sync field
|
|
33
|
+
"""
|
|
34
|
+
self._elements.remove(element)
|
|
35
|
+
self._sync_field()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MultiSelectField(CursorField):
|
|
39
|
+
"""Base class for fields that can be multi-selection or single-selection field"""
|
|
40
|
+
|
|
41
|
+
cursor_class = MultiSelectCursor
|
|
42
|
+
|
|
43
|
+
def get_python(self):
|
|
44
|
+
"""Only return cursor instance if configured for multiselect"""
|
|
45
|
+
if self.multiselect:
|
|
46
|
+
return super(MultiSelectField, self).get_python()
|
|
47
|
+
|
|
48
|
+
return self._get()
|
|
49
|
+
|
|
50
|
+
def get_swimlane(self):
|
|
51
|
+
"""Handle multi-select and single-select modes"""
|
|
52
|
+
if self.multiselect:
|
|
53
|
+
value = self._get()
|
|
54
|
+
children = []
|
|
55
|
+
if value:
|
|
56
|
+
for child in value:
|
|
57
|
+
children.append(self.cast_to_swimlane(child))
|
|
58
|
+
return children
|
|
59
|
+
return None
|
|
60
|
+
return super(MultiSelectField, self).get_swimlane()
|
|
61
|
+
|
|
62
|
+
def _set(self, value):
|
|
63
|
+
"""Override to treat empty lists as None"""
|
|
64
|
+
return super(MultiSelectField, self)._set(value or None)
|
|
65
|
+
|
|
66
|
+
def set_python(self, value):
|
|
67
|
+
"""Override to remove key from raw data when empty to work with server 2.16+ validation"""
|
|
68
|
+
if self.multiselect:
|
|
69
|
+
value = value or []
|
|
70
|
+
elements = []
|
|
71
|
+
|
|
72
|
+
if not isinstance(
|
|
73
|
+
value,
|
|
74
|
+
(
|
|
75
|
+
list,
|
|
76
|
+
MultiSelectCursor,
|
|
77
|
+
SortedSet,
|
|
78
|
+
User,
|
|
79
|
+
),
|
|
80
|
+
):
|
|
81
|
+
value = [value]
|
|
82
|
+
|
|
83
|
+
for element in value:
|
|
84
|
+
self.validate_value(element)
|
|
85
|
+
elements.append(element)
|
|
86
|
+
|
|
87
|
+
value = elements
|
|
88
|
+
else:
|
|
89
|
+
self.validate_value(value)
|
|
90
|
+
|
|
91
|
+
self._set(value)
|
|
92
|
+
|
|
93
|
+
def set_swimlane(self, value):
|
|
94
|
+
"""Cast all multi-select elements to correct internal type like single-select mode"""
|
|
95
|
+
if self.multiselect:
|
|
96
|
+
value = value or []
|
|
97
|
+
children = []
|
|
98
|
+
|
|
99
|
+
for child in value:
|
|
100
|
+
children.append(self.cast_to_python(child))
|
|
101
|
+
|
|
102
|
+
return self._set(children)
|
|
103
|
+
|
|
104
|
+
return super(MultiSelectField, self).set_swimlane(value)
|
|
105
|
+
|
|
106
|
+
def for_json(self):
|
|
107
|
+
"""Handle multi-select vs single-select"""
|
|
108
|
+
|
|
109
|
+
if self.multiselect:
|
|
110
|
+
return super(MultiSelectField, self).for_json()
|
|
111
|
+
|
|
112
|
+
value = self.get_python()
|
|
113
|
+
if hasattr(value, 'for_json'):
|
|
114
|
+
return value.for_json()
|
|
115
|
+
|
|
116
|
+
return value
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import pendulum
|
|
2
|
+
|
|
3
|
+
from swimlane.core.resources.comment import Comment
|
|
4
|
+
from .base import CursorField, FieldCursor, ReadOnly
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CommentCursor(FieldCursor):
|
|
8
|
+
"""Returned by CommentField to allow iteration and creation of Comment instances"""
|
|
9
|
+
|
|
10
|
+
def comment(self, message, rich_text=False):
|
|
11
|
+
"""Add new comment to record comment field"""
|
|
12
|
+
message = str(message)
|
|
13
|
+
if not isinstance(rich_text, bool):
|
|
14
|
+
raise ValueError('rich_text must be a boolean value.')
|
|
15
|
+
|
|
16
|
+
sw_repr = {
|
|
17
|
+
'$type': 'Core.Models.Record.Comments, Core',
|
|
18
|
+
'createdByUser': self._record._swimlane.user.as_usergroup_selection(),
|
|
19
|
+
'createdDate': pendulum.now().to_rfc3339_string(),
|
|
20
|
+
'message': message,
|
|
21
|
+
'isRichText': rich_text
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
comment = Comment(self._swimlane, sw_repr)
|
|
25
|
+
self._elements.append(comment)
|
|
26
|
+
|
|
27
|
+
self._record._raw['comments'].setdefault(self._field.id, [])
|
|
28
|
+
self._record._raw['comments'][self._field.id].append(comment._raw)
|
|
29
|
+
|
|
30
|
+
# Tracking comment changes for patch endpoint
|
|
31
|
+
self._record._comments_modified = True
|
|
32
|
+
|
|
33
|
+
return comment
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CommentsField(ReadOnly, CursorField):
|
|
37
|
+
|
|
38
|
+
field_type = (
|
|
39
|
+
'Core.Models.Fields.CommentsField, Core',
|
|
40
|
+
'Core.Models.Fields.Comments.CommentsField, Core'
|
|
41
|
+
)
|
|
42
|
+
cursor_class = CommentCursor
|
|
43
|
+
bulk_modify_support = False
|
|
44
|
+
|
|
45
|
+
def get_initial_elements(self):
|
|
46
|
+
raw_comments = self.record._raw['comments'].get(self.id, [])
|
|
47
|
+
|
|
48
|
+
return [Comment(self.record._swimlane, raw) for raw in raw_comments]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
from swimlane.utils.date_validator import is_datetime
|
|
3
|
+
|
|
4
|
+
from datetime import date, datetime, time, timedelta
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
import pendulum
|
|
8
|
+
|
|
9
|
+
from .base import Field
|
|
10
|
+
|
|
11
|
+
UTC = pendulum.timezone('UTC')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DatetimeField(Field):
|
|
15
|
+
|
|
16
|
+
field_type = 'Core.Models.Fields.Date.DateField, Core'
|
|
17
|
+
|
|
18
|
+
datetime_format = '%Y-%m-%dT%H:%M:%S.%fZ'
|
|
19
|
+
|
|
20
|
+
_type_date = 'date'
|
|
21
|
+
_type_time = 'time'
|
|
22
|
+
_type_interval = 'timespan'
|
|
23
|
+
|
|
24
|
+
# All others default to datetime
|
|
25
|
+
_input_type_map = {
|
|
26
|
+
_type_interval: [timedelta],
|
|
27
|
+
_type_date: [datetime, date],
|
|
28
|
+
_type_time: [datetime, time]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def __init__(self, *args, **kwargs):
|
|
32
|
+
super(DatetimeField, self).__init__(*args, **kwargs)
|
|
33
|
+
|
|
34
|
+
# Determine supported_types after inspecting input subtype
|
|
35
|
+
self.supported_types = self._input_type_map.get(self.input_type, [datetime])
|
|
36
|
+
|
|
37
|
+
def _set(self, value):
|
|
38
|
+
# Force to appropriate Pendulum instance for consistency
|
|
39
|
+
if value is not None:
|
|
40
|
+
if self.input_type != self._type_interval:
|
|
41
|
+
if self.input_type == self._type_date:
|
|
42
|
+
# Pendulum date
|
|
43
|
+
if isinstance(value, date):
|
|
44
|
+
value = pendulum.DateTime.combine(value, pendulum.time(0))
|
|
45
|
+
elif self.input_type == self._type_time:
|
|
46
|
+
# Pendulum time
|
|
47
|
+
if isinstance(value, time):
|
|
48
|
+
value = pendulum.DateTime.combine(pendulum.today().date(), value)
|
|
49
|
+
|
|
50
|
+
# Convert to Pendulum instance in UTC
|
|
51
|
+
value = UTC.convert(pendulum.instance(value))
|
|
52
|
+
# Drop nanosecond precision to match Mongo precision
|
|
53
|
+
value = value.set(microsecond=int(math.floor(value.microsecond / 1000) * 1000))
|
|
54
|
+
|
|
55
|
+
return super(DatetimeField, self)._set(value)
|
|
56
|
+
|
|
57
|
+
def cast_to_python(self, value):
|
|
58
|
+
if value is not None:
|
|
59
|
+
if self.input_type == self._type_interval:
|
|
60
|
+
value = pendulum.duration(milliseconds=int(value))
|
|
61
|
+
else:
|
|
62
|
+
value = pendulum.parse(value)
|
|
63
|
+
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
def get_python(self):
|
|
67
|
+
"""Coerce to best date type representation for the field subtype"""
|
|
68
|
+
value = super(DatetimeField, self).get_python()
|
|
69
|
+
|
|
70
|
+
if value is not None:
|
|
71
|
+
# Handle subtypes with matching Pendulum types
|
|
72
|
+
if self.input_type == self._type_time:
|
|
73
|
+
value = value.time()
|
|
74
|
+
if self.input_type == self._type_date:
|
|
75
|
+
value = value.date()
|
|
76
|
+
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def format_datetime(cls, target_datetime):
|
|
81
|
+
"""Format datetime as expected by Swimlane API"""
|
|
82
|
+
if not is_datetime(target_datetime):
|
|
83
|
+
target_datetime = datetime.combine(target_datetime, datetime.min.time())
|
|
84
|
+
return UTC.convert(target_datetime).strftime(cls.datetime_format)
|
|
85
|
+
|
|
86
|
+
def cast_to_swimlane(self, value):
|
|
87
|
+
"""Return datetimes formatted as expected by API and timespans as millisecond epochs"""
|
|
88
|
+
if value is None:
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
if self.input_type == self._type_interval:
|
|
92
|
+
return value.in_seconds() * 1000
|
|
93
|
+
|
|
94
|
+
return self.format_datetime(value)
|
|
95
|
+
|
|
96
|
+
def get_batch_representation(self):
|
|
97
|
+
"""Return best batch process representation of field value"""
|
|
98
|
+
return self.get_swimlane()
|
|
99
|
+
|
|
100
|
+
def for_json(self):
|
|
101
|
+
"""Return date ISO8601 string formats for datetime, date, and time values, milliseconds for intervals"""
|
|
102
|
+
value = super(DatetimeField, self).for_json()
|
|
103
|
+
|
|
104
|
+
# Order of instance checks matters for proper inheritance checks
|
|
105
|
+
if isinstance(value, pendulum.Duration):
|
|
106
|
+
return value.in_seconds() * 1000
|
|
107
|
+
if isinstance(value, datetime):
|
|
108
|
+
return self.format_datetime(value)
|
|
109
|
+
if isinstance(value, pendulum.Time):
|
|
110
|
+
return str(value)
|
|
111
|
+
if isinstance(value, pendulum.Date):
|
|
112
|
+
return value.to_date_string()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .base import CursorField, FieldCursor, ReadOnly
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RevisionCursor(FieldCursor):
|
|
5
|
+
"""An iterable object that automatically lazy retrieves and caches history data for a record from API"""
|
|
6
|
+
|
|
7
|
+
def __init__(self, *args, **kwargs):
|
|
8
|
+
super(RevisionCursor, self).__init__(*args, **kwargs)
|
|
9
|
+
self.__retrieved = False
|
|
10
|
+
|
|
11
|
+
def _evaluate(self):
|
|
12
|
+
"""Lazily retrieves, caches, and returns the list of record _revisions"""
|
|
13
|
+
if not self.__retrieved:
|
|
14
|
+
self._elements = self._retrieve_revisions()
|
|
15
|
+
self.__retrieved = True
|
|
16
|
+
|
|
17
|
+
return super(RevisionCursor, self)._evaluate()
|
|
18
|
+
|
|
19
|
+
def _retrieve_revisions(self):
|
|
20
|
+
"""Populate RecordRevision instances."""
|
|
21
|
+
return self._record.revisions.get_all()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HistoryField(ReadOnly, CursorField):
|
|
25
|
+
|
|
26
|
+
field_type = 'Core.Models.Fields.History.HistoryField, Core'
|
|
27
|
+
cursor_class = RevisionCursor
|
|
28
|
+
bulk_modify_support = False
|