From 526278a6b4e162ab6bfc49bd1713e629d61e58e9 Mon Sep 17 00:00:00 2001 From: Michael Butler Date: Thu, 14 Mar 2019 13:00:04 -0400 Subject: [PATCH 01/17] SPT-4113 possible fix Trying my hand at updating this. The update to history fixes the issue we were having, and I added an assert to the mock test but I feel like Im missing something else to actually make that test pass. Cant wrap my head around how it will grab an application history revision. I welcome any pointers on what I could have done better. I wanted to find a way to keep a copy of the current app_revision to pass down the list. Then we could check the app version number instead of having to pull the same version of the app over and over. Couldnt see a way to do that without a much larger overhaul. Hopefully I'm wrong and there is a more efficient way to do it. --- swimlane/core/fields/history.py | 14 +++++++++++++- tests/fields/test_history.py | 2 ++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/swimlane/core/fields/history.py b/swimlane/core/fields/history.py index 9bc71337..418bd0a0 100644 --- a/swimlane/core/fields/history.py +++ b/swimlane/core/fields/history.py @@ -48,12 +48,24 @@ def __init__(self, record, raw): self.revision_number = int(self._raw['revisionNumber']) self.status = self._raw['status'] + #Get Correct App Revision for History Field + from swimlane.core.resources.app import App + self.app_revision_number = int(self._raw['version']['applicationRevision']) + self._app_revision_raw = self._swimlane.request( + 'get', + '/app/{}/history/{}'.format( + self.record.app.id, + self.app_revision_number)).json() + self.app_revision = App(self._swimlane, self._app_revision_raw['version']) + + + # UserGroupSelection, can't set as User without additional lookup self.user = UserGroup(self._swimlane, self._raw['userId']) # Avoid circular imports from swimlane.core.resources.record import Record - self.version = Record(self.record.app, self._raw['version']) + self.version = Record(self.app_revision, self._raw['version']) def __str__(self): return '{} ({})'.format(self.version, self.revision_number) diff --git a/tests/fields/test_history.py b/tests/fields/test_history.py index c5cf1a48..13e15a20 100644 --- a/tests/fields/test_history.py +++ b/tests/fields/test_history.py @@ -4,6 +4,7 @@ from swimlane.core.fields.history import RevisionCursor, Revision from swimlane.core.resources.usergroup import UserGroup +from swimlane.core.resources.app import App raw_revision_data = [ {'$type': 'Core.Models.History.Revision, Core', @@ -481,6 +482,7 @@ def test_history_field(mock_record, mock_swimlane): for idx, revision in enumerate(history): assert isinstance(revision, Revision) assert str(revision) == 'RA-7 ({})'.format(revision.revision_number) + assert isinstance(revision.app_revision, App) assert isinstance(revision.modified_date, datetime) assert isinstance(revision.user, UserGroup) assert revision.version.id == mock_record.id From 5204c598d9637f79f12e39b15166c6b53401c5c1 Mon Sep 17 00:00:00 2001 From: Michael Butler Date: Mon, 8 Apr 2019 12:13:00 -0400 Subject: [PATCH 02/17] Fixed white spaces --- swimlane/core/fields/history.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/swimlane/core/fields/history.py b/swimlane/core/fields/history.py index 418bd0a0..f6dd55a7 100644 --- a/swimlane/core/fields/history.py +++ b/swimlane/core/fields/history.py @@ -58,8 +58,6 @@ def __init__(self, record, raw): self.app_revision_number)).json() self.app_revision = App(self._swimlane, self._app_revision_raw['version']) - - # UserGroupSelection, can't set as User without additional lookup self.user = UserGroup(self._swimlane, self._raw['userId']) From 15c8797fcb7a5016bcb774c31342476fbf71893b Mon Sep 17 00:00:00 2001 From: Michael Butler Date: Mon, 8 Apr 2019 14:53:14 -0400 Subject: [PATCH 03/17] Added history properties --- swimlane/core/fields/history.py | 42 ++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/swimlane/core/fields/history.py b/swimlane/core/fields/history.py index f6dd55a7..488bccd5 100644 --- a/swimlane/core/fields/history.py +++ b/swimlane/core/fields/history.py @@ -41,6 +41,8 @@ class Revision(APIResource): def __init__(self, record, raw): super(Revision, self).__init__(record._swimlane, raw) + self.__app_revision = None + self.__version = None self.record = record @@ -48,26 +50,38 @@ def __init__(self, record, raw): self.revision_number = int(self._raw['revisionNumber']) self.status = self._raw['status'] - #Get Correct App Revision for History Field - from swimlane.core.resources.app import App - self.app_revision_number = int(self._raw['version']['applicationRevision']) - self._app_revision_raw = self._swimlane.request( - 'get', - '/app/{}/history/{}'.format( - self.record.app.id, - self.app_revision_number)).json() - self.app_revision = App(self._swimlane, self._app_revision_raw['version']) - # UserGroupSelection, can't set as User without additional lookup self.user = UserGroup(self._swimlane, self._raw['userId']) - # Avoid circular imports - from swimlane.core.resources.record import Record - self.version = Record(self.app_revision, self._raw['version']) - def __str__(self): return '{} ({})'.format(self.version, self.revision_number) + @property + def app_revision(self): + """Deferring request for app revision until needed""" + if not self.__app_revision: + # Avoid circular imports + from swimlane.core.resources.app import App + + app_revision_number = int(self._raw['version']['applicationRevision']) + app_revision_raw = self._swimlane.request( + 'get', + '/app/{}/history/{}'.format( + self.record.app.id, + app_revision_number)).json() + self.__app_revision = App(self._swimlane, app_revision_raw['version']) + return self.__app_revision + + @property + def version(self): + """Deferring for sake of app revision""" + if not self.__version: + # Avoid circular imports + from swimlane.core.resources.record import Record + + self.__version = Record(self.app_revision, self._raw['version']) + return self.__version + class HistoryField(ReadOnly, CursorField): From d82422f7ee6ca2e98d5da5981734f522e897191d Mon Sep 17 00:00:00 2001 From: Jon Ford Date: Thu, 9 May 2019 15:39:32 -0400 Subject: [PATCH 04/17] add better mock data, get tests running --- tests/fields/test_history.py | 1176 ++++++++++++++++++++-------------- 1 file changed, 711 insertions(+), 465 deletions(-) diff --git a/tests/fields/test_history.py b/tests/fields/test_history.py index 13e15a20..070377d5 100644 --- a/tests/fields/test_history.py +++ b/tests/fields/test_history.py @@ -1,490 +1,736 @@ from datetime import datetime import mock +import pytest from swimlane.core.fields.history import RevisionCursor, Revision from swimlane.core.resources.usergroup import UserGroup from swimlane.core.resources.app import App +from swimlane.core.resources.record import Record -raw_revision_data = [ - {'$type': 'Core.Models.History.Revision, Core', - 'modifiedDate': '2017-04-10T16:26:17.065Z', - 'revisionNumber': 3.0, - 'status': 'historical', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'version': {'$type': 'Core.Models.Record.Record, Core', - 'actionsExecuted': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - '58e557c8b81ce3e2c6515fd1': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.389Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6ac5c88412c4acda3d66c': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.396Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6aca5293267c35188ea37': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.401Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6acc099097a98678ccfcf': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.407Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6bee81f119baf40a659b9': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.984Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c62f257f2c4994b84140': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.412Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c6536afac95743437254': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.419Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c83415177d0ad68ea961': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.426Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e7bb9ad6e4d34e8f557b2b': True, - '58e7bbba9bc82babdd655653': True, - '58e7bd86ad9ba1f93ef4afae': True, - '58e7bddc6ed1ceda2950b621': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.437Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}}, - 'allowed': [], - 'applicationId': '58e4bb4407637a0e4c4f9873', - 'applicationRevision': 72.0, - 'comments': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], mscorlib]], mscorlib'}, - 'createdByUser': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'createdDate': '2017-04-10T16:26:16.486Z', - 'disabled': False, - 'id': '58ebb22807637a02d4a14bd6', - 'isNew': False, - 'linkedData': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - 'a99ut': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 10, - 'dow': 1, - 'h': 16, - 'm': 26, - 'mm': 4, - 'q': 2, - 'w': 15, - 'y': 2017}, - 'ac1oa': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 5, - 'dow': 3, - 'h': 23, - 'm': 42, - 'mm': 4, - 'q': 2, - 'w': 14, - 'y': 2017}, - 'aiir8': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 10, - 'dow': 1, - 'h': 16, - 'm': 26, - 'mm': 4, - 'q': 2, - 'w': 15, - 'y': 2017}}, - 'modifiedByUser': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'modifiedDate': '2017-04-10T16:26:16.999Z', - 'referencedByIds': [], - 'referencedRecordIds': [], - 'sessionTimeSpent': 0, - 'timeTrackingEnabled': True, - 'totalTimeSpent': 0, - 'trackingFull': 'RA-7', - 'trackingId': 7.0, - 'values': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - '58e4bb4407637a0e4c4f9875': 'RA-7', - 'a04rv': '10.0.1.184:56005:3172', - 'a0g40': 'Alert Action', - 'a0k7v': {'$type': 'Core.Models.Record.ValueSelection, Core', - 'id': '58e7bae8c61082e377cc4d82', - 'value': 'Open'}, - 'a18qr': 'User.Activity.Failed Logins', - 'a1r3v': 'Logon', - 'a3rvw': '3172', - 'a4l9p': 'logdec1', - 'a5v09': {'$type': 'Core.Models.Record.ValueSelection, Core', - 'id': '58e7bafd83f418637cf91c3e', - 'value': 'ESA Alert'}, - 'a5yg3': '3172', - 'a6if8': '[{"lifetime": "60", "rid": "4531291", "payload": "7808", "size": "11956", "service": "0", "eth.src": "00:0C:29:5B:98:5E", "feed.name": "Swimlane_Feed", "medium": "1", "ip.dst": "178.238.235.143", "org.dst": "Contabo GmbH", "sessionid": "4534669", "latdec.dst": "51.2993", "eth.type": "2048", "ip.src": "10.0.1.183", "domain.dst": "contabo.host", "eth.dst": "00:50:56:A7:4D:79", "did": "packdec1", "longdec.dst": "9.491", "packets": "122", "streams": "2", "country.dst": "Germany", "time": 1491593904, "ip.proto": "1"}]', - 'a7gm5': '<14>Apr 5 23:42:54 localhost CEF:0|RSA|Security Analytics ESA|10.4|Module_58e40ea9e4b070dfd2a59ad5_Alert|Alert Action|7|rt=2017-04-05T23:42Z id=890eb2b1-2d90-46a5-b1ed-6e58b408ed83 source=10.0.1.184:56005:3172 action=authentication failure;; client=sshd;; device_class=Unix;; device_ip=10.0.1.178;; device_type=crossbeamc;; did=logdec1;; ec_activity=Logon;; ec_outcome=Failure;; ec_subject=User;; ec_theme=Authentication;; esa_time=1491435774280;; event_cat_name=User.Activity.Failed Logins;; event_source_id=10.0.1.184:56005:3172;; header_id=0006;; host_src=178.238.235.143;; level=4;; medium=32;; msg_id=000103;; rid=3172;; sessionid=3172;; size=175;; time=1491435771;; user_dst=user;; username=0;;\x00', - 'a7ira': 'User', - 'a8vjm': 'Failure', - 'a990o': '7', - 'a99ut': '2017-04-10T16:26:17.457Z', - 'aasnf': {'$type': 'Core.Models.Record.ValueSelection, Core', - 'id': '58e7bb144121bd73d8f53845', - 'value': 'High'}, - 'ac1oa': '2017-04-05T23:42:54Z', - 'acqdq': '1491435774280', - 'af967': '890eb2b1-2d90-46a5-b1ed-6e58b408ed83', - 'afbbv': '', - 'afff3': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'ahziu': '178.238.235.143', - 'ai1zz': '4', - 'aiir8': '2017-04-10T16:26:16.389Z', - 'aio1w': 'sshd', - 'aokp5': 'Unix', - 'apf9i': '10.0.1.178', - 'arnd6': 'authentication failure', - 'arok4': 'crossbeamc', - 'aw9gd': 'ip.src=10.0.1.178 || ip.dst=10.0.1.178', - 'awmtq': 'Authentication', - 'axdvr': '175'}, - 'visualizations': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], mscorlib]], mscorlib'}}}, - {'$type': 'Core.Models.History.Revision, Core', - 'modifiedDate': '2017-04-10T16:26:16.998Z', - 'revisionNumber': 2.0, - 'status': 'historical', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'version': {'$type': 'Core.Models.Record.Record, Core', - 'actionsExecuted': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - '58e557c8b81ce3e2c6515fd1': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.389Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6ac5c88412c4acda3d66c': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.396Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6aca5293267c35188ea37': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.401Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6acc099097a98678ccfcf': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.407Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c62f257f2c4994b84140': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.412Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c6536afac95743437254': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.419Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c83415177d0ad68ea961': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.426Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e7bb9ad6e4d34e8f557b2b': True, - '58e7bbba9bc82babdd655653': True, - '58e7bd86ad9ba1f93ef4afae': True, - '58e7bddc6ed1ceda2950b621': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.437Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}}, - 'allowed': [], - 'applicationId': '58e4bb4407637a0e4c4f9873', - 'applicationRevision': 72.0, - 'comments': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], mscorlib]], mscorlib'}, - 'createdByUser': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'createdDate': '2017-04-10T16:26:16.486Z', - 'disabled': False, - 'id': '58ebb22807637a02d4a14bd6', - 'isNew': False, - 'linkedData': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - 'a99ut': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 10, - 'dow': 1, - 'h': 16, - 'm': 26, - 'mm': 4, - 'q': 2, - 'w': 15, - 'y': 2017}, - 'ac1oa': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 5, - 'dow': 3, - 'h': 23, - 'm': 42, - 'mm': 4, - 'q': 2, - 'w': 14, - 'y': 2017}, - 'aiir8': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 10, - 'dow': 1, - 'h': 16, - 'm': 26, - 'mm': 4, - 'q': 2, - 'w': 15, - 'y': 2017}}, - 'modifiedByUser': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'modifiedDate': '2017-04-10T16:26:16.954Z', - 'referencedByIds': [], - 'referencedRecordIds': [], - 'sessionTimeSpent': 0, - 'timeTrackingEnabled': True, - 'totalTimeSpent': 0, - 'trackingFull': 'RA-7', - 'trackingId': 7.0, - 'values': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - '58e4bb4407637a0e4c4f9875': 'RA-7', - 'a04rv': '10.0.1.184:56005:3172', - 'a0g40': 'Alert Action', - 'a0k7v': {'$type': 'Core.Models.Record.ValueSelection, Core', - 'id': '58e7bae8c61082e377cc4d82', - 'value': 'Open'}, - 'a18qr': 'User.Activity.Failed Logins', - 'a1r3v': 'Logon', - 'a3rvw': '3172', - 'a4l9p': 'logdec1', - 'a5v09': {'$type': 'Core.Models.Record.ValueSelection, Core', - 'id': '58e7bafd83f418637cf91c3e', - 'value': 'ESA Alert'}, - 'a5yg3': '3172', - 'a7gm5': '<14>Apr 5 23:42:54 localhost CEF:0|RSA|Security Analytics ESA|10.4|Module_58e40ea9e4b070dfd2a59ad5_Alert|Alert Action|7|rt=2017-04-05T23:42Z id=890eb2b1-2d90-46a5-b1ed-6e58b408ed83 source=10.0.1.184:56005:3172 action=authentication failure;; client=sshd;; device_class=Unix;; device_ip=10.0.1.178;; device_type=crossbeamc;; did=logdec1;; ec_activity=Logon;; ec_outcome=Failure;; ec_subject=User;; ec_theme=Authentication;; esa_time=1491435774280;; event_cat_name=User.Activity.Failed Logins;; event_source_id=10.0.1.184:56005:3172;; header_id=0006;; host_src=178.238.235.143;; level=4;; medium=32;; msg_id=000103;; rid=3172;; sessionid=3172;; size=175;; time=1491435771;; user_dst=user;; username=0;;\x00', - 'a7ira': 'User', - 'a8vjm': 'Failure', - 'a990o': '7', - 'a99ut': '2017-04-10T16:26:17.457Z', - 'aasnf': {'$type': 'Core.Models.Record.ValueSelection, Core', - 'id': '58e7bb144121bd73d8f53845', - 'value': 'High'}, - 'ac1oa': '2017-04-05T23:42:54Z', - 'acqdq': '1491435774280', - 'af967': '890eb2b1-2d90-46a5-b1ed-6e58b408ed83', - 'afbbv': '', - 'afff3': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'ahziu': '178.238.235.143', - 'ai1zz': '4', - 'aiir8': '2017-04-10T16:26:16.389Z', - 'aio1w': 'sshd', - 'aokp5': 'Unix', - 'apf9i': '10.0.1.178', - 'arnd6': 'authentication failure', - 'arok4': 'crossbeamc', - 'aw9gd': 'ip.src=10.0.1.178 || ip.dst=10.0.1.178', - 'awmtq': 'Authentication', - 'axdvr': '175'}, - 'visualizations': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], mscorlib]], mscorlib'}}}, - {'$type': 'Core.Models.History.Revision, Core', - 'modifiedDate': '2017-04-10T16:26:16.953Z', - 'revisionNumber': 1.0, - 'status': 'historical', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'version': {'$type': 'Core.Models.Record.Record, Core', - 'actionsExecuted': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - '58e557c8b81ce3e2c6515fd1': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.389Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6ac5c88412c4acda3d66c': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.396Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6aca5293267c35188ea37': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.401Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6acc099097a98678ccfcf': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.407Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c62f257f2c4994b84140': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.412Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c6536afac95743437254': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.419Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c83415177d0ad68ea961': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.426Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e7bddc6ed1ceda2950b621': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.437Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}}, - 'allowed': [], - 'applicationId': '58e4bb4407637a0e4c4f9873', - 'applicationRevision': 72.0, - 'comments': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], mscorlib]], mscorlib'}, - 'createdByUser': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'createdDate': '2017-04-10T16:26:16.486Z', - 'disabled': False, - 'id': '58ebb22807637a02d4a14bd6', - 'isNew': False, - 'linkedData': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - 'a99ut': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 10, - 'dow': 1, - 'h': 16, - 'm': 26, - 'mm': 4, - 'q': 2, - 'w': 15, - 'y': 2017}, - 'aiir8': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 10, - 'dow': 1, - 'h': 16, - 'm': 26, - 'mm': 4, - 'q': 2, - 'w': 15, - 'y': 2017}}, - 'modifiedByUser': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'modifiedDate': '2017-04-10T16:26:16.486Z', - 'referencedByIds': [], - 'referencedRecordIds': [], - 'sessionTimeSpent': 0, - 'timeTrackingEnabled': True, - 'totalTimeSpent': 0, - 'trackingFull': 'RA-7', - 'trackingId': 7.0, - 'values': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - '58e4bb4407637a0e4c4f9875': 'RA-7', - 'a04rv': '10.0.1.184:56005:3172', - 'a0g40': 'Alert Action', - 'a18qr': 'User.Activity.Failed Logins', - 'a1r3v': 'Logon', - 'a3rvw': '3172', - 'a4l9p': 'logdec1', - 'a5yg3': '3172', - 'a7gm5': '<14>Apr 5 23:42:54 localhost CEF:0|RSA|Security Analytics ESA|10.4|Module_58e40ea9e4b070dfd2a59ad5_Alert|Alert Action|7|rt=2017-04-05T23:42Z id=890eb2b1-2d90-46a5-b1ed-6e58b408ed83 source=10.0.1.184:56005:3172 action=authentication failure;; client=sshd;; device_class=Unix;; device_ip=10.0.1.178;; device_type=crossbeamc;; did=logdec1;; ec_activity=Logon;; ec_outcome=Failure;; ec_subject=User;; ec_theme=Authentication;; esa_time=1491435774280;; event_cat_name=User.Activity.Failed Logins;; event_source_id=10.0.1.184:56005:3172;; header_id=0006;; host_src=178.238.235.143;; level=4;; medium=32;; msg_id=000103;; rid=3172;; sessionid=3172;; size=175;; time=1491435771;; user_dst=user;; username=0;;\x00', - 'a7ira': 'User', - 'a8vjm': 'Failure', - 'a990o': '7', - 'a99ut': '2017-04-10T16:26:17.457Z', - 'acqdq': '1491435774280', - 'af967': '890eb2b1-2d90-46a5-b1ed-6e58b408ed83', - 'afbbv': '', - 'afff3': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'ahziu': '178.238.235.143', - 'ai1zz': '4', - 'aiir8': '2017-04-10T16:26:16.389Z', - 'aio1w': 'sshd', - 'aokp5': 'Unix', - 'apf9i': '10.0.1.178', - 'arnd6': 'authentication failure', - 'arok4': 'crossbeamc', - 'aw9gd': 'ip.src=10.0.1.178 || ip.dst=10.0.1.178', - 'awmtq': 'Authentication', - 'axdvr': '175'}, - 'visualizations': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], mscorlib]], mscorlib'}}}] +raw_app_data = { + "$type":"Core.Models.Application.Application, Core", + "acronym":"PHT", + "trackingFieldId":"5cd46fce433cf20015dd46f7", + "layout":[ + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"agy01", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fdac741d5d54a2b5c32", + "row":1, + "col":1, + "sizex":2.0, + "sizey":0.0 + }, + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"axtys", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fddb36d66d467d4abe6", + "row":1, + "col":3, + "sizex":2.0, + "sizey":0.0 + } + ], + "fields":[ + { + "$type":"Core.Models.Fields.History.HistoryField, Core", + "id":"axtys", + "name":"History", + "key":"history", + "fieldType":"history", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.ValuesListField, Core", + "values":[ + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe3ca4746cda7e41a33", + "name":"value1", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + } + ], + "controlType":"select", + "selectionType":"single", + "id":"agy01", + "name":"Selection", + "key":"selection", + "fieldType":"valuesList", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.TrackingField, Core", + "prefix":"PHT-", + "id":"5cd46fce433cf20015dd46f7", + "name":"Tracking Id", + "key":"tracking-id", + "fieldType":"tracking", + "readOnly":True, + "supportsMultipleOutputMappings":False + } + ], + "maxTrackingId":1.0, + "workspaces":[ + "5cd46fce433cf20015dd4727" + ], + "createWorkspace":False, + "createdDate":"2019-05-09T18:22:06.181Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:24:02.642Z", + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "timeTrackingEnabled":False, + "permissions":{ + "$type":"Core.Models.Security.PermissionMatrix, Core" + }, + "id":"a34xbNOoo2P3ivyjY", + "name":"Pydriver History Test", + "disabled":False +} +raw_record_data = { + "$type":"Core.Models.Record.Record, Core", + "name":"PHT-1", + "allowed":[ -def test_history_field(mock_record, mock_swimlane): - history = mock_record['History'] - assert isinstance(history, RevisionCursor) + ], + "trackingId":1.0, + "trackingFull":"PHT-1", + "applicationId":"a34xbNOoo2P3ivyjY", + "isNew":False, + "values":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01":{ + "$type":"Core.Models.Record.ValueSelection, Core", + "id":"5cd46fe3ca4746cda7e41a33", + "value":"value1" + }, + "5cd46fce433cf20015dd46f7":"PHT-1" + }, + "repeatFieldCurrentValues":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "actionsExecuted":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "visualizations":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "applicationRevision":3.0, + "locked":False, + "comments":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "createdDate":"2019-05-09T18:23:39.881Z", + "modifiedDate":"2019-05-09T18:24:20.027Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "sessionTimeSpent":0, + "totalTimeSpent":0, + "timeTrackingEnabled":True, + "isHangfireCreatedAndUnpersisted":False, + "infiniteLoopFlag":False, + "id":"aPkMtjrzV54YxyS59", + "disabled":False +} + +raw_record_revision_data = [ + { + "$type":"Core.Models.History.Revision, Core", + "revisionNumber":3.0, + "status":"current", + "userId":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:24:20.027Z", + "version":{ + "$type":"Core.Models.Record.Record, Core", + "name":"PHT-1", + "allowed":[ + + ], + "trackingId":1.0, + "trackingFull":"PHT-1", + "applicationId":"a34xbNOoo2P3ivyjY", + "referencedRecordIds":[ + + ], + "referencedByIds":[ + + ], + "isNew":False, + "values":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01":{ + "$type":"Core.Models.Record.ValueSelection, Core", + "id":"5cd46fe3ca4746cda7e41a33", + "value":"value1" + }, + "5cd46fce433cf20015dd46f7":"PHT-1" + }, + "repeatFieldCurrentValues":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "valuesDocument":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01":{ + "$type":"Core.Models.Record.ValueSelection, Core", + "id":"5cd46fe3ca4746cda7e41a33", + "value":"value1" + }, + "5cd46fce433cf20015dd46f7":"PHT-1" + }, + "actionsExecuted":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "visualizations":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "applicationRevision":3.0, + "locked":False, + "comments":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "createdDate":"2019-05-09T18:23:39.881Z", + "modifiedDate":"2019-05-09T18:24:20.027Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "sessionTimeSpent":0, + "totalTimeSpent":0, + "timeTrackingEnabled":True, + "isHangfireCreatedAndUnpersisted":False, + "infiniteLoopFlag":False, + "id":"aPkMtjrzV54YxyS59", + "disabled":False + } + }, + { + "$type":"Core.Models.History.Revision, Core", + "revisionNumber":2.0, + "status":"historical", + "userId":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:23:48.539Z", + "version":{ + "$type":"Core.Models.Record.Record, Core", + "name":"PHT-1", + "allowed":[ + + ], + "trackingId":1.0, + "trackingFull":"PHT-1", + "applicationId":"a34xbNOoo2P3ivyjY", + "referencedRecordIds":[ + + ], + "referencedByIds":[ + + ], + "isNew":False, + "values":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01":{ + "$type":"Core.Models.Record.ValueSelection, Core", + "id":"5cd46fe379be68acd2039061", + "value":"value2" + }, + "5cd46fce433cf20015dd46f7":"PHT-1" + }, + "repeatFieldCurrentValues":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "actionsExecuted":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "visualizations":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "applicationRevision":2.0, + "locked":False, + "comments":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "createdDate":"2019-05-09T18:23:39.881Z", + "modifiedDate":"2019-05-09T18:23:48.539Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "sessionTimeSpent":0, + "totalTimeSpent":0, + "timeTrackingEnabled":True, + "isHangfireCreatedAndUnpersisted":False, + "infiniteLoopFlag":False, + "id":"aPkMtjrzV54YxyS59", + "disabled":False + } + }, + { + "$type":"Core.Models.History.Revision, Core", + "revisionNumber":1.0, + "status":"historical", + "userId":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:23:39.881Z", + "version":{ + "$type":"Core.Models.Record.Record, Core", + "name":"PHT-1", + "allowed":[ + + ], + "trackingId":1.0, + "trackingFull":"PHT-1", + "applicationId":"a34xbNOoo2P3ivyjY", + "referencedRecordIds":[ + + ], + "referencedByIds":[ + + ], + "isNew":False, + "values":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01":{ + "$type":"Core.Models.Record.ValueSelection, Core", + "id":"5cd46fe302c8ff905e042cc0", + "value":"value3" + }, + "5cd46fce433cf20015dd46f7":"PHT-1" + }, + "repeatFieldCurrentValues":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "actionsExecuted":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "visualizations":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "applicationRevision":2.0, + "locked":False, + "comments":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "createdDate":"2019-05-09T18:23:39.881Z", + "modifiedDate":"2019-05-09T18:23:39.881Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "sessionTimeSpent":0, + "totalTimeSpent":0, + "timeTrackingEnabled":True, + "isHangfireCreatedAndUnpersisted":False, + "infiniteLoopFlag":False, + "id":"aPkMtjrzV54YxyS59", + "disabled":False + } + } +] + +raw_app_revision_data = [ + { + "$type":"Core.Models.History.Revision, Core", + "revisionNumber":3.0, + "status":"current", + "userId":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:24:02.642Z", + "version":{ + "$type":"Core.Models.Application.Application, Core", + "acronym":"PHT", + "trackingFieldId":"5cd46fce433cf20015dd46f7", + "layout":[ + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"agy01", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fdac741d5d54a2b5c32", + "row":1, + "col":1, + "sizex":2.0, + "sizey":0.0 + }, + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"axtys", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fddb36d66d467d4abe6", + "row":1, + "col":3, + "sizex":2.0, + "sizey":0.0 + } + ], + "fields":[ + { + "$type":"Core.Models.Fields.History.HistoryField, Core", + "id":"axtys", + "name":"History", + "key":"history", + "fieldType":"history", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.ValuesListField, Core", + "values":[ + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe3ca4746cda7e41a33", + "name":"value1", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + } + ], + "controlType":"select", + "selectionType":"single", + "id":"agy01", + "name":"Selection", + "key":"selection", + "fieldType":"valuesList", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.TrackingField, Core", + "prefix":"PHT-", + "id":"5cd46fce433cf20015dd46f7", + "name":"Tracking Id", + "key":"tracking-id", + "fieldType":"tracking", + "readOnly":True, + "supportsMultipleOutputMappings":False + } + ], + "maxTrackingId":1.0, + "workspaces":[ + + ], + "createWorkspace":False, + "createdDate":"2019-05-09T18:22:06.181Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:24:02.642Z", + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "timeTrackingEnabled":False, + "id":"a34xbNOoo2P3ivyjY", + "name":"Pydriver History Test", + "disabled":False + } + }, + { + "$type":"Core.Models.History.Revision, Core", + "revisionNumber":2.0, + "status":"historical", + "userId":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:23:08.766Z", + "version":{ + "$type":"Core.Models.Application.Application, Core", + "acronym":"PHT", + "trackingFieldId":"5cd46fce433cf20015dd46f7", + "layout":[ + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"agy01", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fdac741d5d54a2b5c32", + "row":1, + "col":1, + "sizex":2.0, + "sizey":0.0 + }, + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"axtys", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fddb36d66d467d4abe6", + "row":1, + "col":3, + "sizex":2.0, + "sizey":0.0 + } + ], + "fields":[ + { + "$type":"Core.Models.Fields.ValuesListField, Core", + "values":[ + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe3ca4746cda7e41a33", + "name":"value1", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + }, + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe379be68acd2039061", + "name":"value2", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + }, + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe302c8ff905e042cc0", + "name":"value3", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + } + ], + "controlType":"select", + "selectionType":"single", + "id":"agy01", + "name":"Selection", + "key":"selection", + "fieldType":"valuesList", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.History.HistoryField, Core", + "id":"axtys", + "name":"History", + "key":"history", + "fieldType":"history", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.TrackingField, Core", + "prefix":"PHT-", + "id":"5cd46fce433cf20015dd46f7", + "name":"Tracking Id", + "key":"tracking-id", + "fieldType":"tracking", + "readOnly":True, + "supportsMultipleOutputMappings":False + } + ], + "maxTrackingId":1.0, + "workspaces":[ + + ], + "createWorkspace":False, + "createdDate":"2019-05-09T18:22:06.181Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:23:08.766Z", + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "timeTrackingEnabled":False, + "id":"a34xbNOoo2P3ivyjY", + "name":"Pydriver History Test", + "disabled":False + } + }, + { + "$type":"Core.Models.History.Revision, Core", + "revisionNumber":1.0, + "status":"historical", + "userId":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:22:06.181Z", + "version":{ + "$type":"Core.Models.Application.Application, Core", + "acronym":"PHT", + "trackingFieldId":"5cd46fce433cf20015dd46f7", + "layout":[ + + ], + "fields":[ + { + "$type":"Core.Models.Fields.TrackingField, Core", + "prefix":"PHT-", + "id":"5cd46fce433cf20015dd46f7", + "name":"Tracking Id", + "key":"tracking-id", + "fieldType":"tracking", + "readOnly":True, + "supportsMultipleOutputMappings":False + } + ], + "maxTrackingId":0.0, + "workspaces":[ + + ], + "createWorkspace":False, + "createdDate":"2019-05-09T18:22:06.181Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:22:06.181Z", + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "timeTrackingEnabled":False, + "id":"a34xbNOoo2P3ivyjY", + "name":"Pydriver History Test", + "disabled":False + } + } +] + + +def request_mock(method, endpoint, params=None): + """Mocks swimlane.request function, returns the API responses for the history endpoints""" + if 'app' in endpoint: + revision = int(endpoint.split('/')[4]) + return_value = next(x for x in raw_app_revision_data if x['revisionNumber'] == revision) + else: + return_value = raw_record_revision_data mock_response = mock.MagicMock() - mock_response.json.return_value = raw_revision_data + mock_response.json.return_value = return_value + return mock_response + + +def test_revision_cursor(mock_swimlane): + with mock.patch.object(mock_swimlane, 'request', side_effect=request_mock): + app = App(mock_swimlane, raw_app_data) + mock_history_record = Record(app, raw_record_data) + history = mock_history_record['History'] + assert isinstance(history, RevisionCursor) - with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): +def test_num_revisions(mock_swimlane): + with mock.patch.object(mock_swimlane, 'request', side_effect=request_mock): + app = App(mock_swimlane, raw_app_data) + mock_history_record = Record(app, raw_record_data) + history = mock_history_record['History'] # Get number of revisions num_revisions = len(history) assert num_revisions == 3 + +def test_revisions(mock_swimlane): + with mock.patch.object(mock_swimlane, 'request', side_effect=request_mock): + app = App(mock_swimlane, raw_app_data) + mock_history_record = Record(app, raw_record_data) + history = mock_history_record['History'] # Iterate backwards over revisions for idx, revision in enumerate(history): assert isinstance(revision, Revision) - assert str(revision) == 'RA-7 ({})'.format(revision.revision_number) + assert str(revision) == 'PHT-1 ({})'.format(revision.revision_number) assert isinstance(revision.app_revision, App) assert isinstance(revision.modified_date, datetime) assert isinstance(revision.user, UserGroup) - assert revision.version.id == mock_record.id - assert num_revisions - revision.revision_number == idx + assert revision.version.id == mock_history_record.id + assert len(history) - revision.revision_number == idx + +def test_app_revision_caching(mock_swimlane): + assert True is True From 96fc2c8c8e4905c76a078fb816459adece6686de Mon Sep 17 00:00:00 2001 From: Jon Ford Date: Fri, 10 May 2019 10:56:02 -0400 Subject: [PATCH 05/17] use fixtures to share test setup --- swimlane/core/fields/history.py | 1 + tests/fields/test_history.py | 60 ++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/swimlane/core/fields/history.py b/swimlane/core/fields/history.py index 47f2a5a9..e74e23e7 100644 --- a/swimlane/core/fields/history.py +++ b/swimlane/core/fields/history.py @@ -90,6 +90,7 @@ def version(self): self.__version = Record(self.app_revision, self._raw['version']) return self.__version + class HistoryField(ReadOnly, CursorField): field_type = 'Core.Models.Fields.History.HistoryField, Core' diff --git a/tests/fields/test_history.py b/tests/fields/test_history.py index 070377d5..f49855c8 100644 --- a/tests/fields/test_history.py +++ b/tests/fields/test_history.py @@ -698,38 +698,44 @@ def request_mock(method, endpoint, params=None): return mock_response -def test_revision_cursor(mock_swimlane): +@pytest.fixture +def mock_swimlane_history(mock_swimlane): with mock.patch.object(mock_swimlane, 'request', side_effect=request_mock): - app = App(mock_swimlane, raw_app_data) - mock_history_record = Record(app, raw_record_data) - history = mock_history_record['History'] - assert isinstance(history, RevisionCursor) + yield mock_swimlane -def test_num_revisions(mock_swimlane): - with mock.patch.object(mock_swimlane, 'request', side_effect=request_mock): - app = App(mock_swimlane, raw_app_data) - mock_history_record = Record(app, raw_record_data) - history = mock_history_record['History'] - # Get number of revisions - num_revisions = len(history) - assert num_revisions == 3 +@pytest.fixture +def mock_history_record(mock_swimlane_history): + app = App(mock_swimlane_history, raw_app_data) + return Record(app, raw_record_data) -def test_revisions(mock_swimlane): - with mock.patch.object(mock_swimlane, 'request', side_effect=request_mock): - app = App(mock_swimlane, raw_app_data) - mock_history_record = Record(app, raw_record_data) - history = mock_history_record['History'] - # Iterate backwards over revisions - for idx, revision in enumerate(history): - assert isinstance(revision, Revision) - assert str(revision) == 'PHT-1 ({})'.format(revision.revision_number) - assert isinstance(revision.app_revision, App) - assert isinstance(revision.modified_date, datetime) - assert isinstance(revision.user, UserGroup) - assert revision.version.id == mock_history_record.id - assert len(history) - revision.revision_number == idx +@pytest.fixture +def history(mock_history_record): + return mock_history_record['History'] + + +def test_revision_cursor(history): + assert isinstance(history, RevisionCursor) + + +def test_num_revisions(history): + # Get number of revisions + num_revisions = len(history) + assert num_revisions == 3 + + +def test_revisions(history, mock_history_record): + # Iterate backwards over revisions + for idx, revision in enumerate(history): + assert isinstance(revision, Revision) + assert str(revision) == 'PHT-1 ({})'.format(revision.revision_number) + assert isinstance(revision.app_revision, App) + assert isinstance(revision.modified_date, datetime) + assert isinstance(revision.user, UserGroup) + assert isinstance(revision.version, Record) + assert revision.version.id == mock_history_record.id + assert len(history) - revision.revision_number == idx def test_app_revision_caching(mock_swimlane): From e16d409acc69ab3e33ea148c2d12eaae3b527d38 Mon Sep 17 00:00:00 2001 From: Jon Ford Date: Fri, 10 May 2019 16:15:35 -0400 Subject: [PATCH 06/17] add app/record revision resources and adapters, refactor history field cursor --- swimlane/core/adapters/__init__.py | 2 + swimlane/core/adapters/app_revision.py | 24 +++++++ swimlane/core/adapters/record_revision.py | 26 ++++++++ swimlane/core/fields/history.py | 74 +--------------------- swimlane/core/resources/app.py | 3 +- swimlane/core/resources/app_revision.py | 25 ++++++++ swimlane/core/resources/record.py | 4 ++ swimlane/core/resources/record_revision.py | 51 +++++++++++++++ tests/fields/test_history.py | 16 +++-- 9 files changed, 145 insertions(+), 80 deletions(-) create mode 100644 swimlane/core/adapters/app_revision.py create mode 100644 swimlane/core/adapters/record_revision.py create mode 100644 swimlane/core/resources/app_revision.py create mode 100644 swimlane/core/resources/record_revision.py diff --git a/swimlane/core/adapters/__init__.py b/swimlane/core/adapters/__init__.py index 6abd3ac7..6973a133 100644 --- a/swimlane/core/adapters/__init__.py +++ b/swimlane/core/adapters/__init__.py @@ -5,3 +5,5 @@ from .report import ReportAdapter from .usergroup import UserAdapter, GroupAdapter from .helper import HelperAdapter +from .app_revision import AppRevisionAdapter +from .record_revision import RecordRevisionAdapter \ No newline at end of file diff --git a/swimlane/core/adapters/app_revision.py b/swimlane/core/adapters/app_revision.py new file mode 100644 index 00000000..ce83d654 --- /dev/null +++ b/swimlane/core/adapters/app_revision.py @@ -0,0 +1,24 @@ +from swimlane.core.cache import check_cache +from swimlane.core.resolver import AppResolver +from swimlane.core.resources.app_revision import AppRevision + + +class AppRevisionAdapter(AppResolver): + """Handles retrieval and creation of Swimlane App Revision resources""" + + @check_cache(AppRevision) + def get(self, revision_number): + """Gets a specific app revision. + + Supports resource cache + + Keyword Args: + revision_number (int): App revision number + + Returns: + AppRevision: The AppRevision for the given revision number. + """ + + app_revision_raw = self._swimlane.request('get', 'app/{0}/history/{1}'.format(self._app.id, + revision_number)).json() + return AppRevision(self._swimlane, app_revision_raw) diff --git a/swimlane/core/adapters/record_revision.py b/swimlane/core/adapters/record_revision.py new file mode 100644 index 00000000..21f7b59f --- /dev/null +++ b/swimlane/core/adapters/record_revision.py @@ -0,0 +1,26 @@ +from swimlane.core.cache import check_cache +from swimlane.core.resolver import AppResolver +from swimlane.core.resources.record_revision import RecordRevision + + +class RecordRevisionAdapter(AppResolver): + """Handles retrieval and creation of Swimlane Record Revision resources""" + + def __init__(self, app, record): + super(RecordRevisionAdapter, self).__init__(app) + self.record = record + + @check_cache(RecordRevision) + def get(self): + """Get all revisions for a single record. + + Supports resource cache + + Returns: + RecordRevision[]: All record revisions for the given record ID. + """ + response = self._swimlane.request('get', 'app/{0}/record/{1}/history'.format(self._app.id, self.record.id)) + + raw_revisions = response.json() + + return [RecordRevision(self._app, raw) for raw in raw_revisions] diff --git a/swimlane/core/fields/history.py b/swimlane/core/fields/history.py index e74e23e7..3ac00d2b 100644 --- a/swimlane/core/fields/history.py +++ b/swimlane/core/fields/history.py @@ -1,7 +1,3 @@ -import pendulum - -from swimlane.core.resources.base import APIResource -from swimlane.core.resources.usergroup import UserGroup from .base import CursorField, FieldCursor, ReadOnly @@ -21,74 +17,8 @@ def _evaluate(self): return super(RevisionCursor, self)._evaluate() def _retrieve_revisions(self): - """Retrieve and populate Revision instances from history API endpoint""" - response = self._swimlane.request( - 'get', - 'history', - params={ - 'type': 'Records', - 'id': self._record.id - } - ) - - raw_revisions = response.json() - - return [Revision(self._record, raw) for raw in raw_revisions] - - -class Revision(APIResource): - """Encapsulates a single revision returned from a History lookup""" - - def __init__(self, record, raw): - super(Revision, self).__init__(record._swimlane, raw) - self.__app_revision = None - self.__version = None - - self.record = record - - self.modified_date = pendulum.parse(self._raw['modifiedDate']) - self.revision_number = int(self._raw['revisionNumber']) - self.status = self._raw['status'] - - # UserGroupSelection, can't set as User without additional lookup - self.user = UserGroup(self._swimlane, self._raw['userId']) - - def __str__(self): - return '{} ({})'.format(self.version, self.revision_number) - - def for_json(self): - """Return revision metadata""" - return { - 'modifiedDate': self._raw['modifiedDate'], - 'revisionNumber': self.revision_number, - 'user': self.user.for_json() - } - - @property - def app_revision(self): - """Deferring request for app revision until needed""" - if not self.__app_revision: - # Avoid circular imports - from swimlane.core.resources.app import App - - app_revision_number = int(self._raw['version']['applicationRevision']) - app_revision_raw = self._swimlane.request( - 'get', - '/app/{}/history/{}'.format( - self.record.app.id, - app_revision_number)).json() - self.__app_revision = App(self._swimlane, app_revision_raw['version']) - return self.__app_revision - - @property - def version(self): - """Deferring for sake of app revision""" - if not self.__version: - # Avoid circular imports - from swimlane.core.resources.record import Record - - self.__version = Record(self.app_revision, self._raw['version']) - return self.__version + """Populate RecordRevision instances.""" + return self._record.revisions.get() class HistoryField(ReadOnly, CursorField): diff --git a/swimlane/core/resources/app.py b/swimlane/core/resources/app.py index 1f3db0b6..c300c56a 100644 --- a/swimlane/core/resources/app.py +++ b/swimlane/core/resources/app.py @@ -44,9 +44,10 @@ def __init__(self, swimlane, raw): self._keys_to_field_names[key] = name # Avoid circular import - from swimlane.core.adapters import RecordAdapter, ReportAdapter + from swimlane.core.adapters import RecordAdapter, ReportAdapter, AppRevisionAdapter self.records = RecordAdapter(self) self.reports = ReportAdapter(self) + self.revisions = AppRevisionAdapter(self) def __str__(self): return '{self.name} ({self.acronym})'.format(self=self) diff --git a/swimlane/core/resources/app_revision.py b/swimlane/core/resources/app_revision.py new file mode 100644 index 00000000..9eb5f454 --- /dev/null +++ b/swimlane/core/resources/app_revision.py @@ -0,0 +1,25 @@ +from pendulum import pendulum + +from swimlane.core.resources.app import App +from swimlane.core.resources.base import APIResource +from swimlane.core.resources.usergroup import UserGroup + + +class AppRevision(APIResource): + """ + Encapsulates a single revision returned from a History lookup. + """ + + def __init__(self, swimlane, raw): + super(AppRevision, self).__init__(swimlane, raw) + self.version = App(swimlane, raw['version']) + + self.modified_date = pendulum.parse(self._raw['modifiedDate']) + self.revision_number = int(self._raw['revisionNumber']) + self.status = self._raw['status'] + + # UserGroupSelection, can't set as User without additional lookup + self.user = UserGroup(self._swimlane, self._raw['userId']) + + def __str__(self): + return '{} ({})'.format(self.version, self.revision_number) diff --git a/swimlane/core/resources/record.py b/swimlane/core/resources/record.py index 14d2e23f..6910e99b 100644 --- a/swimlane/core/resources/record.py +++ b/swimlane/core/resources/record.py @@ -59,6 +59,10 @@ def __init__(self, app, raw): self._fields = {} self.__premap_fields() + # avoid circular reference + from swimlane.core.adapters import RecordRevisionAdapter + self.revisions = RecordRevisionAdapter(app, self) + @property def app(self): return self.__app diff --git a/swimlane/core/resources/record_revision.py b/swimlane/core/resources/record_revision.py new file mode 100644 index 00000000..445fad11 --- /dev/null +++ b/swimlane/core/resources/record_revision.py @@ -0,0 +1,51 @@ +import pendulum + +from swimlane.core.resources.base import APIResource +from swimlane.core.resources.usergroup import UserGroup + + +class RecordRevision(APIResource): + """ + Encapsulates a single revision returned from a History lookup. + """ + + def __init__(self, app, raw): + super(RecordRevision, self).__init__(app._swimlane, raw) + self.__app_version = None + self.__version = None + + self._app = app + + self.modified_date = pendulum.parse(self._raw['modifiedDate']) + self.revision_number = int(self._raw['revisionNumber']) + self.status = self._raw['status'] + + # UserGroupSelection, can't set as User without additional lookup + self.user = UserGroup(self._swimlane, self._raw['userId']) + + def __str__(self): + return '{} ({})'.format(self.version, self.revision_number) + + def for_json(self): + """Return revision metadata""" + return { + 'modifiedDate': self._raw['modifiedDate'], + 'revisionNumber': self.revision_number, + 'user': self.user.for_json() + } + + @property + def app_version(self): + """The app revision corresponding to this record revision. Lazy loaded""" + if not self.__app_version: + self.__app_version = self._app.revisions.get(self.revision_number).version + return self.__app_version + + @property + def version(self): + """The record contained in this record revision. Lazy loaded""" + if not self.__version: + # avoid circular imports + from swimlane.core.resources.record import Record + self.__version = Record(self.app_version, self._raw['version']) + return self.__version diff --git a/tests/fields/test_history.py b/tests/fields/test_history.py index f49855c8..50ccebbb 100644 --- a/tests/fields/test_history.py +++ b/tests/fields/test_history.py @@ -3,11 +3,13 @@ import mock import pytest -from swimlane.core.fields.history import RevisionCursor, Revision +from swimlane.core.fields.history import RevisionCursor +from swimlane.core.resources.record_revision import RecordRevision from swimlane.core.resources.usergroup import UserGroup from swimlane.core.resources.app import App from swimlane.core.resources.record import Record +# an app with two fields: value selection and raw_app_data = { "$type":"Core.Models.Application.Application, Core", "acronym":"PHT", @@ -687,11 +689,11 @@ def request_mock(method, endpoint, params=None): """Mocks swimlane.request function, returns the API responses for the history endpoints""" - if 'app' in endpoint: - revision = int(endpoint.split('/')[4]) - return_value = next(x for x in raw_app_revision_data if x['revisionNumber'] == revision) - else: + if 'record' in endpoint: return_value = raw_record_revision_data + else: + revision = int(endpoint.split('/')[3]) + return_value = next(x for x in raw_app_revision_data if x['revisionNumber'] == revision) mock_response = mock.MagicMock() mock_response.json.return_value = return_value @@ -728,9 +730,9 @@ def test_num_revisions(history): def test_revisions(history, mock_history_record): # Iterate backwards over revisions for idx, revision in enumerate(history): - assert isinstance(revision, Revision) + assert isinstance(revision, RecordRevision) assert str(revision) == 'PHT-1 ({})'.format(revision.revision_number) - assert isinstance(revision.app_revision, App) + assert isinstance(revision.app_version, App) assert isinstance(revision.modified_date, datetime) assert isinstance(revision.user, UserGroup) assert isinstance(revision.version, Record) From 170e7ed36f4901a4b9b8a2974c06de5d36c8a700 Mon Sep 17 00:00:00 2001 From: Jon Ford Date: Mon, 13 May 2019 17:15:57 -0400 Subject: [PATCH 07/17] add adapter tests, switch to float values in api calls --- swimlane/core/adapters/app_revision.py | 2 +- swimlane/core/resources/app_revision.py | 2 +- swimlane/core/resources/record_revision.py | 2 +- tests/adapters/test_app_revision_adapter.py | 23 + .../adapters/test_record_revision_adapter.py | 24 + tests/conftest.py | 3 + tests/conftest_revisions.py | 709 ++++++++++++++++++ tests/fields/test_history.py | 9 +- 8 files changed, 768 insertions(+), 6 deletions(-) create mode 100644 tests/adapters/test_app_revision_adapter.py create mode 100644 tests/adapters/test_record_revision_adapter.py create mode 100644 tests/conftest_revisions.py diff --git a/swimlane/core/adapters/app_revision.py b/swimlane/core/adapters/app_revision.py index ce83d654..74154370 100644 --- a/swimlane/core/adapters/app_revision.py +++ b/swimlane/core/adapters/app_revision.py @@ -13,7 +13,7 @@ def get(self, revision_number): Supports resource cache Keyword Args: - revision_number (int): App revision number + revision_number (float): App revision number Returns: AppRevision: The AppRevision for the given revision number. diff --git a/swimlane/core/resources/app_revision.py b/swimlane/core/resources/app_revision.py index 9eb5f454..dd8d82d7 100644 --- a/swimlane/core/resources/app_revision.py +++ b/swimlane/core/resources/app_revision.py @@ -15,7 +15,7 @@ def __init__(self, swimlane, raw): self.version = App(swimlane, raw['version']) self.modified_date = pendulum.parse(self._raw['modifiedDate']) - self.revision_number = int(self._raw['revisionNumber']) + self.revision_number = self._raw['revisionNumber'] self.status = self._raw['status'] # UserGroupSelection, can't set as User without additional lookup diff --git a/swimlane/core/resources/record_revision.py b/swimlane/core/resources/record_revision.py index 445fad11..42f40869 100644 --- a/swimlane/core/resources/record_revision.py +++ b/swimlane/core/resources/record_revision.py @@ -17,7 +17,7 @@ def __init__(self, app, raw): self._app = app self.modified_date = pendulum.parse(self._raw['modifiedDate']) - self.revision_number = int(self._raw['revisionNumber']) + self.revision_number = self._raw['revisionNumber'] self.status = self._raw['status'] # UserGroupSelection, can't set as User without additional lookup diff --git a/tests/adapters/test_app_revision_adapter.py b/tests/adapters/test_app_revision_adapter.py new file mode 100644 index 00000000..c8425201 --- /dev/null +++ b/tests/adapters/test_app_revision_adapter.py @@ -0,0 +1,23 @@ +import mock + +from swimlane.core.resources.app_revision import AppRevision + + +def test_get(mock_swimlane, mock_revision_app, raw_app_revision_data): + mock_response = mock.MagicMock() + + with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): + # return just one revision in response + raw_revision = raw_app_revision_data[0] + mock_response.json.return_value = raw_revision + mock_response.status_code = 200 + + revision = mock_revision_app.revisions.get(raw_revision['revisionNumber']) + + mock_swimlane.request.assert_called_with('get', 'app/{0}/history/{1}'.format(mock_revision_app.id, + raw_revision['revisionNumber'])) + + assert isinstance(revision, AppRevision) + assert revision.revision_number is raw_revision['revisionNumber'] + assert revision.status is raw_revision['status'] + diff --git a/tests/adapters/test_record_revision_adapter.py b/tests/adapters/test_record_revision_adapter.py new file mode 100644 index 00000000..54bd41c1 --- /dev/null +++ b/tests/adapters/test_record_revision_adapter.py @@ -0,0 +1,24 @@ + + +import mock + +from swimlane.core.resources.record_revision import RecordRevision +from swimlane.core.resources.usergroup import UserGroup + + +def test_get(mock_swimlane, mock_revision_record, raw_record_revision_data): + mock_response = mock.MagicMock() + + with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): + mock_response.status_code = 200 + mock_response.json.return_value = raw_record_revision_data + + revisions = mock_revision_record.revisions.get() + + mock_swimlane.request.assert_called_with('get', 'app/{0}/record/{1}/history'.format(mock_revision_record.app.id, + mock_revision_record.id)) + + for idx, revision in enumerate(revisions): + assert isinstance(revision, RecordRevision) + assert revision.revision_number is raw_record_revision_data[idx]['revisionNumber'] + assert revision.status is raw_record_revision_data[idx]['status'] diff --git a/tests/conftest.py b/tests/conftest.py index db9f9e01..0217541b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3799,3 +3799,6 @@ def random_mock_user(mock_swimlane): 'passwordResetRequired': False, 'roles': [], 'userName': 'admin'}) + + +pytest_plugins = ['conftest_revisions'] diff --git a/tests/conftest_revisions.py b/tests/conftest_revisions.py new file mode 100644 index 00000000..9d191720 --- /dev/null +++ b/tests/conftest_revisions.py @@ -0,0 +1,709 @@ +import pytest + +from swimlane.core.resources.app import App +from swimlane.core.resources.app_revision import AppRevision +from swimlane.core.resources.record import Record +from swimlane.core.resources.record_revision import RecordRevision + + +@pytest.fixture +def mock_revision_app(mock_swimlane): + """An app with two fields: value selection and history.""" + return App(mock_swimlane, { + "$type":"Core.Models.Application.Application, Core", + "acronym":"PHT", + "trackingFieldId":"5cd46fce433cf20015dd46f7", + "layout":[ + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"agy01", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fdac741d5d54a2b5c32", + "row":1, + "col":1, + "sizex":2.0, + "sizey":0.0 + }, + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"axtys", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fddb36d66d467d4abe6", + "row":1, + "col":3, + "sizex":2.0, + "sizey":0.0 + } + ], + "fields":[ + { + "$type":"Core.Models.Fields.History.HistoryField, Core", + "id":"axtys", + "name":"History", + "key":"history", + "fieldType":"history", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.ValuesListField, Core", + "values":[ + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe3ca4746cda7e41a33", + "name":"value1", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + } + ], + "controlType":"select", + "selectionType":"single", + "id":"agy01", + "name":"Selection", + "key":"selection", + "fieldType":"valuesList", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.TrackingField, Core", + "prefix":"PHT-", + "id":"5cd46fce433cf20015dd46f7", + "name":"Tracking Id", + "key":"tracking-id", + "fieldType":"tracking", + "readOnly":True, + "supportsMultipleOutputMappings":False + } + ], + "maxTrackingId":1.0, + "workspaces":[ + "5cd46fce433cf20015dd4727" + ], + "createWorkspace":False, + "createdDate":"2019-05-09T18:22:06.181Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:24:02.642Z", + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "timeTrackingEnabled":False, + "permissions":{ + "$type":"Core.Models.Security.PermissionMatrix, Core" + }, + "id":"a34xbNOoo2P3ivyjY", + "name":"Pydriver History Test", + "disabled":False + }) + + +@pytest.fixture +def mock_revision_record(mock_revision_app): + """A record from the mock_revision_app.""" + return Record(mock_revision_app, { + "$type":"Core.Models.Record.Record, Core", + "name":"PHT-1", + "allowed":[ + + ], + "trackingId":1.0, + "trackingFull":"PHT-1", + "applicationId":"a34xbNOoo2P3ivyjY", + "isNew":False, + "values":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01":{ + "$type":"Core.Models.Record.ValueSelection, Core", + "id":"5cd46fe3ca4746cda7e41a33", + "value":"value1" + }, + "5cd46fce433cf20015dd46f7":"PHT-1" + }, + "repeatFieldCurrentValues":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "actionsExecuted":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "visualizations":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "applicationRevision":3.0, + "locked":False, + "comments":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "createdDate":"2019-05-09T18:23:39.881Z", + "modifiedDate":"2019-05-09T18:24:20.027Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "sessionTimeSpent":0, + "totalTimeSpent":0, + "timeTrackingEnabled":True, + "isHangfireCreatedAndUnpersisted":False, + "infiniteLoopFlag":False, + "id":"aPkMtjrzV54YxyS59", + "disabled":False + }) + + +@pytest.fixture +def raw_record_revision_data(): + """Raw record revision data for mock_revision_record.""" + return [ + { + "$type": "Core.Models.History.Revision, Core", + "revisionNumber": 3.0, + "status": "current", + "userId": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "modifiedDate": "2019-05-09T18:24:20.027Z", + "version": { + "$type": "Core.Models.Record.Record, Core", + "name": "PHT-1", + "allowed": [ + + ], + "trackingId": 1.0, + "trackingFull": "PHT-1", + "applicationId": "a34xbNOoo2P3ivyjY", + "referencedRecordIds": [ + + ], + "referencedByIds": [ + + ], + "isNew": False, + "values": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01": { + "$type": "Core.Models.Record.ValueSelection, Core", + "id": "5cd46fe3ca4746cda7e41a33", + "value": "value1" + }, + "5cd46fce433cf20015dd46f7": "PHT-1" + }, + "repeatFieldCurrentValues": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "valuesDocument": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01": { + "$type": "Core.Models.Record.ValueSelection, Core", + "id": "5cd46fe3ca4746cda7e41a33", + "value": "value1" + }, + "5cd46fce433cf20015dd46f7": "PHT-1" + }, + "actionsExecuted": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "visualizations": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "applicationRevision": 3.0, + "locked": False, + "comments": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "createdDate": "2019-05-09T18:23:39.881Z", + "modifiedDate": "2019-05-09T18:24:20.027Z", + "createdByUser": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "modifiedByUser": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "sessionTimeSpent": 0, + "totalTimeSpent": 0, + "timeTrackingEnabled": True, + "isHangfireCreatedAndUnpersisted": False, + "infiniteLoopFlag": False, + "id": "aPkMtjrzV54YxyS59", + "disabled": False + } + }, + { + "$type": "Core.Models.History.Revision, Core", + "revisionNumber": 2.0, + "status": "historical", + "userId": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "modifiedDate": "2019-05-09T18:23:48.539Z", + "version": { + "$type": "Core.Models.Record.Record, Core", + "name": "PHT-1", + "allowed": [ + + ], + "trackingId": 1.0, + "trackingFull": "PHT-1", + "applicationId": "a34xbNOoo2P3ivyjY", + "referencedRecordIds": [ + + ], + "referencedByIds": [ + + ], + "isNew": False, + "values": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01": { + "$type": "Core.Models.Record.ValueSelection, Core", + "id": "5cd46fe379be68acd2039061", + "value": "value2" + }, + "5cd46fce433cf20015dd46f7": "PHT-1" + }, + "repeatFieldCurrentValues": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "actionsExecuted": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "visualizations": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "applicationRevision": 2.0, + "locked": False, + "comments": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "createdDate": "2019-05-09T18:23:39.881Z", + "modifiedDate": "2019-05-09T18:23:48.539Z", + "createdByUser": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "modifiedByUser": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "sessionTimeSpent": 0, + "totalTimeSpent": 0, + "timeTrackingEnabled": True, + "isHangfireCreatedAndUnpersisted": False, + "infiniteLoopFlag": False, + "id": "aPkMtjrzV54YxyS59", + "disabled": False + } + }, + { + "$type": "Core.Models.History.Revision, Core", + "revisionNumber": 1.0, + "status": "historical", + "userId": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "modifiedDate": "2019-05-09T18:23:39.881Z", + "version": { + "$type": "Core.Models.Record.Record, Core", + "name": "PHT-1", + "allowed": [ + + ], + "trackingId": 1.0, + "trackingFull": "PHT-1", + "applicationId": "a34xbNOoo2P3ivyjY", + "referencedRecordIds": [ + + ], + "referencedByIds": [ + + ], + "isNew": False, + "values": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01": { + "$type": "Core.Models.Record.ValueSelection, Core", + "id": "5cd46fe302c8ff905e042cc0", + "value": "value3" + }, + "5cd46fce433cf20015dd46f7": "PHT-1" + }, + "repeatFieldCurrentValues": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "actionsExecuted": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "visualizations": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "applicationRevision": 2.0, + "locked": False, + "comments": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "createdDate": "2019-05-09T18:23:39.881Z", + "modifiedDate": "2019-05-09T18:23:39.881Z", + "createdByUser": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "modifiedByUser": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "sessionTimeSpent": 0, + "totalTimeSpent": 0, + "timeTrackingEnabled": True, + "isHangfireCreatedAndUnpersisted": False, + "infiniteLoopFlag": False, + "id": "aPkMtjrzV54YxyS59", + "disabled": False + } + } + ] + + +@pytest.fixture +def mock_record_revisions(mock_revision_app, raw_record_revision_data): + """All record revisions for mock_revision_record.""" + return [RecordRevision(mock_revision_app, raw) for raw in raw_record_revision_data] + + +@pytest.fixture +def raw_app_revision_data(): + """Raw app revision data for mock_revision_app""" + return [ + { + "$type":"Core.Models.History.Revision, Core", + "revisionNumber":3.0, + "status":"current", + "userId":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:24:02.642Z", + "version":{ + "$type":"Core.Models.Application.Application, Core", + "acronym":"PHT", + "trackingFieldId":"5cd46fce433cf20015dd46f7", + "layout":[ + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"agy01", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fdac741d5d54a2b5c32", + "row":1, + "col":1, + "sizex":2.0, + "sizey":0.0 + }, + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"axtys", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fddb36d66d467d4abe6", + "row":1, + "col":3, + "sizex":2.0, + "sizey":0.0 + } + ], + "fields":[ + { + "$type":"Core.Models.Fields.History.HistoryField, Core", + "id":"axtys", + "name":"History", + "key":"history", + "fieldType":"history", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.ValuesListField, Core", + "values":[ + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe3ca4746cda7e41a33", + "name":"value1", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + } + ], + "controlType":"select", + "selectionType":"single", + "id":"agy01", + "name":"Selection", + "key":"selection", + "fieldType":"valuesList", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.TrackingField, Core", + "prefix":"PHT-", + "id":"5cd46fce433cf20015dd46f7", + "name":"Tracking Id", + "key":"tracking-id", + "fieldType":"tracking", + "readOnly":True, + "supportsMultipleOutputMappings":False + } + ], + "maxTrackingId":1.0, + "workspaces":[ + + ], + "createWorkspace":False, + "createdDate":"2019-05-09T18:22:06.181Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:24:02.642Z", + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "timeTrackingEnabled":False, + "id":"a34xbNOoo2P3ivyjY", + "name":"Pydriver History Test", + "disabled":False + } + }, + { + "$type":"Core.Models.History.Revision, Core", + "revisionNumber":2.0, + "status":"historical", + "userId":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:23:08.766Z", + "version":{ + "$type":"Core.Models.Application.Application, Core", + "acronym":"PHT", + "trackingFieldId":"5cd46fce433cf20015dd46f7", + "layout":[ + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"agy01", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fdac741d5d54a2b5c32", + "row":1, + "col":1, + "sizex":2.0, + "sizey":0.0 + }, + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"axtys", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fddb36d66d467d4abe6", + "row":1, + "col":3, + "sizex":2.0, + "sizey":0.0 + } + ], + "fields":[ + { + "$type":"Core.Models.Fields.ValuesListField, Core", + "values":[ + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe3ca4746cda7e41a33", + "name":"value1", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + }, + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe379be68acd2039061", + "name":"value2", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + }, + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe302c8ff905e042cc0", + "name":"value3", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + } + ], + "controlType":"select", + "selectionType":"single", + "id":"agy01", + "name":"Selection", + "key":"selection", + "fieldType":"valuesList", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.History.HistoryField, Core", + "id":"axtys", + "name":"History", + "key":"history", + "fieldType":"history", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.TrackingField, Core", + "prefix":"PHT-", + "id":"5cd46fce433cf20015dd46f7", + "name":"Tracking Id", + "key":"tracking-id", + "fieldType":"tracking", + "readOnly":True, + "supportsMultipleOutputMappings":False + } + ], + "maxTrackingId":1.0, + "workspaces":[ + + ], + "createWorkspace":False, + "createdDate":"2019-05-09T18:22:06.181Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:23:08.766Z", + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "timeTrackingEnabled":False, + "id":"a34xbNOoo2P3ivyjY", + "name":"Pydriver History Test", + "disabled":False + } + }, + { + "$type":"Core.Models.History.Revision, Core", + "revisionNumber":1.0, + "status":"historical", + "userId":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:22:06.181Z", + "version":{ + "$type":"Core.Models.Application.Application, Core", + "acronym":"PHT", + "trackingFieldId":"5cd46fce433cf20015dd46f7", + "layout":[ + + ], + "fields":[ + { + "$type":"Core.Models.Fields.TrackingField, Core", + "prefix":"PHT-", + "id":"5cd46fce433cf20015dd46f7", + "name":"Tracking Id", + "key":"tracking-id", + "fieldType":"tracking", + "readOnly":True, + "supportsMultipleOutputMappings":False + } + ], + "maxTrackingId":0.0, + "workspaces":[ + + ], + "createWorkspace":False, + "createdDate":"2019-05-09T18:22:06.181Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:22:06.181Z", + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "timeTrackingEnabled":False, + "id":"a34xbNOoo2P3ivyjY", + "name":"Pydriver History Test", + "disabled":False + } + } + ] + +@pytest.fixture +def mock_app_revisions(mock_swimlane, raw_app_revision_data): + """All record revisions for mock_revision_record.""" + return [AppRevision(mock_swimlane, raw) for raw in raw_app_revision_data] diff --git a/tests/fields/test_history.py b/tests/fields/test_history.py index 50ccebbb..eb42d33b 100644 --- a/tests/fields/test_history.py +++ b/tests/fields/test_history.py @@ -692,7 +692,7 @@ def request_mock(method, endpoint, params=None): if 'record' in endpoint: return_value = raw_record_revision_data else: - revision = int(endpoint.split('/')[3]) + revision = float(endpoint.split('/')[3]) return_value = next(x for x in raw_app_revision_data if x['revisionNumber'] == revision) mock_response = mock.MagicMock() @@ -727,11 +727,14 @@ def test_num_revisions(history): assert num_revisions == 3 +def test_revision_str(history): + for revision in history: + assert str(revision) == 'PHT-1 ({})'.format(revision.revision_number) + + def test_revisions(history, mock_history_record): - # Iterate backwards over revisions for idx, revision in enumerate(history): assert isinstance(revision, RecordRevision) - assert str(revision) == 'PHT-1 ({})'.format(revision.revision_number) assert isinstance(revision.app_version, App) assert isinstance(revision.modified_date, datetime) assert isinstance(revision.user, UserGroup) From e2daf14f19f9a79189b0aedd02fe62e38f69b756 Mon Sep 17 00:00:00 2001 From: Jon Ford Date: Tue, 14 May 2019 09:28:35 -0400 Subject: [PATCH 08/17] add RecordRevision resource tests --- swimlane/core/resources/record_revision.py | 3 +- tests/adapters/test_app_revision_adapter.py | 9 ++- .../adapters/test_record_revision_adapter.py | 5 +- tests/fields/test_history.py | 53 ++++++++--------- tests/resources/test_app_revision.py | 0 tests/resources/test_record_revision.py | 58 +++++++++++++++++++ 6 files changed, 91 insertions(+), 37 deletions(-) create mode 100644 tests/resources/test_app_revision.py create mode 100644 tests/resources/test_record_revision.py diff --git a/swimlane/core/resources/record_revision.py b/swimlane/core/resources/record_revision.py index 42f40869..76a884fa 100644 --- a/swimlane/core/resources/record_revision.py +++ b/swimlane/core/resources/record_revision.py @@ -16,6 +16,7 @@ def __init__(self, app, raw): self._app = app + self.app_revision_number = self._raw['version']['applicationRevision'] self.modified_date = pendulum.parse(self._raw['modifiedDate']) self.revision_number = self._raw['revisionNumber'] self.status = self._raw['status'] @@ -38,7 +39,7 @@ def for_json(self): def app_version(self): """The app revision corresponding to this record revision. Lazy loaded""" if not self.__app_version: - self.__app_version = self._app.revisions.get(self.revision_number).version + self.__app_version = self._app.revisions.get(self.app_revision_number).version return self.__app_version @property diff --git a/tests/adapters/test_app_revision_adapter.py b/tests/adapters/test_app_revision_adapter.py index c8425201..9f7b517f 100644 --- a/tests/adapters/test_app_revision_adapter.py +++ b/tests/adapters/test_app_revision_adapter.py @@ -5,13 +5,12 @@ def test_get(mock_swimlane, mock_revision_app, raw_app_revision_data): mock_response = mock.MagicMock() + # return just one revision in response + raw_revision = raw_app_revision_data[0] + mock_response.json.return_value = raw_revision + mock_response.status_code = 200 with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): - # return just one revision in response - raw_revision = raw_app_revision_data[0] - mock_response.json.return_value = raw_revision - mock_response.status_code = 200 - revision = mock_revision_app.revisions.get(raw_revision['revisionNumber']) mock_swimlane.request.assert_called_with('get', 'app/{0}/history/{1}'.format(mock_revision_app.id, diff --git a/tests/adapters/test_record_revision_adapter.py b/tests/adapters/test_record_revision_adapter.py index 54bd41c1..cd683fa2 100644 --- a/tests/adapters/test_record_revision_adapter.py +++ b/tests/adapters/test_record_revision_adapter.py @@ -8,11 +8,10 @@ def test_get(mock_swimlane, mock_revision_record, raw_record_revision_data): mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = raw_record_revision_data with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): - mock_response.status_code = 200 - mock_response.json.return_value = raw_record_revision_data - revisions = mock_revision_record.revisions.get() mock_swimlane.request.assert_called_with('get', 'app/{0}/record/{1}/history'.format(mock_revision_record.app.id, diff --git a/tests/fields/test_history.py b/tests/fields/test_history.py index eb42d33b..3395a4c3 100644 --- a/tests/fields/test_history.py +++ b/tests/fields/test_history.py @@ -717,31 +717,28 @@ def history(mock_history_record): return mock_history_record['History'] -def test_revision_cursor(history): - assert isinstance(history, RevisionCursor) - - -def test_num_revisions(history): - # Get number of revisions - num_revisions = len(history) - assert num_revisions == 3 - - -def test_revision_str(history): - for revision in history: - assert str(revision) == 'PHT-1 ({})'.format(revision.revision_number) - - -def test_revisions(history, mock_history_record): - for idx, revision in enumerate(history): - assert isinstance(revision, RecordRevision) - assert isinstance(revision.app_version, App) - assert isinstance(revision.modified_date, datetime) - assert isinstance(revision.user, UserGroup) - assert isinstance(revision.version, Record) - assert revision.version.id == mock_history_record.id - assert len(history) - revision.revision_number == idx - - -def test_app_revision_caching(mock_swimlane): - assert True is True +class TestHistory(object): + def test_revision_cursor(self, history): + assert isinstance(history, RevisionCursor) + + def test_num_revisions(self, history): + # Get number of revisions + num_revisions = len(history) + assert num_revisions == 3 + + def test_revision_str(self, history): + for revision in history: + assert str(revision) == 'PHT-1 ({})'.format(revision.revision_number) + + def test_revisions(self, history, mock_history_record): + for idx, revision in enumerate(history): + assert isinstance(revision, RecordRevision) + assert isinstance(revision.app_version, App) + assert isinstance(revision.modified_date, datetime) + assert isinstance(revision.user, UserGroup) + assert isinstance(revision.version, Record) + assert revision.version.id == mock_history_record.id + assert len(history) - revision.revision_number == idx + + def test_app_revision_caching(self, mock_swimlane): + assert True is True diff --git a/tests/resources/test_app_revision.py b/tests/resources/test_app_revision.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/resources/test_record_revision.py b/tests/resources/test_record_revision.py new file mode 100644 index 00000000..c678c9d3 --- /dev/null +++ b/tests/resources/test_record_revision.py @@ -0,0 +1,58 @@ +import mock +import pendulum +import pytest + +from swimlane.core.resources.app import App +from swimlane.core.resources.record import Record +from swimlane.core.resources.record_revision import RecordRevision +from swimlane.core.resources.usergroup import UserGroup + + +@pytest.fixture +def mock_rr_raw_record_revision(raw_record_revision_data): + return raw_record_revision_data[0] + + +@pytest.fixture +def mock_rr_app_revision(mock_app_revisions, mock_rr_raw_record_revision): + app_revision_number = mock_rr_raw_record_revision['version']['applicationRevision'] + return next(x for x in mock_app_revisions if x.revision_number == app_revision_number) + + +@pytest.fixture +def mock_rr_app(mock_rr_app_revision, mock_revision_app): + mock_revisions = mock.MagicMock() + mock_revisions.get.return_value = mock_rr_app_revision + + mock_revisions_property = mock.PropertyMock(return_value=mock_revisions) + + # mocks App.revisions as a property so it doesn't need to be called like a method to get the mock_revisions + # sets it up so mock_rr_app.revisions.get() returns mock_rr_app_revision + with mock.patch.object(mock_revision_app, 'revisions', new_callable=mock_revisions_property): + yield mock_revision_app + + +@pytest.fixture +def mock_rr_record_revision(mock_rr_raw_record_revision, mock_rr_app): + return RecordRevision(mock_rr_app, mock_rr_raw_record_revision) + + +class TestRecordRevision(object): + def test_constructor(self, mock_swimlane, mock_rr_record_revision, mock_rr_raw_record_revision): + assert mock_rr_record_revision.app_revision_number is mock_rr_raw_record_revision['version']['applicationRevision'] + assert str(mock_rr_record_revision.modified_date) == str(pendulum.parse(mock_rr_raw_record_revision['modifiedDate'])) + assert mock_rr_record_revision.revision_number is mock_rr_raw_record_revision['revisionNumber'] + assert mock_rr_record_revision.status is mock_rr_raw_record_revision['status'] + assert str(mock_rr_record_revision.user) == str(UserGroup(mock_swimlane, mock_rr_raw_record_revision['userId'])) + + def test_app_version(self, mock_rr_record_revision, mock_rr_app_revision, mock_rr_app): + app_version = mock_rr_record_revision.app_version + mock_rr_app.revisions.get.assert_called_with(mock_rr_record_revision.app_revision_number) + assert isinstance(app_version, App) + assert app_version is mock_rr_app_revision.version + + def test_version(self, mock_rr_app_revision, mock_rr_record_revision, mock_rr_raw_record_revision): + version = mock_rr_record_revision.version + + assert isinstance(version, Record) + assert version.app is mock_rr_app_revision.version From e883447affc062805cf0f431cbb9d839156c2882 Mon Sep 17 00:00:00 2001 From: Jon Ford Date: Tue, 14 May 2019 09:45:00 -0400 Subject: [PATCH 09/17] add AppRevision resource tests, fix date parsing bug --- swimlane/core/resources/app_revision.py | 2 +- tests/resources/test_app_revision.py | 30 +++++++++++++++++++++++++ tests/resources/test_record_revision.py | 9 +++++--- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/swimlane/core/resources/app_revision.py b/swimlane/core/resources/app_revision.py index dd8d82d7..1bf362e2 100644 --- a/swimlane/core/resources/app_revision.py +++ b/swimlane/core/resources/app_revision.py @@ -1,4 +1,4 @@ -from pendulum import pendulum +import pendulum from swimlane.core.resources.app import App from swimlane.core.resources.base import APIResource diff --git a/tests/resources/test_app_revision.py b/tests/resources/test_app_revision.py index e69de29b..54bbdfb2 100644 --- a/tests/resources/test_app_revision.py +++ b/tests/resources/test_app_revision.py @@ -0,0 +1,30 @@ +import pendulum +import pytest + +from swimlane.core.resources.app import App +from swimlane.core.resources.app_revision import AppRevision +from swimlane.core.resources.usergroup import UserGroup + + +@pytest.fixture +def mock_ar_raw_app_revision(raw_app_revision_data): + return raw_app_revision_data[0] + + +@pytest.fixture +def mock_ar_app_revision(mock_swimlane, mock_ar_raw_app_revision): + return AppRevision(mock_swimlane, mock_ar_raw_app_revision) + + +class TestAppRevision(object): + def test_constructor(self, mock_swimlane, mock_ar_app_revision, mock_ar_raw_app_revision): + assert str(mock_ar_app_revision.modified_date) == str(pendulum.parse(mock_ar_raw_app_revision['modifiedDate'])) + assert mock_ar_app_revision.revision_number is mock_ar_raw_app_revision['revisionNumber'] + assert mock_ar_app_revision.status is mock_ar_raw_app_revision['status'] + assert str(mock_ar_app_revision.user) == str(UserGroup(mock_swimlane, mock_ar_raw_app_revision['userId'])) + + def test_version(self, mock_ar_app_revision): + version = mock_ar_app_revision.version + + assert isinstance(version, App) + assert version._raw is mock_ar_app_revision._raw['version'] diff --git a/tests/resources/test_record_revision.py b/tests/resources/test_record_revision.py index c678c9d3..eeeda906 100644 --- a/tests/resources/test_record_revision.py +++ b/tests/resources/test_record_revision.py @@ -39,8 +39,10 @@ def mock_rr_record_revision(mock_rr_raw_record_revision, mock_rr_app): class TestRecordRevision(object): def test_constructor(self, mock_swimlane, mock_rr_record_revision, mock_rr_raw_record_revision): - assert mock_rr_record_revision.app_revision_number is mock_rr_raw_record_revision['version']['applicationRevision'] - assert str(mock_rr_record_revision.modified_date) == str(pendulum.parse(mock_rr_raw_record_revision['modifiedDate'])) + assert mock_rr_record_revision.app_revision_number is mock_rr_raw_record_revision['version'][ + 'applicationRevision'] + assert str(mock_rr_record_revision.modified_date) == str( + pendulum.parse(mock_rr_raw_record_revision['modifiedDate'])) assert mock_rr_record_revision.revision_number is mock_rr_raw_record_revision['revisionNumber'] assert mock_rr_record_revision.status is mock_rr_raw_record_revision['status'] assert str(mock_rr_record_revision.user) == str(UserGroup(mock_swimlane, mock_rr_raw_record_revision['userId'])) @@ -51,8 +53,9 @@ def test_app_version(self, mock_rr_record_revision, mock_rr_app_revision, mock_r assert isinstance(app_version, App) assert app_version is mock_rr_app_revision.version - def test_version(self, mock_rr_app_revision, mock_rr_record_revision, mock_rr_raw_record_revision): + def test_version(self, mock_rr_app_revision, mock_rr_record_revision): version = mock_rr_record_revision.version assert isinstance(version, Record) assert version.app is mock_rr_app_revision.version + assert version._raw is mock_rr_record_revision._raw['version'] From 52dbd4466cb30d3084f347b708f3e0e2f08f032d Mon Sep 17 00:00:00 2001 From: Jon Ford Date: Tue, 14 May 2019 10:07:31 -0400 Subject: [PATCH 10/17] add app and record tests to cover adapters, reduce deuplication in history tests --- tests/fields/test_history.py | 692 +-------------------------------- tests/resources/test_app.py | 137 ++++--- tests/resources/test_record.py | 4 + 3 files changed, 81 insertions(+), 752 deletions(-) diff --git a/tests/fields/test_history.py b/tests/fields/test_history.py index 3395a4c3..e699a62d 100644 --- a/tests/fields/test_history.py +++ b/tests/fields/test_history.py @@ -9,691 +9,17 @@ from swimlane.core.resources.app import App from swimlane.core.resources.record import Record -# an app with two fields: value selection and -raw_app_data = { - "$type":"Core.Models.Application.Application, Core", - "acronym":"PHT", - "trackingFieldId":"5cd46fce433cf20015dd46f7", - "layout":[ - { - "$type":"Core.Models.Layouts.FieldLayout, Core", - "fieldId":"agy01", - "helpTextType":"none", - "helpText":" ", - "layoutType":"field", - "id":"5cd46fdac741d5d54a2b5c32", - "row":1, - "col":1, - "sizex":2.0, - "sizey":0.0 - }, - { - "$type":"Core.Models.Layouts.FieldLayout, Core", - "fieldId":"axtys", - "helpTextType":"none", - "helpText":" ", - "layoutType":"field", - "id":"5cd46fddb36d66d467d4abe6", - "row":1, - "col":3, - "sizex":2.0, - "sizey":0.0 - } - ], - "fields":[ - { - "$type":"Core.Models.Fields.History.HistoryField, Core", - "id":"axtys", - "name":"History", - "key":"history", - "fieldType":"history", - "required":False, - "readOnly":False, - "supportsMultipleOutputMappings":False - }, - { - "$type":"Core.Models.Fields.ValuesListField, Core", - "values":[ - { - "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", - "id":"5cd46fe3ca4746cda7e41a33", - "name":"value1", - "selected":False, - "description":"", - "otherText":False, - "otherTextDescription":"", - "otherTextDefaultValue":"", - "otherTextRequired":"False" - } - ], - "controlType":"select", - "selectionType":"single", - "id":"agy01", - "name":"Selection", - "key":"selection", - "fieldType":"valuesList", - "required":False, - "readOnly":False, - "supportsMultipleOutputMappings":False - }, - { - "$type":"Core.Models.Fields.TrackingField, Core", - "prefix":"PHT-", - "id":"5cd46fce433cf20015dd46f7", - "name":"Tracking Id", - "key":"tracking-id", - "fieldType":"tracking", - "readOnly":True, - "supportsMultipleOutputMappings":False - } - ], - "maxTrackingId":1.0, - "workspaces":[ - "5cd46fce433cf20015dd4727" - ], - "createWorkspace":False, - "createdDate":"2019-05-09T18:22:06.181Z", - "createdByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "modifiedDate":"2019-05-09T18:24:02.642Z", - "modifiedByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "timeTrackingEnabled":False, - "permissions":{ - "$type":"Core.Models.Security.PermissionMatrix, Core" - }, - "id":"a34xbNOoo2P3ivyjY", - "name":"Pydriver History Test", - "disabled":False -} - -raw_record_data = { - "$type":"Core.Models.Record.Record, Core", - "name":"PHT-1", - "allowed":[ - - ], - "trackingId":1.0, - "trackingFull":"PHT-1", - "applicationId":"a34xbNOoo2P3ivyjY", - "isNew":False, - "values":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", - "agy01":{ - "$type":"Core.Models.Record.ValueSelection, Core", - "id":"5cd46fe3ca4746cda7e41a33", - "value":"value1" - }, - "5cd46fce433cf20015dd46f7":"PHT-1" - }, - "repeatFieldCurrentValues":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" - }, - "actionsExecuted":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" - }, - "visualizations":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" - }, - "applicationRevision":3.0, - "locked":False, - "comments":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" - }, - "createdDate":"2019-05-09T18:23:39.881Z", - "modifiedDate":"2019-05-09T18:24:20.027Z", - "createdByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "modifiedByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "sessionTimeSpent":0, - "totalTimeSpent":0, - "timeTrackingEnabled":True, - "isHangfireCreatedAndUnpersisted":False, - "infiniteLoopFlag":False, - "id":"aPkMtjrzV54YxyS59", - "disabled":False -} - -raw_record_revision_data = [ - { - "$type":"Core.Models.History.Revision, Core", - "revisionNumber":3.0, - "status":"current", - "userId":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "modifiedDate":"2019-05-09T18:24:20.027Z", - "version":{ - "$type":"Core.Models.Record.Record, Core", - "name":"PHT-1", - "allowed":[ - - ], - "trackingId":1.0, - "trackingFull":"PHT-1", - "applicationId":"a34xbNOoo2P3ivyjY", - "referencedRecordIds":[ - - ], - "referencedByIds":[ - - ], - "isNew":False, - "values":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", - "agy01":{ - "$type":"Core.Models.Record.ValueSelection, Core", - "id":"5cd46fe3ca4746cda7e41a33", - "value":"value1" - }, - "5cd46fce433cf20015dd46f7":"PHT-1" - }, - "repeatFieldCurrentValues":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" - }, - "valuesDocument":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", - "agy01":{ - "$type":"Core.Models.Record.ValueSelection, Core", - "id":"5cd46fe3ca4746cda7e41a33", - "value":"value1" - }, - "5cd46fce433cf20015dd46f7":"PHT-1" - }, - "actionsExecuted":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" - }, - "visualizations":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" - }, - "applicationRevision":3.0, - "locked":False, - "comments":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" - }, - "createdDate":"2019-05-09T18:23:39.881Z", - "modifiedDate":"2019-05-09T18:24:20.027Z", - "createdByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "modifiedByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "sessionTimeSpent":0, - "totalTimeSpent":0, - "timeTrackingEnabled":True, - "isHangfireCreatedAndUnpersisted":False, - "infiniteLoopFlag":False, - "id":"aPkMtjrzV54YxyS59", - "disabled":False - } - }, - { - "$type":"Core.Models.History.Revision, Core", - "revisionNumber":2.0, - "status":"historical", - "userId":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "modifiedDate":"2019-05-09T18:23:48.539Z", - "version":{ - "$type":"Core.Models.Record.Record, Core", - "name":"PHT-1", - "allowed":[ - - ], - "trackingId":1.0, - "trackingFull":"PHT-1", - "applicationId":"a34xbNOoo2P3ivyjY", - "referencedRecordIds":[ - - ], - "referencedByIds":[ - - ], - "isNew":False, - "values":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", - "agy01":{ - "$type":"Core.Models.Record.ValueSelection, Core", - "id":"5cd46fe379be68acd2039061", - "value":"value2" - }, - "5cd46fce433cf20015dd46f7":"PHT-1" - }, - "repeatFieldCurrentValues":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" - }, - "actionsExecuted":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" - }, - "visualizations":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" - }, - "applicationRevision":2.0, - "locked":False, - "comments":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" - }, - "createdDate":"2019-05-09T18:23:39.881Z", - "modifiedDate":"2019-05-09T18:23:48.539Z", - "createdByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "modifiedByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "sessionTimeSpent":0, - "totalTimeSpent":0, - "timeTrackingEnabled":True, - "isHangfireCreatedAndUnpersisted":False, - "infiniteLoopFlag":False, - "id":"aPkMtjrzV54YxyS59", - "disabled":False - } - }, - { - "$type":"Core.Models.History.Revision, Core", - "revisionNumber":1.0, - "status":"historical", - "userId":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "modifiedDate":"2019-05-09T18:23:39.881Z", - "version":{ - "$type":"Core.Models.Record.Record, Core", - "name":"PHT-1", - "allowed":[ - - ], - "trackingId":1.0, - "trackingFull":"PHT-1", - "applicationId":"a34xbNOoo2P3ivyjY", - "referencedRecordIds":[ - - ], - "referencedByIds":[ - - ], - "isNew":False, - "values":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", - "agy01":{ - "$type":"Core.Models.Record.ValueSelection, Core", - "id":"5cd46fe302c8ff905e042cc0", - "value":"value3" - }, - "5cd46fce433cf20015dd46f7":"PHT-1" - }, - "repeatFieldCurrentValues":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" - }, - "actionsExecuted":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" - }, - "visualizations":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" - }, - "applicationRevision":2.0, - "locked":False, - "comments":{ - "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" - }, - "createdDate":"2019-05-09T18:23:39.881Z", - "modifiedDate":"2019-05-09T18:23:39.881Z", - "createdByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "modifiedByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "sessionTimeSpent":0, - "totalTimeSpent":0, - "timeTrackingEnabled":True, - "isHangfireCreatedAndUnpersisted":False, - "infiniteLoopFlag":False, - "id":"aPkMtjrzV54YxyS59", - "disabled":False - } - } -] - -raw_app_revision_data = [ - { - "$type":"Core.Models.History.Revision, Core", - "revisionNumber":3.0, - "status":"current", - "userId":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "modifiedDate":"2019-05-09T18:24:02.642Z", - "version":{ - "$type":"Core.Models.Application.Application, Core", - "acronym":"PHT", - "trackingFieldId":"5cd46fce433cf20015dd46f7", - "layout":[ - { - "$type":"Core.Models.Layouts.FieldLayout, Core", - "fieldId":"agy01", - "helpTextType":"none", - "helpText":" ", - "layoutType":"field", - "id":"5cd46fdac741d5d54a2b5c32", - "row":1, - "col":1, - "sizex":2.0, - "sizey":0.0 - }, - { - "$type":"Core.Models.Layouts.FieldLayout, Core", - "fieldId":"axtys", - "helpTextType":"none", - "helpText":" ", - "layoutType":"field", - "id":"5cd46fddb36d66d467d4abe6", - "row":1, - "col":3, - "sizex":2.0, - "sizey":0.0 - } - ], - "fields":[ - { - "$type":"Core.Models.Fields.History.HistoryField, Core", - "id":"axtys", - "name":"History", - "key":"history", - "fieldType":"history", - "required":False, - "readOnly":False, - "supportsMultipleOutputMappings":False - }, - { - "$type":"Core.Models.Fields.ValuesListField, Core", - "values":[ - { - "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", - "id":"5cd46fe3ca4746cda7e41a33", - "name":"value1", - "selected":False, - "description":"", - "otherText":False, - "otherTextDescription":"", - "otherTextDefaultValue":"", - "otherTextRequired":"False" - } - ], - "controlType":"select", - "selectionType":"single", - "id":"agy01", - "name":"Selection", - "key":"selection", - "fieldType":"valuesList", - "required":False, - "readOnly":False, - "supportsMultipleOutputMappings":False - }, - { - "$type":"Core.Models.Fields.TrackingField, Core", - "prefix":"PHT-", - "id":"5cd46fce433cf20015dd46f7", - "name":"Tracking Id", - "key":"tracking-id", - "fieldType":"tracking", - "readOnly":True, - "supportsMultipleOutputMappings":False - } - ], - "maxTrackingId":1.0, - "workspaces":[ - - ], - "createWorkspace":False, - "createdDate":"2019-05-09T18:22:06.181Z", - "createdByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "modifiedDate":"2019-05-09T18:24:02.642Z", - "modifiedByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "timeTrackingEnabled":False, - "id":"a34xbNOoo2P3ivyjY", - "name":"Pydriver History Test", - "disabled":False - } - }, - { - "$type":"Core.Models.History.Revision, Core", - "revisionNumber":2.0, - "status":"historical", - "userId":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "modifiedDate":"2019-05-09T18:23:08.766Z", - "version":{ - "$type":"Core.Models.Application.Application, Core", - "acronym":"PHT", - "trackingFieldId":"5cd46fce433cf20015dd46f7", - "layout":[ - { - "$type":"Core.Models.Layouts.FieldLayout, Core", - "fieldId":"agy01", - "helpTextType":"none", - "helpText":" ", - "layoutType":"field", - "id":"5cd46fdac741d5d54a2b5c32", - "row":1, - "col":1, - "sizex":2.0, - "sizey":0.0 - }, - { - "$type":"Core.Models.Layouts.FieldLayout, Core", - "fieldId":"axtys", - "helpTextType":"none", - "helpText":" ", - "layoutType":"field", - "id":"5cd46fddb36d66d467d4abe6", - "row":1, - "col":3, - "sizex":2.0, - "sizey":0.0 - } - ], - "fields":[ - { - "$type":"Core.Models.Fields.ValuesListField, Core", - "values":[ - { - "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", - "id":"5cd46fe3ca4746cda7e41a33", - "name":"value1", - "selected":False, - "description":"", - "otherText":False, - "otherTextDescription":"", - "otherTextDefaultValue":"", - "otherTextRequired":"False" - }, - { - "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", - "id":"5cd46fe379be68acd2039061", - "name":"value2", - "selected":False, - "description":"", - "otherText":False, - "otherTextDescription":"", - "otherTextDefaultValue":"", - "otherTextRequired":"False" - }, - { - "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", - "id":"5cd46fe302c8ff905e042cc0", - "name":"value3", - "selected":False, - "description":"", - "otherText":False, - "otherTextDescription":"", - "otherTextDefaultValue":"", - "otherTextRequired":"False" - } - ], - "controlType":"select", - "selectionType":"single", - "id":"agy01", - "name":"Selection", - "key":"selection", - "fieldType":"valuesList", - "required":False, - "readOnly":False, - "supportsMultipleOutputMappings":False - }, - { - "$type":"Core.Models.Fields.History.HistoryField, Core", - "id":"axtys", - "name":"History", - "key":"history", - "fieldType":"history", - "required":False, - "readOnly":False, - "supportsMultipleOutputMappings":False - }, - { - "$type":"Core.Models.Fields.TrackingField, Core", - "prefix":"PHT-", - "id":"5cd46fce433cf20015dd46f7", - "name":"Tracking Id", - "key":"tracking-id", - "fieldType":"tracking", - "readOnly":True, - "supportsMultipleOutputMappings":False - } - ], - "maxTrackingId":1.0, - "workspaces":[ - - ], - "createWorkspace":False, - "createdDate":"2019-05-09T18:22:06.181Z", - "createdByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "modifiedDate":"2019-05-09T18:23:08.766Z", - "modifiedByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "timeTrackingEnabled":False, - "id":"a34xbNOoo2P3ivyjY", - "name":"Pydriver History Test", - "disabled":False - } - }, - { - "$type":"Core.Models.History.Revision, Core", - "revisionNumber":1.0, - "status":"historical", - "userId":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "modifiedDate":"2019-05-09T18:22:06.181Z", - "version":{ - "$type":"Core.Models.Application.Application, Core", - "acronym":"PHT", - "trackingFieldId":"5cd46fce433cf20015dd46f7", - "layout":[ - - ], - "fields":[ - { - "$type":"Core.Models.Fields.TrackingField, Core", - "prefix":"PHT-", - "id":"5cd46fce433cf20015dd46f7", - "name":"Tracking Id", - "key":"tracking-id", - "fieldType":"tracking", - "readOnly":True, - "supportsMultipleOutputMappings":False - } - ], - "maxTrackingId":0.0, - "workspaces":[ - - ], - "createWorkspace":False, - "createdDate":"2019-05-09T18:22:06.181Z", - "createdByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "modifiedDate":"2019-05-09T18:22:06.181Z", - "modifiedByUser":{ - "$type":"Core.Models.Utilities.UserGroupSelection, Core", - "id":"aBZ3vZmPSsd6l4GLj", - "name":"admin" - }, - "timeTrackingEnabled":False, - "id":"a34xbNOoo2P3ivyjY", - "name":"Pydriver History Test", - "disabled":False - } - } -] +# slight hack, these are pytest fixtures but need to be used outside a test in the request_mock function below +from tests.conftest_revisions import raw_app_revision_data, raw_record_revision_data def request_mock(method, endpoint, params=None): - """Mocks swimlane.request function, returns the API responses for the history endpoints""" + """Mocks swimlane.request function, returns the API responses for the history endpoints.""" if 'record' in endpoint: - return_value = raw_record_revision_data + return_value = raw_record_revision_data() else: revision = float(endpoint.split('/')[3]) - return_value = next(x for x in raw_app_revision_data if x['revisionNumber'] == revision) + return_value = next(x for x in raw_app_revision_data() if x['revisionNumber'] == revision) mock_response = mock.MagicMock() mock_response.json.return_value = return_value @@ -701,15 +27,15 @@ def request_mock(method, endpoint, params=None): @pytest.fixture -def mock_swimlane_history(mock_swimlane): +def mock_history_swimlane(mock_swimlane): with mock.patch.object(mock_swimlane, 'request', side_effect=request_mock): yield mock_swimlane @pytest.fixture -def mock_history_record(mock_swimlane_history): - app = App(mock_swimlane_history, raw_app_data) - return Record(app, raw_record_data) +def mock_history_record(mock_history_swimlane, mock_revision_app, mock_revision_record): + app = App(mock_history_swimlane, mock_revision_app._raw) + return Record(app, mock_revision_record._raw) @pytest.fixture diff --git a/tests/resources/test_app.py b/tests/resources/test_app.py index 64df6c8e..e4fbab63 100644 --- a/tests/resources/test_app.py +++ b/tests/resources/test_app.py @@ -1,75 +1,74 @@ import copy - import pytest +from swimlane.core.adapters import AppRevisionAdapter from swimlane.exceptions import UnknownField -def test_repr(mock_app): - assert repr(mock_app) == '' - - -def test_get_field_definitions(mock_app): - """Test retrieving field definitions by name, key, or id and UnknownField error with recommendation(s)""" - field_def = mock_app.get_field_definition_by_name('Numeric') - assert field_def['name'] == 'Numeric' - - assert field_def == mock_app.get_field_definition_by_id(field_def['id']) - - # Test UnknownField metadata - # Suggestions for potential typo - try: - mock_app.get_field_definition_by_name('Muneric') - except UnknownField as error: - assert error.app is mock_app - assert error.field_name == 'Muneric' - assert error.similar_field_names == ['Numeric'] - assert 'Numeric' in str(error) - else: - raise RuntimeError - - # Same behavior for get_by_id - try: - mock_app.get_field_definition_by_id('aqkf3') - except UnknownField as error: - assert error.app is mock_app - assert error.field_name == 'aqkf3' - assert error.similar_field_names == ['aqkg3', 'ayqk6', 'aqc6k'] - else: - raise RuntimeError - - -def test_resolve_field_name(mock_app): - """Test that fields keys + names resolve to field name or None if not found""" - # Field name - assert mock_app.resolve_field_name('Action') == 'Action' - # Key - assert mock_app.resolve_field_name('action-key') == 'Action' - # Missing - assert mock_app.resolve_field_name('unknown field name/key') is None - - -def test_get_field_definition_by_name_resolves_keys(mock_app): - """Test that field keys are auto-resolved to field names when getting definition by name""" - assert mock_app.get_field_definition_by_name('action-key') is mock_app.get_field_definition_by_name('Action') - - -def test_equality(mock_app): - """Test equality and inequality of Apps by name""" - app_clone = copy.copy(mock_app) - assert app_clone == mock_app - assert not app_clone != mock_app - - app_clone.name = mock_app.name + 'test' - assert app_clone != mock_app - assert not app_clone == mock_app - - -def test_comparison(mock_app, mock_record): - with pytest.raises(TypeError): - mock_app < mock_record - - app_clone = copy.copy(mock_app) - app_clone.name = mock_app.name + 'test' - assert mock_app < app_clone - assert app_clone > mock_app +class TestApp(object): + def test_repr(self, mock_app): + assert repr(mock_app) == '' + + def test_get_field_definitions(self, mock_app): + """Test retrieving field definitions by name, key, or id and UnknownField error with recommendation(s)""" + field_def = mock_app.get_field_definition_by_name('Numeric') + assert field_def['name'] == 'Numeric' + + assert field_def == mock_app.get_field_definition_by_id(field_def['id']) + + # Test UnknownField metadata + # Suggestions for potential typo + try: + mock_app.get_field_definition_by_name('Muneric') + except UnknownField as error: + assert error.app is mock_app + assert error.field_name == 'Muneric' + assert error.similar_field_names == ['Numeric'] + assert 'Numeric' in str(error) + else: + raise RuntimeError + + # Same behavior for get_by_id + try: + mock_app.get_field_definition_by_id('aqkf3') + except UnknownField as error: + assert error.app is mock_app + assert error.field_name == 'aqkf3' + assert error.similar_field_names == ['aqkg3', 'ayqk6', 'aqc6k'] + else: + raise RuntimeError + + def test_resolve_field_name(self, mock_app): + """Test that fields keys + names resolve to field name or None if not found""" + # Field name + assert mock_app.resolve_field_name('Action') == 'Action' + # Key + assert mock_app.resolve_field_name('action-key') == 'Action' + # Missing + assert mock_app.resolve_field_name('unknown field name/key') is None + + def test_get_field_definition_by_name_resolves_keys(self, mock_app): + """Test that field keys are auto-resolved to field names when getting definition by name""" + assert mock_app.get_field_definition_by_name('action-key') is mock_app.get_field_definition_by_name('Action') + + def test_equality(self, mock_app): + """Test equality and inequality of Apps by name""" + app_clone = copy.copy(mock_app) + assert app_clone == mock_app + assert not app_clone != mock_app + + app_clone.name = mock_app.name + 'test' + assert app_clone != mock_app + assert not app_clone == mock_app + + def test_comparison(self, mock_app, mock_record): + with pytest.raises(TypeError): + mock_app < mock_record + + app_clone = copy.copy(mock_app) + app_clone.name = mock_app.name + 'test' + assert mock_app < app_clone + assert app_clone > mock_app + + def test_revisions(self, mock_app): + assert isinstance(mock_app.revisions, AppRevisionAdapter) diff --git a/tests/resources/test_record.py b/tests/resources/test_record.py index 763345c9..aae486c4 100644 --- a/tests/resources/test_record.py +++ b/tests/resources/test_record.py @@ -4,6 +4,7 @@ import mock import pytest +from swimlane.core.adapters import RecordRevisionAdapter from swimlane.core.resources.record import Record, record_factory from swimlane.exceptions import UnknownField, ValidationError @@ -204,3 +205,6 @@ def test_for_json_dumps(self, mock_record): """Test .for_json() with all fields passes json.dumps() with no specific assertions beyond no exceptions""" result = mock_record.for_json() json.dumps(result) + + def test_revisions(self, mock_record): + assert isinstance(mock_record.revisions, RecordRevisionAdapter) From 5a9f0eb4dab004ee6a4831d5cb86bdc60e51575b Mon Sep 17 00:00:00 2001 From: Jon Ford Date: Tue, 14 May 2019 11:22:30 -0400 Subject: [PATCH 11/17] support resource cache for app revisions --- swimlane/core/adapters/__init__.py | 2 +- swimlane/core/adapters/app_revision.py | 13 ++++++++--- swimlane/core/adapters/record_revision.py | 4 +--- swimlane/core/resources/app_revision.py | 27 ++++++++++++++++++++--- tests/fields/test_history.py | 5 +---- tests/resources/test_app_revision.py | 7 ++++++ 6 files changed, 44 insertions(+), 14 deletions(-) diff --git a/swimlane/core/adapters/__init__.py b/swimlane/core/adapters/__init__.py index 6973a133..7cd3b4a2 100644 --- a/swimlane/core/adapters/__init__.py +++ b/swimlane/core/adapters/__init__.py @@ -6,4 +6,4 @@ from .usergroup import UserAdapter, GroupAdapter from .helper import HelperAdapter from .app_revision import AppRevisionAdapter -from .record_revision import RecordRevisionAdapter \ No newline at end of file +from .record_revision import RecordRevisionAdapter diff --git a/swimlane/core/adapters/app_revision.py b/swimlane/core/adapters/app_revision.py index 74154370..e061f797 100644 --- a/swimlane/core/adapters/app_revision.py +++ b/swimlane/core/adapters/app_revision.py @@ -1,12 +1,12 @@ from swimlane.core.cache import check_cache from swimlane.core.resolver import AppResolver from swimlane.core.resources.app_revision import AppRevision +from swimlane.utils import one_of_keyword_only class AppRevisionAdapter(AppResolver): """Handles retrieval and creation of Swimlane App Revision resources""" - @check_cache(AppRevision) def get(self, revision_number): """Gets a specific app revision. @@ -19,6 +19,13 @@ def get(self, revision_number): AppRevision: The AppRevision for the given revision number. """ - app_revision_raw = self._swimlane.request('get', 'app/{0}/history/{1}'.format(self._app.id, - revision_number)).json() + key_value = AppRevision.get_unique_id(self._app.id, revision_number) + app_revision_raw = self.__get(app_id_revision=key_value) return AppRevision(self._swimlane, app_revision_raw) + + @check_cache(AppRevision) + @one_of_keyword_only('app_id_revision') + def __get(self, key, value): + """Underlying get method supporting resource cache.""" + app_id, revision_number = AppRevision.parse_unique_id(value) + return self._swimlane.request('get', 'app/{0}/history/{1}'.format(app_id, revision_number)).json() diff --git a/swimlane/core/adapters/record_revision.py b/swimlane/core/adapters/record_revision.py index 21f7b59f..9555212e 100644 --- a/swimlane/core/adapters/record_revision.py +++ b/swimlane/core/adapters/record_revision.py @@ -1,6 +1,7 @@ from swimlane.core.cache import check_cache from swimlane.core.resolver import AppResolver from swimlane.core.resources.record_revision import RecordRevision +from swimlane.utils import one_of_keyword_only class RecordRevisionAdapter(AppResolver): @@ -10,12 +11,9 @@ def __init__(self, app, record): super(RecordRevisionAdapter, self).__init__(app) self.record = record - @check_cache(RecordRevision) def get(self): """Get all revisions for a single record. - Supports resource cache - Returns: RecordRevision[]: All record revisions for the given record ID. """ diff --git a/swimlane/core/resources/app_revision.py b/swimlane/core/resources/app_revision.py index 1bf362e2..0e8afde5 100644 --- a/swimlane/core/resources/app_revision.py +++ b/swimlane/core/resources/app_revision.py @@ -6,9 +6,7 @@ class AppRevision(APIResource): - """ - Encapsulates a single revision returned from a History lookup. - """ + """Encapsulates a single revision returned from a History lookup.""" def __init__(self, swimlane, raw): super(AppRevision, self).__init__(swimlane, raw) @@ -23,3 +21,26 @@ def __init__(self, swimlane, raw): def __str__(self): return '{} ({})'.format(self.version, self.revision_number) + + @staticmethod + def __separator(): + """ + Separator for unique ids. Unlikely to be found in application ids. Although technically we do not currently + validate app ids in the backend for specific characters so this sequence could be found. + """ + return ' --- ' + + @staticmethod + def get_unique_id(app_id, revision_number): + """Return the unique identifier for the given AppRevision.""" + return '{0}{1}{2}'.format(app_id, AppRevision.__separator(), revision_number) + + @staticmethod + def parse_unique_id(unique_id): + return unique_id.split(AppRevision.__separator()) + + def get_cache_index_keys(self): + """Returns cache index keys for this AppRevision.""" + return { + 'app_id_revision': self.get_unique_id(self.version.id, self.revision_number) + } diff --git a/tests/fields/test_history.py b/tests/fields/test_history.py index e699a62d..a5915867 100644 --- a/tests/fields/test_history.py +++ b/tests/fields/test_history.py @@ -54,7 +54,7 @@ def test_num_revisions(self, history): def test_revision_str(self, history): for revision in history: - assert str(revision) == 'PHT-1 ({})'.format(revision.revision_number) + assert str(revision) == 'PHT-1 ({0})'.format(revision.revision_number) def test_revisions(self, history, mock_history_record): for idx, revision in enumerate(history): @@ -65,6 +65,3 @@ def test_revisions(self, history, mock_history_record): assert isinstance(revision.version, Record) assert revision.version.id == mock_history_record.id assert len(history) - revision.revision_number == idx - - def test_app_revision_caching(self, mock_swimlane): - assert True is True diff --git a/tests/resources/test_app_revision.py b/tests/resources/test_app_revision.py index 54bbdfb2..005a952b 100644 --- a/tests/resources/test_app_revision.py +++ b/tests/resources/test_app_revision.py @@ -28,3 +28,10 @@ def test_version(self, mock_ar_app_revision): assert isinstance(version, App) assert version._raw is mock_ar_app_revision._raw['version'] + + def test_get_cache_index(self, mock_ar_app_revision): + keys = mock_ar_app_revision.get_cache_index_keys() + + assert len(keys) is 1 + assert 'app_id_revision' in keys + assert keys['app_id_revision'] == 'a34xbNOoo2P3ivyjY --- 3.0' From 4812c3c2731e33b8d34c7d197557eb12ac6299c6 Mon Sep 17 00:00:00 2001 From: Jon Ford Date: Tue, 14 May 2019 11:29:11 -0400 Subject: [PATCH 12/17] remove unneeded imports --- swimlane/core/adapters/app_revision.py | 8 ++++---- swimlane/core/adapters/record_revision.py | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/swimlane/core/adapters/app_revision.py b/swimlane/core/adapters/app_revision.py index e061f797..533bf6b7 100644 --- a/swimlane/core/adapters/app_revision.py +++ b/swimlane/core/adapters/app_revision.py @@ -5,7 +5,7 @@ class AppRevisionAdapter(AppResolver): - """Handles retrieval and creation of Swimlane App Revision resources""" + """Handles retrieval of Swimlane App Revision resources""" def get(self, revision_number): """Gets a specific app revision. @@ -20,12 +20,12 @@ def get(self, revision_number): """ key_value = AppRevision.get_unique_id(self._app.id, revision_number) - app_revision_raw = self.__get(app_id_revision=key_value) - return AppRevision(self._swimlane, app_revision_raw) + return self.__get(app_id_revision=key_value) @check_cache(AppRevision) @one_of_keyword_only('app_id_revision') def __get(self, key, value): """Underlying get method supporting resource cache.""" app_id, revision_number = AppRevision.parse_unique_id(value) - return self._swimlane.request('get', 'app/{0}/history/{1}'.format(app_id, revision_number)).json() + app_revision_raw = self._swimlane.request('get', 'app/{0}/history/{1}'.format(app_id, revision_number)).json() + return AppRevision(self._swimlane, app_revision_raw) diff --git a/swimlane/core/adapters/record_revision.py b/swimlane/core/adapters/record_revision.py index 9555212e..223941e9 100644 --- a/swimlane/core/adapters/record_revision.py +++ b/swimlane/core/adapters/record_revision.py @@ -1,11 +1,9 @@ -from swimlane.core.cache import check_cache from swimlane.core.resolver import AppResolver from swimlane.core.resources.record_revision import RecordRevision -from swimlane.utils import one_of_keyword_only class RecordRevisionAdapter(AppResolver): - """Handles retrieval and creation of Swimlane Record Revision resources""" + """Handles retrieval of Swimlane Record Revision resources""" def __init__(self, app, record): super(RecordRevisionAdapter, self).__init__(app) From a6cd8ab6bee1f312bc80d32f094f6185191a1cbb Mon Sep 17 00:00:00 2001 From: Jon Ford Date: Tue, 14 May 2019 14:27:33 -0400 Subject: [PATCH 13/17] add revision base class --- swimlane/core/resources/app_revision.py | 30 ++++++++-------- swimlane/core/resources/record_revision.py | 37 +++++++------------- swimlane/core/resources/revision_base.py | 40 ++++++++++++++++++++++ 3 files changed, 66 insertions(+), 41 deletions(-) create mode 100644 swimlane/core/resources/revision_base.py diff --git a/swimlane/core/resources/app_revision.py b/swimlane/core/resources/app_revision.py index 0e8afde5..9de3424a 100644 --- a/swimlane/core/resources/app_revision.py +++ b/swimlane/core/resources/app_revision.py @@ -1,26 +1,24 @@ -import pendulum - from swimlane.core.resources.app import App -from swimlane.core.resources.base import APIResource -from swimlane.core.resources.usergroup import UserGroup +from swimlane.core.resources.revision_base import Revision + +class AppRevision(Revision): + """ + Encapsulates a single revision returned from a History lookup. -class AppRevision(APIResource): - """Encapsulates a single revision returned from a History lookup.""" + Attributes: + Attributes: + modified_date: The date this app revision was created. + revision_number: The revision number of this app revision. + status: Indicates whether this revision is the current revision or a historical revision. + user: The user that saved this revision of the record. + version: The App corresponding to the data contained in this app revision. + """ def __init__(self, swimlane, raw): super(AppRevision, self).__init__(swimlane, raw) - self.version = App(swimlane, raw['version']) - - self.modified_date = pendulum.parse(self._raw['modifiedDate']) - self.revision_number = self._raw['revisionNumber'] - self.status = self._raw['status'] - - # UserGroupSelection, can't set as User without additional lookup - self.user = UserGroup(self._swimlane, self._raw['userId']) - def __str__(self): - return '{} ({})'.format(self.version, self.revision_number) + self.version = App(swimlane, self._version) @staticmethod def __separator(): diff --git a/swimlane/core/resources/record_revision.py b/swimlane/core/resources/record_revision.py index 76a884fa..fecb3236 100644 --- a/swimlane/core/resources/record_revision.py +++ b/swimlane/core/resources/record_revision.py @@ -1,39 +1,26 @@ -import pendulum +from swimlane.core.resources.revision_base import Revision -from swimlane.core.resources.base import APIResource -from swimlane.core.resources.usergroup import UserGroup - -class RecordRevision(APIResource): +class RecordRevision(Revision): """ Encapsulates a single revision returned from a History lookup. + + Attributes: + app_revision_number: The app revision number this record revision was created using. + + Properties: + app_version: Returns an App corresponding to the app_revision_number of this record revision. + version: Returns a Record corresponding to the app_version and data contained in this record revision. """ def __init__(self, app, raw): super(RecordRevision, self).__init__(app._swimlane, raw) + self.__app_version = None self.__version = None - self._app = app - self.app_revision_number = self._raw['version']['applicationRevision'] - self.modified_date = pendulum.parse(self._raw['modifiedDate']) - self.revision_number = self._raw['revisionNumber'] - self.status = self._raw['status'] - - # UserGroupSelection, can't set as User without additional lookup - self.user = UserGroup(self._swimlane, self._raw['userId']) - - def __str__(self): - return '{} ({})'.format(self.version, self.revision_number) - - def for_json(self): - """Return revision metadata""" - return { - 'modifiedDate': self._raw['modifiedDate'], - 'revisionNumber': self.revision_number, - 'user': self.user.for_json() - } + self.app_revision_number = self._version['applicationRevision'] @property def app_version(self): @@ -48,5 +35,5 @@ def version(self): if not self.__version: # avoid circular imports from swimlane.core.resources.record import Record - self.__version = Record(self.app_version, self._raw['version']) + self.__version = Record(self.app_version, self._version) return self.__version diff --git a/swimlane/core/resources/revision_base.py b/swimlane/core/resources/revision_base.py new file mode 100644 index 00000000..03412228 --- /dev/null +++ b/swimlane/core/resources/revision_base.py @@ -0,0 +1,40 @@ +import pendulum + +from swimlane.core.resources.base import APIResource +from swimlane.core.resources.usergroup import UserGroup + + +class Revision(APIResource): + """ + The base class representing a single revision returned from a History lookup. + + Attributes: + Attributes: + modified_date: The date this app revision was created. + revision_number: The revision number of this app revision. + status: Indicates whether this revision is the current revision or a historical revision. + user: The user that saved this revision of the record. + """ + + def __init__(self, swimlane, raw): + super(Revision, self).__init__(swimlane, raw) + + self.modified_date = pendulum.parse(self._raw['modifiedDate']) + self.revision_number = self._raw['revisionNumber'] + self.status = self._raw['status'] + + # UserGroupSelection, can't set as User without additional lookup + self.user = UserGroup(self._swimlane, self._raw['userId']) + + self._version = self._raw['version'] + + def __str__(self): + return '{} ({})'.format(self.version, self.revision_number) + + def for_json(self): + """Return revision metadata""" + return { + 'modifiedDate': self._raw['modifiedDate'], + 'revisionNumber': self.revision_number, + 'user': self.user.for_json() + } From 99febf9ed3a8b7dcbd4694310973cf539376e2e2 Mon Sep 17 00:00:00 2001 From: Jon Ford Date: Tue, 14 May 2019 18:02:37 -0400 Subject: [PATCH 14/17] add documentation, add ability to get by revision number and get all revisions for both App and Record revisions (and tests) --- docs/examples/resources.rst | 80 +++++++++++++++++++ swimlane/core/adapters/app_revision.py | 14 +++- swimlane/core/adapters/record_revision.py | 21 ++++- swimlane/core/fields/history.py | 2 +- swimlane/core/resources/revision_base.py | 6 +- tests/adapters/test_app_revision_adapter.py | 46 +++++++---- .../adapters/test_record_revision_adapter.py | 42 ++++++---- 7 files changed, 171 insertions(+), 40 deletions(-) diff --git a/docs/examples/resources.rst b/docs/examples/resources.rst index dfd1a985..739b6be7 100644 --- a/docs/examples/resources.rst +++ b/docs/examples/resources.rst @@ -55,6 +55,7 @@ on the report. ('Reference (Single-Select)', 'equals', target_record) ) + Keyword-searches can be performed by providing a `keywords` list parameter. All records with fields matching the provided keywords will be returned @@ -67,6 +68,7 @@ provided keywords will be returned ] ) + Available operators are just strings as shown above, but are made available as constants in the :mod:`~swimlane.core.search` module @@ -79,6 +81,7 @@ Available operators are just strings as shown above, but are made available as c ('Number Field', GTE, 0), ) + .. warning:: Report results are retrieved during on-demand during iteration, requesting record data from the API before each loop @@ -99,6 +102,7 @@ Available operators are just strings as shown above, but are made available as c limit=0 ) + To operate on large search results as records are returned from API or retrieve only partial results :class:`~swimlane.core.resources.report.Report` should be used instead. @@ -159,6 +163,7 @@ Any records not passing validation will cause the entire operation to fail. ... ) + .. note:: .. versionchanged:: 2.17.0 @@ -181,6 +186,7 @@ Delete multiple records at once. app.records.bulk_delete(record1, record2, record3) + Delete multiple records at once by filters using filter format from search. .. code-block:: python @@ -191,6 +197,7 @@ Delete multiple records at once by filters using filter format from search. ('Field_2', 'equals', value2) ) + Bulk Record Modify ^^^^^^^^^^^^^^^^^^^ @@ -217,6 +224,7 @@ Invalid field values will cause entire operation to fail. } ) + Bulk modify records by filter tuples without record instances. .. code-block:: python @@ -234,6 +242,7 @@ Bulk modify records by filter tuples without record instances. } ) + Use bulk modify to append, remove, or clear list field values .. code-block:: python @@ -250,6 +259,24 @@ Use bulk modify to append, remove, or clear list field values } ) + +Retrieve App Revisions +^^^^^^^^^^^^^^^^^^^^^^ + +Retrieve historical revisions of the application. + +.. code-block:: python + + # get all revisions + app_revisions = app.revisions.get_all() + + # get by revision number + app_revision = app.revisions.get(2) + + # get the historical version of the app + historical_app_version = app_revision.version + + Record ------ @@ -451,6 +478,24 @@ Record restrictions can be modified using :meth:`~swimlane.core.resources.record record.save() +Retrieve Record Revisions +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Retrieve historical revisions of the record. + +.. code-block:: python + + # get all revisions + record_revisions = record.revisions.get_all() + + # get by revision number + record_revision = record.revisions.get(2) + + # get the historical version of the app + # automatically retrieves the corresponding app revision to create the Record object + historical_record_version = record_revision.version + + UserGroup --------- @@ -519,3 +564,38 @@ To iterate over individual users in a group, use group.users property group = swimlane.groups.get(name='Everyone') for user in group.users: assert isinstance(user, User) + + +Revisions +--------- + +Revisions represent historical versions of another resource. Currently, App and Record revisions are supported. For more +details on how to retrieve revisions, see the "Retrieve App Revisions" and "Retrieve Record Revisions" sections above. + +Get Information About the Revision +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + revision = app.revisions.get(1) + + revision.modified_date # The date this revision was created. + revision.revision_number # The revision number of this revision. + revision.status # Indicates whether this revision is the current revision or a historical revision. + revision.user # The user that saved this revision. + + app = revision.version # returns an App or Record object representing the revision depending on revision type. + +Record Revisions +^^^^^^^^^^^^^^^^ + +Record revisions additionally have attributes containing information about their app. + +.. code-block:: python + + revision = record.revisions.get(1) + + revision.app_revision_number # The app revision number this record revision was created using. + + app = revision.app_version # Returns an App corresponding to the app_revision_number of this record revision. + diff --git a/swimlane/core/adapters/app_revision.py b/swimlane/core/adapters/app_revision.py index 533bf6b7..0664515c 100644 --- a/swimlane/core/adapters/app_revision.py +++ b/swimlane/core/adapters/app_revision.py @@ -7,8 +7,19 @@ class AppRevisionAdapter(AppResolver): """Handles retrieval of Swimlane App Revision resources""" + def get_all(self): + """ + Gets all app revisions. + + Returns: + AppRevision[]: Returns all AppRevisions for this Adapter's app. + """ + raw_revisions = self._swimlane.request('get', 'app/{0}/history'.format(self._app.id)).json() + return [AppRevision(self._swimlane, raw) for raw in raw_revisions] + def get(self, revision_number): - """Gets a specific app revision. + """ + Gets a specific app revision. Supports resource cache @@ -18,7 +29,6 @@ def get(self, revision_number): Returns: AppRevision: The AppRevision for the given revision number. """ - key_value = AppRevision.get_unique_id(self._app.id, revision_number) return self.__get(app_id_revision=key_value) diff --git a/swimlane/core/adapters/record_revision.py b/swimlane/core/adapters/record_revision.py index 223941e9..ba3991f8 100644 --- a/swimlane/core/adapters/record_revision.py +++ b/swimlane/core/adapters/record_revision.py @@ -9,14 +9,27 @@ def __init__(self, app, record): super(RecordRevisionAdapter, self).__init__(app) self.record = record - def get(self): + def get_all(self): """Get all revisions for a single record. Returns: RecordRevision[]: All record revisions for the given record ID. """ - response = self._swimlane.request('get', 'app/{0}/record/{1}/history'.format(self._app.id, self.record.id)) + raw_revisions = self._swimlane.request('get', + 'app/{0}/record/{1}/history'.format(self._app.id, self.record.id)).json() + return [RecordRevision(self._app, raw) for raw in raw_revisions] - raw_revisions = response.json() + def get(self, revision_number): + """Gets a specific record revision. - return [RecordRevision(self._app, raw) for raw in raw_revisions] + Keyword Args: + revision_number (float): Record revision number + + Returns: + RecordRevision: The RecordRevision for the given revision number. + """ + record_revision_raw = self._swimlane.request('get', + 'app/{0}/record/{1}/history/{2}'.format(self._app.id, + self.record.id, + revision_number)).json() + return RecordRevision(self._app, record_revision_raw) diff --git a/swimlane/core/fields/history.py b/swimlane/core/fields/history.py index 3ac00d2b..d50a89a4 100644 --- a/swimlane/core/fields/history.py +++ b/swimlane/core/fields/history.py @@ -18,7 +18,7 @@ def _evaluate(self): def _retrieve_revisions(self): """Populate RecordRevision instances.""" - return self._record.revisions.get() + return self._record.revisions.get_all() class HistoryField(ReadOnly, CursorField): diff --git a/swimlane/core/resources/revision_base.py b/swimlane/core/resources/revision_base.py index 03412228..56b686a0 100644 --- a/swimlane/core/resources/revision_base.py +++ b/swimlane/core/resources/revision_base.py @@ -10,10 +10,10 @@ class Revision(APIResource): Attributes: Attributes: - modified_date: The date this app revision was created. - revision_number: The revision number of this app revision. + modified_date: The date this revision was created. + revision_number: The revision number of this revision. status: Indicates whether this revision is the current revision or a historical revision. - user: The user that saved this revision of the record. + user: The user that saved this revision. """ def __init__(self, swimlane, raw): diff --git a/tests/adapters/test_app_revision_adapter.py b/tests/adapters/test_app_revision_adapter.py index 9f7b517f..b29cfe01 100644 --- a/tests/adapters/test_app_revision_adapter.py +++ b/tests/adapters/test_app_revision_adapter.py @@ -3,20 +3,34 @@ from swimlane.core.resources.app_revision import AppRevision -def test_get(mock_swimlane, mock_revision_app, raw_app_revision_data): - mock_response = mock.MagicMock() - # return just one revision in response - raw_revision = raw_app_revision_data[0] - mock_response.json.return_value = raw_revision - mock_response.status_code = 200 - - with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): - revision = mock_revision_app.revisions.get(raw_revision['revisionNumber']) - - mock_swimlane.request.assert_called_with('get', 'app/{0}/history/{1}'.format(mock_revision_app.id, - raw_revision['revisionNumber'])) - - assert isinstance(revision, AppRevision) - assert revision.revision_number is raw_revision['revisionNumber'] - assert revision.status is raw_revision['status'] +class TestAppRevisionAdapter(object): + def test_get_all(self, mock_swimlane, mock_revision_app, raw_app_revision_data): + mock_response = mock.MagicMock() + raw_revision = raw_app_revision_data + mock_response.json.return_value = raw_revision + mock_response.status_code = 200 + + with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): + revisions = mock_revision_app.revisions.get_all() + + mock_swimlane.request.assert_called_with('get', 'app/{0}/history'.format(mock_revision_app.id,)) + + for idx, revision in enumerate(revisions): + assert isinstance(revision, AppRevision) + assert revision.revision_number is raw_app_revision_data[idx]['revisionNumber'] + + def test_get(self, mock_swimlane, mock_revision_app, raw_app_revision_data): + mock_response = mock.MagicMock() + raw_revision = raw_app_revision_data[0] + mock_response.json.return_value = raw_revision + mock_response.status_code = 200 + + with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): + revision = mock_revision_app.revisions.get(raw_revision['revisionNumber']) + + mock_swimlane.request.assert_called_with('get', 'app/{0}/history/{1}'.format(mock_revision_app.id, + raw_revision['revisionNumber'])) + + assert isinstance(revision, AppRevision) + assert revision.revision_number is raw_revision['revisionNumber'] diff --git a/tests/adapters/test_record_revision_adapter.py b/tests/adapters/test_record_revision_adapter.py index cd683fa2..2b330203 100644 --- a/tests/adapters/test_record_revision_adapter.py +++ b/tests/adapters/test_record_revision_adapter.py @@ -1,23 +1,37 @@ - - import mock from swimlane.core.resources.record_revision import RecordRevision -from swimlane.core.resources.usergroup import UserGroup -def test_get(mock_swimlane, mock_revision_record, raw_record_revision_data): - mock_response = mock.MagicMock() - mock_response.status_code = 200 - mock_response.json.return_value = raw_record_revision_data +class TestRecordRevisionAdapter(object): + def test_get_all(self, mock_swimlane, mock_revision_record, raw_record_revision_data): + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = raw_record_revision_data + + with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): + revisions = mock_revision_record.revisions.get_all() + + mock_swimlane.request.assert_called_with('get', + 'app/{0}/record/{1}/history'.format(mock_revision_record.app.id, + mock_revision_record.id)) + + for idx, revision in enumerate(revisions): + assert isinstance(revision, RecordRevision) + assert revision.revision_number is raw_record_revision_data[idx]['revisionNumber'] + + def test_get(self, mock_swimlane, mock_revision_record, raw_record_revision_data): + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = raw_record_revision_data[0] - with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): - revisions = mock_revision_record.revisions.get() + with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): + revision = mock_revision_record.revisions.get(3) - mock_swimlane.request.assert_called_with('get', 'app/{0}/record/{1}/history'.format(mock_revision_record.app.id, - mock_revision_record.id)) + mock_swimlane.request.assert_called_with('get', 'app/{0}/record/{1}/history/{2}'.format( + mock_revision_record.app.id, + mock_revision_record.id, + 3)) - for idx, revision in enumerate(revisions): assert isinstance(revision, RecordRevision) - assert revision.revision_number is raw_record_revision_data[idx]['revisionNumber'] - assert revision.status is raw_record_revision_data[idx]['status'] + assert revision.revision_number is raw_record_revision_data[0]['revisionNumber'] From d2451c95ff23e31c00c41fcc80d1b41555963ec4 Mon Sep 17 00:00:00 2001 From: Jon Ford Date: Wed, 15 May 2019 10:07:46 -0400 Subject: [PATCH 15/17] add version to revision base class, add tests for str(revision) and revision.for_json, additional docs --- docs/examples/fields.rst | 5 ++-- docs/examples/resources.rst | 6 ++++ swimlane/core/resources/app_revision.py | 17 +++++++---- swimlane/core/resources/record_revision.py | 16 +++++----- swimlane/core/resources/revision_base.py | 11 +++++-- tests/fields/test_history.py | 35 ++++++++++------------ tests/resources/test_app_revision.py | 12 ++++++++ tests/resources/test_record_revision.py | 12 ++++++++ 8 files changed, 74 insertions(+), 40 deletions(-) diff --git a/docs/examples/fields.rst b/docs/examples/fields.rst index dfb32324..204dc6c4 100644 --- a/docs/examples/fields.rst +++ b/docs/examples/fields.rst @@ -575,9 +575,10 @@ Cursor managing iteration and addition of comments HistoryField ------------ -Returns a readonly RevisionCursor object that abstracts out retrieval and pagination of record history +Returns a readonly RevisionCursor object that abstracts out retrieval of record history. -Performs additional request(s) to history API endpoint as accessed +Each item in the RevisionCursor is a RecordRevision object, which performs additional requests to history API endpoints +as accessed. See the "Resources" section of the documentation for more information about the RecordRevision object. .. code-block:: python diff --git a/docs/examples/resources.rst b/docs/examples/resources.rst index 739b6be7..430532a1 100644 --- a/docs/examples/resources.rst +++ b/docs/examples/resources.rst @@ -586,6 +586,11 @@ Get Information About the Revision app = revision.version # returns an App or Record object representing the revision depending on revision type. + # additional functions + text = str(revision) # returns name of the revision and the revision number as a string + json = revision.for_json # returns a dict containing modifiedDate, revisionNumber, and user keys/attribute values + + Record Revisions ^^^^^^^^^^^^^^^^ @@ -599,3 +604,4 @@ Record revisions additionally have attributes containing information about their app = revision.app_version # Returns an App corresponding to the app_revision_number of this record revision. + diff --git a/swimlane/core/resources/app_revision.py b/swimlane/core/resources/app_revision.py index 9de3424a..68e973cc 100644 --- a/swimlane/core/resources/app_revision.py +++ b/swimlane/core/resources/app_revision.py @@ -1,8 +1,8 @@ from swimlane.core.resources.app import App -from swimlane.core.resources.revision_base import Revision +from swimlane.core.resources.revision_base import RevisionBase -class AppRevision(Revision): +class AppRevision(RevisionBase): """ Encapsulates a single revision returned from a History lookup. @@ -14,12 +14,9 @@ class AppRevision(Revision): user: The user that saved this revision of the record. version: The App corresponding to the data contained in this app revision. """ - def __init__(self, swimlane, raw): super(AppRevision, self).__init__(swimlane, raw) - self.version = App(swimlane, self._version) - @staticmethod def __separator(): """ @@ -30,13 +27,21 @@ def __separator(): @staticmethod def get_unique_id(app_id, revision_number): - """Return the unique identifier for the given AppRevision.""" + """Returns the unique identifier for the given AppRevision.""" return '{0}{1}{2}'.format(app_id, AppRevision.__separator(), revision_number) @staticmethod def parse_unique_id(unique_id): + """Returns an array containing two items: the app_id and revision number parsed from the given unique_id.""" return unique_id.split(AppRevision.__separator()) + @property + def version(self): + """Returns an App from the _raw_version info in this app revision. Lazy loaded. Overridden from base class.""" + if not self._version: + self._version = App(self._swimlane, self._raw_version) + return self._version + def get_cache_index_keys(self): """Returns cache index keys for this AppRevision.""" return { diff --git a/swimlane/core/resources/record_revision.py b/swimlane/core/resources/record_revision.py index fecb3236..a528d336 100644 --- a/swimlane/core/resources/record_revision.py +++ b/swimlane/core/resources/record_revision.py @@ -1,7 +1,7 @@ -from swimlane.core.resources.revision_base import Revision +from swimlane.core.resources.revision_base import RevisionBase -class RecordRevision(Revision): +class RecordRevision(RevisionBase): """ Encapsulates a single revision returned from a History lookup. @@ -12,15 +12,13 @@ class RecordRevision(Revision): app_version: Returns an App corresponding to the app_revision_number of this record revision. version: Returns a Record corresponding to the app_version and data contained in this record revision. """ - def __init__(self, app, raw): super(RecordRevision, self).__init__(app._swimlane, raw) self.__app_version = None - self.__version = None self._app = app - self.app_revision_number = self._version['applicationRevision'] + self.app_revision_number = self._raw_version['applicationRevision'] @property def app_version(self): @@ -31,9 +29,9 @@ def app_version(self): @property def version(self): - """The record contained in this record revision. Lazy loaded""" - if not self.__version: + """The record contained in this record revision. Lazy loaded. Overridden from base class.""" + if not self._version: # avoid circular imports from swimlane.core.resources.record import Record - self.__version = Record(self.app_version, self._version) - return self.__version + self._version = Record(self.app_version, self._raw_version) + return self._version diff --git a/swimlane/core/resources/revision_base.py b/swimlane/core/resources/revision_base.py index 56b686a0..55700009 100644 --- a/swimlane/core/resources/revision_base.py +++ b/swimlane/core/resources/revision_base.py @@ -4,7 +4,7 @@ from swimlane.core.resources.usergroup import UserGroup -class Revision(APIResource): +class RevisionBase(APIResource): """ The base class representing a single revision returned from a History lookup. @@ -17,7 +17,7 @@ class Revision(APIResource): """ def __init__(self, swimlane, raw): - super(Revision, self).__init__(swimlane, raw) + super(RevisionBase, self).__init__(swimlane, raw) self.modified_date = pendulum.parse(self._raw['modifiedDate']) self.revision_number = self._raw['revisionNumber'] @@ -26,11 +26,16 @@ def __init__(self, swimlane, raw): # UserGroupSelection, can't set as User without additional lookup self.user = UserGroup(self._swimlane, self._raw['userId']) - self._version = self._raw['version'] + self._raw_version = self._raw['version'] + self._version = None def __str__(self): return '{} ({})'.format(self.version, self.revision_number) + @property + def version(self): + raise NotImplementedError + def for_json(self): """Return revision metadata""" return { diff --git a/tests/fields/test_history.py b/tests/fields/test_history.py index a5915867..ff6d6e14 100644 --- a/tests/fields/test_history.py +++ b/tests/fields/test_history.py @@ -1,7 +1,6 @@ -from datetime import datetime - import mock import pytest +import pendulum from swimlane.core.fields.history import RevisionCursor from swimlane.core.resources.record_revision import RecordRevision @@ -9,25 +8,21 @@ from swimlane.core.resources.app import App from swimlane.core.resources.record import Record -# slight hack, these are pytest fixtures but need to be used outside a test in the request_mock function below -from tests.conftest_revisions import raw_app_revision_data, raw_record_revision_data - - -def request_mock(method, endpoint, params=None): - """Mocks swimlane.request function, returns the API responses for the history endpoints.""" - if 'record' in endpoint: - return_value = raw_record_revision_data() - else: - revision = float(endpoint.split('/')[3]) - return_value = next(x for x in raw_app_revision_data() if x['revisionNumber'] == revision) - - mock_response = mock.MagicMock() - mock_response.json.return_value = return_value - return mock_response - @pytest.fixture -def mock_history_swimlane(mock_swimlane): +def mock_history_swimlane(mock_swimlane, raw_app_revision_data, raw_record_revision_data): + def request_mock(method, endpoint, params=None): + """Mocks swimlane.request function, returns the API responses for the history endpoints.""" + if 'record' in endpoint: + return_value = raw_record_revision_data + else: + revision = float(endpoint.split('/')[3]) + return_value = next(x for x in raw_app_revision_data if x['revisionNumber'] == revision) + + mock_response = mock.MagicMock() + mock_response.json.return_value = return_value + return mock_response + with mock.patch.object(mock_swimlane, 'request', side_effect=request_mock): yield mock_swimlane @@ -60,7 +55,7 @@ def test_revisions(self, history, mock_history_record): for idx, revision in enumerate(history): assert isinstance(revision, RecordRevision) assert isinstance(revision.app_version, App) - assert isinstance(revision.modified_date, datetime) + assert isinstance(revision.modified_date, pendulum.datetime) assert isinstance(revision.user, UserGroup) assert isinstance(revision.version, Record) assert revision.version.id == mock_history_record.id diff --git a/tests/resources/test_app_revision.py b/tests/resources/test_app_revision.py index 005a952b..d3301e06 100644 --- a/tests/resources/test_app_revision.py +++ b/tests/resources/test_app_revision.py @@ -35,3 +35,15 @@ def test_get_cache_index(self, mock_ar_app_revision): assert len(keys) is 1 assert 'app_id_revision' in keys assert keys['app_id_revision'] == 'a34xbNOoo2P3ivyjY --- 3.0' + + def test_for_json(self, mock_ar_app_revision): + json = mock_ar_app_revision.for_json() + + assert 'modifiedDate' in json + assert 'revisionNumber' in json + assert 'user' in json + + def test_str(self, mock_ar_app_revision): + text = str(mock_ar_app_revision) + + assert text == 'Pydriver History Test (PHT) (3.0)' diff --git a/tests/resources/test_record_revision.py b/tests/resources/test_record_revision.py index eeeda906..f8836788 100644 --- a/tests/resources/test_record_revision.py +++ b/tests/resources/test_record_revision.py @@ -59,3 +59,15 @@ def test_version(self, mock_rr_app_revision, mock_rr_record_revision): assert isinstance(version, Record) assert version.app is mock_rr_app_revision.version assert version._raw is mock_rr_record_revision._raw['version'] + + def test_for_json(self, mock_rr_record_revision): + json = mock_rr_record_revision.for_json() + + assert 'modifiedDate' in json + assert 'revisionNumber' in json + assert 'user' in json + + def test_str(self, mock_rr_record_revision): + text = str(mock_rr_record_revision) + + assert text == 'PHT-1 (3.0)' From 20fba02bbad001f9a970afd9298ad9deeb175a35 Mon Sep 17 00:00:00 2001 From: Jon Ford Date: Wed, 15 May 2019 11:50:50 -0400 Subject: [PATCH 16/17] move import to top of record_revision.py --- swimlane/core/resources/record_revision.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/swimlane/core/resources/record_revision.py b/swimlane/core/resources/record_revision.py index a528d336..95e8bc29 100644 --- a/swimlane/core/resources/record_revision.py +++ b/swimlane/core/resources/record_revision.py @@ -1,3 +1,4 @@ +from swimlane.core.resources.record import Record from swimlane.core.resources.revision_base import RevisionBase @@ -31,7 +32,5 @@ def app_version(self): def version(self): """The record contained in this record revision. Lazy loaded. Overridden from base class.""" if not self._version: - # avoid circular imports - from swimlane.core.resources.record import Record self._version = Record(self.app_version, self._raw_version) return self._version From bb550cd1e1cee2055292ea4dc3715f97c32963de Mon Sep 17 00:00:00 2001 From: Jon Ford Date: Thu, 16 May 2019 10:01:21 -0400 Subject: [PATCH 17/17] remove redundant __init__, switch separator to a const --- swimlane/core/resources/app_revision.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/swimlane/core/resources/app_revision.py b/swimlane/core/resources/app_revision.py index 68e973cc..fb0d8a16 100644 --- a/swimlane/core/resources/app_revision.py +++ b/swimlane/core/resources/app_revision.py @@ -14,26 +14,20 @@ class AppRevision(RevisionBase): user: The user that saved this revision of the record. version: The App corresponding to the data contained in this app revision. """ - def __init__(self, swimlane, raw): - super(AppRevision, self).__init__(swimlane, raw) - @staticmethod - def __separator(): - """ - Separator for unique ids. Unlikely to be found in application ids. Although technically we do not currently - validate app ids in the backend for specific characters so this sequence could be found. - """ - return ' --- ' + # Separator for unique ids. Unlikely to be found in application ids. Although technically we do not currently + # validate app ids in the backend for specific characters so this sequence could be found. + SEPARATOR = ' --- ' @staticmethod def get_unique_id(app_id, revision_number): """Returns the unique identifier for the given AppRevision.""" - return '{0}{1}{2}'.format(app_id, AppRevision.__separator(), revision_number) + return '{0}{1}{2}'.format(app_id, AppRevision.SEPARATOR, revision_number) @staticmethod def parse_unique_id(unique_id): """Returns an array containing two items: the app_id and revision number parsed from the given unique_id.""" - return unique_id.split(AppRevision.__separator()) + return unique_id.split(AppRevision.SEPARATOR) @property def version(self):