Update revision and document tables and add more unit tests.

This commit is contained in:
Felipe Monteiro 2017-07-23 01:50:57 +01:00
parent f230d9b364
commit 8b79789425
4 changed files with 124 additions and 42 deletions

View File

@ -72,7 +72,7 @@ class BaseResource(object):
def return_error(self, resp, status_code, message="", retry=False):
resp.body = json.dumps(
{'type': 'error', 'message': message, 'retry': retry})
{'type': 'error', 'message': str(message), 'retry': retry})
resp.status = status_code

View File

@ -121,16 +121,15 @@ def document_create(values, session=None):
values['schema_version'] = values.pop('schemaVersion')
session = session or get_session()
filters = copy.copy(models.Document.UNIQUE_CONSTRAINTS)
filters = [f for f in filters if f != 'revision_index']
filters = models.Document.UNIQUE_CONSTRAINTS
existing_document = document_get(**{c: values[c] for c in filters})
created_document = {}
def _document_changed():
other_document = copy.deepcopy(existing_document)
other_document = other_document.to_dict()
# The document has changed if at least one value in ``values`` differs.
for key, val in values.items():
if val != other_document[key]:
if val != existing_document[key]:
return True
return False
@ -139,62 +138,87 @@ def document_create(values, session=None):
with session.begin():
document.update(values)
document.save(session=session)
return document
return document.to_dict()
created_document = {}
if existing_document:
# Only generate a new revision and entirely new document if anything
# was changed.
if _document_changed():
revision_index = revision_update(
revision_index=existing_document['revision_index'])['id']
values['revision_index'] = revision_index
created_document = _document_create().to_dict()
# TODO: indicate that now document was actually created.
created_document = _document_create()
revision_update(created_document['id'], existing_document['id'])
else:
revision_index = revision_create()['id']
values['revision_index'] = revision_index
created_document = _document_create().to_dict()
created_document = _document_create()
revision_create(created_document['id'])
return created_document
def document_get(session=None, **filters):
session = session or get_session()
document = session.query(models.Document)\
.filter_by(**filters)\
.options(sa_orm.joinedload("revision_index"))\
.order_by(desc(models.Revision.created_at))\
.first()
document = session.query(models.Document).filter_by(**filters).first()
return document.to_dict() if document else {}
####################
def revision_create(session=None):
def revision_create(document_id, session=None):
session = session or get_session()
revision = models.Revision()
with session.begin():
revision.update({'document_id': document_id})
revision.save(session=session)
return revision.to_dict()
def revision_update(session=None, revision_index=None):
def revision_get(document_id, session=None):
session = session or get_session()
previous_revision = session.query(models.Revision).get(revision_index)
revision = session.query(models.Revision)\
.filter_by(document_id=document_id).first()
return revision.to_dict()
new_revision = models.Revision()
def revision_update(document_id, child_document_id, session=None):
"""Create a parent revision and update the child revision.
The ``document_id`` references the newly created document that is a more
up-to-date revision. Create a new (parent) revision that references
``document_id`` and whose ``child_id`` is ``child_document_id``.
Set the ``parent_id`` for ``child_revision`` to ``document_id``.
After this function has executed, the following relationship is true:
parent_document <-- parent_revision
^ /
\ (has child)
\ /
\ /
\ /
/ \
/ \
/ \
/ (has parent)
v \
child_document <-- child_revision
:param document_id: The ID corresponding to the up-to-date document.
:param child_document_id: The ID corresponding tothe out-of-date document.
:param session: The database session.
:returns: The dictionary representation of the newly created revision.
"""
session = session or get_session()
parent_revision = models.Revision()
with session.begin():
# Create the new revision with a reference to the previous revision.
new_revision.update({'previous': revision_index})
new_revision.save(session=session)
parent_revision.update({'document_id': document_id,
'child_id': child_document_id})
parent_revision.save(session=session)
# Update the previous revision with a reference to the new revision.
previous_revision.update({'next': new_revision.id})
previous_revision.save(session=session)
child_revision = session.query(models.Revision)\
.filter_by(document_id=child_document_id).first()
with session.begin():
child_revision.update({'parent_id': document_id})
child_revision.save(session=session)
return new_revision.to_dict()
return parent_revision.to_dict()

View File

@ -128,20 +128,20 @@ class Revision(BASE, DeckhandBase):
id = Column(String(36), primary_key=True,
default=lambda: str(uuid.uuid4()))
previous = Column(Integer, ForeignKey('revisions.id'), nullable=True)
next = Column(Integer, ForeignKey('revisions.id'), nullable=True)
document_id = Column(Integer, ForeignKey('documents.id'), nullable=True)
parent_id = Column(Integer, ForeignKey('documents.id'), nullable=True)
child_id = Column(Integer, ForeignKey('documents.id'), nullable=True)
document = relationship("Document", back_populates="revision",
foreign_keys=[document_id])
class Document(BASE, DeckhandBase):
UNIQUE_CONSTRAINTS = ('schema_version', 'kind', 'revision_index')
UNIQUE_CONSTRAINTS = ('schema_version', 'kind')
__tablename__ = 'documents'
__table_args__ = (DeckhandBase.gen_unqiue_contraint(*UNIQUE_CONSTRAINTS),)
#__table_args__ = (DeckhandBase.gen_unqiue_contraint(*UNIQUE_CONSTRAINTS),)
id = Column(String(36), primary_key=True,
default=lambda: str(uuid.uuid4()))
revision_index = Column(Integer, ForeignKey('revisions.id'),
nullable=False)
revision = relationship(Revision, backref=backref('revisions'))
schema_version = Column(String(64), nullable=False)
kind = Column(String(64), nullable=False)
# NOTE: Do not define a maximum length for these JSON data below. However,
@ -149,6 +149,9 @@ class Document(BASE, DeckhandBase):
# "metadata" is reserved, so use "doc_metadata" instead.
doc_metadata = Column(JSONEncodedDict(), nullable=False)
data = Column(JSONEncodedDict(), nullable=False)
revision = relationship("Revision", uselist=False,
back_populates="document",
foreign_keys="[Revision.document_id]")
def register_models(engine):

View File

@ -13,8 +13,10 @@
# limitations under the License.
import mock
import uuid
import testtools
from testtools import matchers
from deckhand.db.sqlalchemy import api as db_api
from deckhand.tests.unit import base
@ -24,7 +26,7 @@ class DocumentFixture(object):
def get_minimal_fixture(self, **kwargs):
fixture = {'data': 'fake document data',
'metadata': 'fake meta',
'metadata': 'fake metadata',
'kind': 'FakeConfigType',
'schemaVersion': 'deckhand/v1'}
fixture.update(kwargs)
@ -43,7 +45,60 @@ class TestDocumentsApi(base.DeckhandWithDBTestCase):
self.assertIn(key, actual)
self.assertEqual(val, actual[key])
def _validate_revision(self, revision):
expected_attrs = ('id', 'document_id', 'child_id', 'parent_id')
for attr in expected_attrs:
self.assertIn(attr, revision)
self.assertThat(revision[attr], matchers.MatchesAny(
matchers.Is(None), matchers.IsInstance(unicode)))
def test_create_document(self):
fixture = DocumentFixture().get_minimal_fixture()
document = db_api.document_create(fixture)
self._validate_document(fixture, document)
revision = db_api.revision_get(document['id'])
self._validate_revision(revision)
self.assertEqual(document['id'], revision['document_id'])
def test_create_and_update_document(self):
"""
Check that the following relationship is true:
parent_document <-- parent_revision
^ /
\ (has child)
\ /
\ /
\ /
/ \
/ \
/ \
/ (has parent)
v \
child_document <-- child_revision
"""
fixture = DocumentFixture().get_minimal_fixture()
child_document = db_api.document_create(fixture)
fixture['metadata'] = 'modified fake metadata'
parent_document = db_api.document_create(fixture)
self._validate_document(fixture, parent_document)
# Validate that the new document was created.
self.assertEqual('modified fake metadata',
parent_document['doc_metadata'])
self.assertNotEqual(child_document['id'], parent_document['id'])
# Validate that the parent document has a different revision and
# that the revisions and document links are correct.
child_revision = db_api.revision_get(child_document['id'])
parent_revision = db_api.revision_get(parent_document['id'])
for revision in (child_revision, parent_revision):
self._validate_revision(revision)
self.assertNotEqual(child_revision['id'], parent_revision['id'])
self.assertEqual(parent_document['id'],
parent_revision['document_id'])
self.assertEqual(child_document['id'], parent_revision['child_id'])
self.assertEqual(parent_document['id'], child_revision['parent_id'])