From 96896be662149f96dda094a1014aced87fa5213d Mon Sep 17 00:00:00 2001 From: Rob D Date: Fri, 26 Jan 2024 13:46:31 -0500 Subject: [PATCH 1/2] Fix for record that are locked. --- swimlane/core/resources/record.py | 40 +++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/swimlane/core/resources/record.py b/swimlane/core/resources/record.py index a7bd713e..ccaa62e9 100644 --- a/swimlane/core/resources/record.py +++ b/swimlane/core/resources/record.py @@ -68,9 +68,11 @@ def __init__(self, app, raw): self.__existing_values = {k: self.get_field(k).get_batch_representation() for (k, v) in self} self._comments_modified = False - self.locked = False - self.locking_user = None - self.locked_date = None + # If record is locked parse the data + self.locked = self._raw.get('locked', False) + if self.locked: + self.locking_user = User(self._swimlane, self._raw.get('locking_user')) + self.locked_date = pendulum.parse(self._raw.get('locked_date')) # avoid circular reference from swimlane.core.adapters import RecordRevisionAdapter @@ -394,7 +396,6 @@ def lock(self): """ Lock the record to the Current User. - Notes: Warnings: @@ -413,26 +414,25 @@ def lock(self): self.locked_date = response['lockedDate'] def unlock(self): - """ - Unlock the record. - + """ + Unlock the record. - Notes: + Notes: - Warnings: + Warnings: - Args: + Args: - """ - self.validate() - self._swimlane.request( - 'post', - 'app/{}/record/{}/unlock'.format( - self.app.id, self.id) - ).json() - self.locked = False - self.locking_user = None - self.locked_date = None + """ + self.validate() + self._swimlane.request( + 'post', + 'app/{}/record/{}/unlock'.format( + self.app.id, self.id) + ).json() + self.locked = False + self.locking_user = None + self.locked_date = None def execute_task(self, task_name, timeout=int(20)): job_info = swimlane.core.adapters.task.TaskAdapter(self.app._swimlane).execute(task_name, self._raw) From 6fc0c9b50686e89bbfbec6cad44b3179a91e094c Mon Sep 17 00:00:00 2001 From: Rob D Date: Thu, 8 Feb 2024 18:28:17 -0500 Subject: [PATCH 2/2] Removing User parsing since the type comes back different in 10.15.1 --- swimlane/core/resources/record.py | 213 ++++++++++++++---------------- 1 file changed, 98 insertions(+), 115 deletions(-) diff --git a/swimlane/core/resources/record.py b/swimlane/core/resources/record.py index ccaa62e9..12cdca9f 100644 --- a/swimlane/core/resources/record.py +++ b/swimlane/core/resources/record.py @@ -10,7 +10,6 @@ import swimlane.core.adapters.helper # avoid circular reference - @total_ordering class Record(APIResource): """A single Swimlane Record instance @@ -24,37 +23,35 @@ class Record(APIResource): app (App): App instance that Record belongs to """ - _type = 'Core.Models.Record.Record, Core' + _type = "Core.Models.Record.Record, Core" def __init__(self, app, raw): super(Record, self).__init__(app._swimlane, raw) self.__app = app - self.is_new = self._raw.get('isNew', False) + self.is_new = self._raw.get("isNew", False) # Protect against creation from generic raw data not yet containing server-generated values if self.is_new: self.id = self.tracking_id = self.created = self.modified = None else: - record_app_id = raw['applicationId'] + record_app_id = raw["applicationId"] if record_app_id != app.id: - raise ValueError('Record applicationId "{}" does not match source app id "{}"'.format( - record_app_id, - app.id - )) + raise ValueError( + 'Record applicationId "{}" does not match source app id "{}"'.format( + record_app_id, app.id + ) + ) - self.id = self._raw['id'] + self.id = self._raw["id"] # Combine app acronym + trackingId instead of using trackingFull raw # for guaranteed value (not available through report results) - self.tracking_id = '-'.join([ - self.app.acronym, - str(int(self._raw['trackingId'])) - ]) + self.tracking_id = "-".join([self.app.acronym, str(int(self._raw["trackingId"]))]) - self.created = pendulum.parse(self._raw['createdDate']) - self.modified = pendulum.parse(self._raw['modifiedDate']) + self.created = pendulum.parse(self._raw["createdDate"]) + self.modified = pendulum.parse(self._raw["modifiedDate"]) self.__allowed = [] @@ -62,20 +59,23 @@ def __init__(self, app, raw): self.__premap_fields() # Get trackingFull if available - if app.tracking_id in self._raw['values']: - self._raw['trackingFull'] = self._raw['values'].get(app.tracking_id) + if app.tracking_id in self._raw["values"]: + self._raw["trackingFull"] = self._raw["values"].get(app.tracking_id) - self.__existing_values = {k: self.get_field(k).get_batch_representation() for (k, v) in self} + self.__existing_values = { + k: self.get_field(k).get_batch_representation() for (k, v) in self + } self._comments_modified = False # If record is locked parse the data - self.locked = self._raw.get('locked', False) + self.locked = self._raw.get("locked", False) if self.locked: - self.locking_user = User(self._swimlane, self._raw.get('locking_user')) - self.locked_date = pendulum.parse(self._raw.get('locked_date')) + self.locking_user = self._raw.get("locking_user") + self.locked_date = pendulum.parse(self._raw.get("locked_date")) # avoid circular reference from swimlane.core.adapters import RecordRevisionAdapter + self.revisions = RecordRevisionAdapter(app, self) @property @@ -84,13 +84,13 @@ def app(self): def __str__(self): if self.is_new: - return '{} - New'.format(self.app.acronym) + return "{} - New".format(self.app.acronym) return str(self.tracking_id) def __setitem__(self, field_name, value): keys = dir(value) - if '_elements' in keys: + if "_elements" in keys: value = value._elements self.get_field(field_name).set_python(value) @@ -109,13 +109,14 @@ def __hash__(self): def __lt__(self, other): if not isinstance(other, self.__class__): - raise TypeError('Comparisons not supported between instances of "{}" and "{}"'.format( - other.__class__.__name__, - self.__class__.__name__ - )) + raise TypeError( + 'Comparisons not supported between instances of "{}" and "{}"'.format( + other.__class__.__name__, self.__class__.__name__ + ) + ) - tracking_number_self = int(self.tracking_id.split('-')[1]) - tracking_number_other = int(other.tracking_id.split('-')[1]) + tracking_number_self = int(self.tracking_id.split("-")[1]) + tracking_number_other = int(other.tracking_id.split("-")[1]) return (self.app.name, tracking_number_self) < (other.app.name, tracking_number_other) @@ -127,11 +128,11 @@ def __premap_fields(self): # Circular imports from swimlane.core.fields import resolve_field_class - for field_definition in self.app._raw['fields']: + for field_definition in self.app._raw["fields"]: field_class = resolve_field_class(field_definition) - field_instance = field_class(field_definition['name'], self) - value = self._raw['values'].get(field_instance.id) + field_instance = field_class(field_definition["name"], self) + value = self._raw["values"].get(field_instance.id) field_instance.set_swimlane(value) self._fields[field_instance.name] = field_instance @@ -141,10 +142,7 @@ def get_cache_index_keys(self): if not (self.id and self.tracking_id): raise NotImplementedError - return { - 'id': self.id, - 'tracking_id': self.tracking_id - } + return {"id": self.id, "tracking_id": self.tracking_id} def get_field(self, field_name): """Get field instance used to get, set, and serialize internal field value @@ -174,15 +172,10 @@ def validate(self): """ for field in (_field for _field in six.itervalues(self._fields) if _field.required): if field.get_swimlane() is None: - raise ValidationError( - self, 'Required field "{}" is not set'.format(field.name)) + raise ValidationError(self, 'Required field "{}" is not set'.format(field.name)) def __request_and_reinitialize(self, method, endpoint, data): - response = self._swimlane.request( - method, - endpoint, - json=data - ) + response = self._swimlane.request(method, endpoint, json=data) # Reinitialize record with new raw content returned from server to update any calculated fields self.__init__(self.app, response.json()) @@ -201,25 +194,21 @@ def save(self): """ if self.is_new: - method = 'post' + method = "post" else: - method = 'put' + method = "put" # Pop off fields with None value to allow for saving empty fields copy_raw = copy.copy(self._raw) values_dict = {} - for key, value in six.iteritems(copy_raw['values']): + for key, value in six.iteritems(copy_raw["values"]): if value is not None: values_dict[key] = value - copy_raw['values'] = values_dict + copy_raw["values"] = values_dict self.validate() - self.__request_and_reinitialize( - method, - 'app/{}/record'.format(self.app.id), - copy_raw - ) + self.__request_and_reinitialize(method, "app/{}/record".format(self.app.id), copy_raw) def patch(self): """Patch record on Swimlane server @@ -228,36 +217,35 @@ def patch(self): ValueError: If record.is_new, or if comments or attachments are attempted to be patched """ if self.is_new: - raise ValueError('Cannot patch a new Record') + raise ValueError("Cannot patch a new Record") elif self._comments_modified: - raise ValueError('Can not patch with added comments') + raise ValueError("Can not patch with added comments") copy_raw = copy.copy(self._raw) pending_values = {k: self.get_field(k).get_batch_representation() for (k, v) in self} patch_values = { - self.get_field(k).id: pending_values[k] for k in set(pending_values) & set(self.__existing_values) + self.get_field(k).id: pending_values[k] + for k in set(pending_values) & set(self.__existing_values) if pending_values[k] != self.__existing_values[k] } for field_id, value in six.iteritems(patch_values): # - if self.app.get_field_definition_by_id(field_id)['fieldType'] == 'attachment': - raise ValueError('Can not patch new attachments') + if self.app.get_field_definition_by_id(field_id)["fieldType"] == "attachment": + raise ValueError("Can not patch new attachments") # Use None for empty arrays to ensure field is removed from Record on PATCH if not value and value != 0: patch_values[field_id] = None # $type needed here for dotnet to deserialize correctly - patch_values['$type'] = self._raw['values']['$type'] - copy_raw['values'] = patch_values + patch_values["$type"] = self._raw["values"]["$type"] + copy_raw["values"] = patch_values self.validate() self.__request_and_reinitialize( - 'patch', - 'app/{}/record/{}'.format(self.app.id, self.id), - copy_raw + "patch", "app/{}/record/{}".format(self.app.id, self.id), copy_raw ) def delete(self): @@ -271,19 +259,16 @@ def delete(self): ValueError: If record.is_new """ if self.is_new: - raise ValueError('Cannot delete a new Record') + raise ValueError("Cannot delete a new Record") - self._swimlane.request( - 'delete', - 'app/{}/record/{}'.format(self.app.id, self.id) - ) + self._swimlane.request("delete", "app/{}/record/{}".format(self.app.id, self.id)) del self._swimlane.resources_cache[self] # Modify current raw values indicating an unsaved record but persisting field data raw = copy.deepcopy(self._raw) - raw['id'] = None - raw['isNew'] = True + raw["id"] = None + raw["isNew"] = True self.__init__(self.app, raw) @@ -310,7 +295,7 @@ def for_json(self, *field_names): @property def restrictions(self): """Returns cached set of retrieved UserGroups in the record's list of allowed accounts""" - return [UserGroup(self._swimlane, raw) for raw in self._raw['allowed']] + return [UserGroup(self._swimlane, raw) for raw in self._raw["allowed"]] def add_restriction(self, *usergroups): """Add UserGroup(s) to list of accounts with access to record @@ -328,15 +313,13 @@ def add_restriction(self, *usergroups): TypeError: If 0 UserGroups provided or provided a non-UserGroup instance """ if not usergroups: - raise TypeError( - 'Must provide at least one UserGroup for restriction') + raise TypeError("Must provide at least one UserGroup for restriction") - allowed = copy.copy(self._raw.get('allowed', [])) + allowed = copy.copy(self._raw.get("allowed", [])) for usergroup in usergroups: if not isinstance(usergroup, UserGroup): - raise TypeError( - 'Expected UserGroup, received "{}" instead'.format(usergroup)) + raise TypeError('Expected UserGroup, received "{}" instead'.format(usergroup)) selection = usergroup.as_usergroup_selection() if selection not in allowed: @@ -344,12 +327,10 @@ def add_restriction(self, *usergroups): self.validate() self._swimlane.request( - 'put', - 'app/{}/record/{}/restrict'.format(self.app.id, self.id), - json=allowed + "put", "app/{}/record/{}/restrict".format(self.app.id, self.id), json=allowed ) - self._raw['allowed'] = allowed + self._raw["allowed"] = allowed def remove_restriction(self, *usergroups): """Remove UserGroup(s) from list of accounts with access to record @@ -369,28 +350,26 @@ def remove_restriction(self, *usergroups): ValueError: If provided UserGroup not in current restriction list """ if usergroups: - allowed = copy.copy(self._raw.get('allowed', [])) + allowed = copy.copy(self._raw.get("allowed", [])) for usergroup in usergroups: if not isinstance(usergroup, UserGroup): - raise TypeError( - 'Expected UserGroup, received "{}" instead'.format(usergroup)) + raise TypeError('Expected UserGroup, received "{}" instead'.format(usergroup)) try: allowed.remove(usergroup.as_usergroup_selection()) except ValueError: raise ValueError( - 'UserGroup "{}" not in record "{}" restriction list'.format(usergroup, self)) + 'UserGroup "{}" not in record "{}" restriction list'.format(usergroup, self) + ) else: allowed = [] self.validate() self._swimlane.request( - 'put', - 'app/{}/record/{}/restrict'.format(self.app.id, self.id), - json=allowed + "put", "app/{}/record/{}/restrict".format(self.app.id, self.id), json=allowed ) - self._raw['allowed'] = allowed + self._raw["allowed"] = allowed def lock(self): """ @@ -405,13 +384,11 @@ def lock(self): """ self.validate() response = self._swimlane.request( - 'post', - 'app/{}/record/{}/lock'.format( - self.app.id, self.id) + "post", "app/{}/record/{}/lock".format(self.app.id, self.id) ).json() self.locked = True - self.locking_user = User(self._swimlane, response['lockingUser']) - self.locked_date = response['lockedDate'] + self.locking_user = User(self._swimlane, response["lockingUser"]) + self.locked_date = response["lockedDate"] def unlock(self): """ @@ -426,29 +403,32 @@ def unlock(self): """ self.validate() self._swimlane.request( - 'post', - 'app/{}/record/{}/unlock'.format( - self.app.id, self.id) + "post", "app/{}/record/{}/unlock".format(self.app.id, self.id) ).json() - self.locked = False + self.locked = False self.locking_user = None self.locked_date = None - + def execute_task(self, task_name, timeout=int(20)): - job_info = swimlane.core.adapters.task.TaskAdapter(self.app._swimlane).execute(task_name, self._raw) + job_info = swimlane.core.adapters.task.TaskAdapter(self.app._swimlane).execute( + task_name, self._raw + ) timeout_start = pendulum.now() while pendulum.now() < timeout_start.add(seconds=timeout): status = self.app._swimlane.helpers.check_bulk_job_status(job_info.text) if len(status): for item in status: - if item.get('status') == 'completed': + if item.get("status") == "completed": self.__request_and_reinitialize( - 'get', '/app/{appId}/record/{id}'.format(appId=self.app.id, id=self.id), None) + "get", + "/app/{appId}/record/{id}".format(appId=self.app.id, id=self.id), + None, + ) timeout = 0 - if item.get('status') == 'failed': - raise SwimlaneException('Task failed: {}'.format(item.get('message'))) + if item.get("status") == "failed": + raise SwimlaneException("Task failed: {}".format(item.get("message"))) time.sleep(1) - + def record_factory(app, fields=None): """Return a temporary Record instance to be used for field validation and value parsing @@ -461,17 +441,20 @@ def record_factory(app, fields=None): Record: Unsaved Record instance to be used for validation, creation, etc. """ # pylint: disable=line-too-long - record = Record(app, { - '$type': Record._type, - 'isNew': True, - 'applicationId': app.id, - 'comments': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], mscorlib]], mscorlib' + record = Record( + app, + { + "$type": Record._type, + "isNew": True, + "applicationId": app.id, + "comments": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], mscorlib]], mscorlib" + }, + "values": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib" + }, }, - 'values': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib' - } - }) + ) fields = fields or {} @@ -486,9 +469,9 @@ def record_factory(app, fields=None): # Pop off fields with None value to allow for saving empty fields copy_raw = copy.copy(record._raw) values_dict = {} - for key, value in six.iteritems(copy_raw['values']): + for key, value in six.iteritems(copy_raw["values"]): if value is not None: values_dict[key] = value - record._raw['values'] = values_dict + record._raw["values"] = values_dict return record