Skip to content

Commit

Permalink
Merge pull request #59 from swimlane/SPT-4113-bugfix-history-field
Browse files Browse the repository at this point in the history
SPT-4113: Support for App and Record Revisions, along with history fields
  • Loading branch information
jondavidford authored May 17, 2019
2 parents 588fc50 + bb550cd commit 797912f
Show file tree
Hide file tree
Showing 20 changed files with 1,325 additions and 593 deletions.
5 changes: 3 additions & 2 deletions docs/examples/fields.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions docs/examples/resources.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -159,6 +163,7 @@ Any records not passing validation will cause the entire operation to fail.
...
)
.. note::

.. versionchanged:: 2.17.0
Expand All @@ -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
Expand All @@ -191,6 +197,7 @@ Delete multiple records at once by filters using filter format from search.
('Field_2', 'equals', value2)
)
Bulk Record Modify
^^^^^^^^^^^^^^^^^^^

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
------

Expand Down Expand Up @@ -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
---------

Expand Down Expand Up @@ -519,3 +564,44 @@ 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.
# 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
^^^^^^^^^^^^^^^^

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.
2 changes: 2 additions & 0 deletions swimlane/core/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
41 changes: 41 additions & 0 deletions swimlane/core/adapters/app_revision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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 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.
Supports resource cache
Keyword Args:
revision_number (float): App 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)

@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)
app_revision_raw = self._swimlane.request('get', 'app/{0}/history/{1}'.format(app_id, revision_number)).json()
return AppRevision(self._swimlane, app_revision_raw)
35 changes: 35 additions & 0 deletions swimlane/core/adapters/record_revision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from swimlane.core.resolver import AppResolver
from swimlane.core.resources.record_revision import RecordRevision


class RecordRevisionAdapter(AppResolver):
"""Handles retrieval of Swimlane Record Revision resources"""

def __init__(self, app, record):
super(RecordRevisionAdapter, self).__init__(app)
self.record = record

def get_all(self):
"""Get all revisions for a single record.
Returns:
RecordRevision[]: All record revisions for the given 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]

def get(self, revision_number):
"""Gets a specific record revision.
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)
50 changes: 2 additions & 48 deletions swimlane/core/fields/history.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -21,50 +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.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'])

# Avoid circular imports
from swimlane.core.resources.record import Record
self.version = Record(self.record.app, 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()
}
"""Populate RecordRevision instances."""
return self._record.revisions.get_all()


class HistoryField(ReadOnly, CursorField):
Expand Down
3 changes: 2 additions & 1 deletion swimlane/core/resources/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 43 additions & 0 deletions swimlane/core/resources/app_revision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from swimlane.core.resources.app import App
from swimlane.core.resources.revision_base import RevisionBase


class AppRevision(RevisionBase):
"""
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.
"""

# 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)

@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 {
'app_id_revision': self.get_unique_id(self.version.id, self.revision_number)
}
4 changes: 4 additions & 0 deletions swimlane/core/resources/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 797912f

Please sign in to comment.