Changeset - c44ddd3740e8
[Not reviewed]
1 6 6
Lance Edgar (lance) - 11 years ago 2013-08-25 11:12:54
lance@edbob.org
Overhauled some database stuff; added tests.
13 files changed with 624 insertions and 181 deletions:
0 comments (0 inline, 0 general)
rattail/core.py
Show inline comments
 
new file 100644
 
#!/usr/bin/env python
 
# -*- coding: utf-8  -*-
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2012 Lance Edgar
 
#
 
#  This file is part of Rattail.
 
#
 
#  Rattail is free software: you can redistribute it and/or modify it under the
 
#  terms of the GNU Affero General Public License as published by the Free
 
#  Software Foundation, either version 3 of the License, or (at your option)
 
#  any later version.
 
#
 
#  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
 
#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 
#  FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for
 
#  more details.
 
#
 
#  You should have received a copy of the GNU Affero General Public License
 
#  along with Rattail.  If not, see <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 

	
 
"""
 
Core Stuff
 
"""
 

	
 
from uuid import uuid1
 

	
 

	
 
__all__ = ['get_uuid']
 

	
 

	
 
def get_uuid():
 
    """
 
    Generate a universally-unique identifier.
 

	
 
    :returns: A 32-character hex string.
 
    """
 

	
 
    return uuid1().hex
rattail/db/__init__.py
Show inline comments
 
@@ -20,125 +20,29 @@
 
#  You should have received a copy of the GNU Affero General Public License
 
#  along with Rattail.  If not, see <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 

	
 
"""
 
``rattail.db`` -- Database Stuff
 
Database Stuff
 
"""
 

	
 
import logging
 
import warnings
 

	
 
from sqlalchemy.event import listen
 
from sqlalchemy.orm import object_mapper, RelationshipProperty
 

	
 
import edbob
 
from edbob.db import Session
 

	
 
import rattail
 

	
 

	
 
ignore_role_changes = None
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
def before_flush(session, flush_context, instances):
 
    """
 
    Listens for session flush events.  This function is responsible for adding
 
    stub records to the 'changes' table, which will in turn be used by the
 
    database synchronizer.
 
    """
 

	
 
    def ensure_uuid(instance):
 
        if instance.uuid:
 
            return
 

	
 
        # If the 'uuid' column is actually a foreign key to another
 
        # table...well, then we can't just generate a new value for it.
 
        # Instead we must traverse the relationship and fetch the existing
 
        # foreign key value...
 

	
 
        mapper = object_mapper(instance)
 
        uuid = mapper.columns['uuid']
 
        if uuid.foreign_keys:
 

	
 
            for prop in mapper.iterate_properties:
 
                if (isinstance(prop, RelationshipProperty)
 
                    and len(prop.remote_side) == 1
 
                    and list(prop.remote_side)[0].key == 'uuid'):
 

	
 
                    foreign_instance = getattr(instance, prop.key)
 
                    ensure_uuid(foreign_instance)
 
                    instance.uuid = foreign_instance.uuid
 
                    break
 
            assert instance.uuid
 

	
 
        # ...but if there is no foreign key, just generate a new UUID.
 
        else:
 
            instance.uuid = edbob.get_uuid()
 

	
 
    def record_change(instance, deleted=False):
 

	
 
        # No need to record changes for Change. :)
 
        if isinstance(instance, rattail.Change):
 
            return
 

	
 
        # No need to record changes for batch data.
 
        if isinstance(instance, (rattail.Batch,
 
                                 rattail.BatchColumn,
 
                                 rattail.BatchRow)):
 
            return
 

	
 
        # Ignore instances which don't use UUID.
 
        if not hasattr(instance, 'uuid'):
 
            return
 

	
 
        # Ignore Role instances, if so configured.
 
        if ignore_role_changes:
 
            if isinstance(instance, (edbob.Role, edbob.UserRole)):
 
                return
 

	
 
        # Provide an UUID value, if necessary.
 
        ensure_uuid(instance)
 

	
 
        # Record the change.
 
        change = session.query(rattail.Change).get(
 
            (instance.__class__.__name__, instance.uuid))
 
        if not change:
 
            change = rattail.Change(
 
                class_name=instance.__class__.__name__,
 
                uuid=instance.uuid)
 
            session.add(change)
 
        change.deleted = deleted
 
        log.debug("before_flush: recorded change: %s" % repr(change))
 

	
 
    for instance in session.deleted:
 
        log.debug("before_flush: deleted instance: %s" % repr(instance))
 
        record_change(instance, deleted=True)
 

	
 
    for instance in session.new:
 
        log.debug("before_flush: new instance: %s" % repr(instance))
 
        record_change(instance)
 

	
 
    for instance in session.dirty:
 
        if session.is_modified(instance, passive=True):
 
            log.debug("before_flush: dirty instance: %s" % repr(instance))
 
            record_change(instance)
 

	
 

	
 
def record_changes(session):
 
    listen(session, 'before_flush', before_flush)
 
from .core import *
 
from .changes import *
 

	
 

	
 
def init(config):
 
    """
 
    Initialize the Rattail database framework.
 
    """
 

	
 
    import edbob
 
    import rattail
 

	
 
    # Pretend all ``edbob`` models come from Rattail, until that is true.
 
    from edbob.db import Base
 
    names = []
 
    for name in edbob.__all__:
 
        obj = getattr(edbob, name)
 
        if isinstance(obj, type) and issubclass(obj, Base):
 
@@ -149,18 +53,18 @@ def init(config):
 
    from edbob import enum
 
    edbob.graft(rattail, enum)
 

	
 
    from rattail.db.extension import model
 
    edbob.graft(rattail, model)
 

	
 
    global ignore_role_changes
 
    ignore_role_changes = config.getboolean(
 
        'rattail.db', 'changes.ignore_roles', default=True)
 

	
 
    if config.getboolean('rattail.db', 'changes.record'):
 
        record_changes(Session)
 
        record_changes(Session, ignore_role_changes)
 

	
 
    elif config.getboolean('rattail.db', 'record_changes'):
 
        import warnings
 
        warnings.warn("Config setting 'record_changes' in section [rattail.db] "
 
                      "is deprecated; please use 'changes.record' instead.",
 
                      DeprecationWarning)
 
        record_changes(Session)
 
        record_changes(Session, ignore_role_changes)
rattail/db/changes.py
Show inline comments
 
new file 100644
 
#!/usr/bin/env python
 
# -*- coding: utf-8  -*-
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2012 Lance Edgar
 
#
 
#  This file is part of Rattail.
 
#
 
#  Rattail is free software: you can redistribute it and/or modify it under the
 
#  terms of the GNU Affero General Public License as published by the Free
 
#  Software Foundation, either version 3 of the License, or (at your option)
 
#  any later version.
 
#
 
#  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
 
#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 
#  FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for
 
#  more details.
 
#
 
#  You should have received a copy of the GNU Affero General Public License
 
#  along with Rattail.  If not, see <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 

	
 
"""
 
Data Changes
 
"""
 

	
 
from sqlalchemy.event import listen
 
from sqlalchemy.orm import object_mapper, RelationshipProperty
 

	
 
from .model import Change, Batch, BatchColumn, BatchRow, Role, UserRole
 
from ..core import get_uuid
 

	
 

	
 
__all__ = ['record_changes']
 

	
 

	
 
import logging
 
log = logging.getLogger(__name__)
 

	
 

	
 
def record_changes(session, ignore_role_changes=True):
 
    """
 
    Record all changes which occur within a session.
 

	
 
    :param session: A ``sqlalchemy.orm.sessionmaker`` class, or an instance
 
       thereof.
 

	
 
    :param ignore_role_changes: Whether changes involving roles and role
 
       membership should be ignored.  This defaults to ``True``, which means
 
       each database will be responsible for maintaining its own role (and by
 
       extension, permissions) data.
 
    """
 

	
 
    listen(session, 'before_flush', ChangeRecorder(ignore_role_changes))
 

	
 

	
 
class ChangeRecorder(object):
 
    """
 
    Listener for session ``before_flush`` events.
 

	
 
    This class is responsible for adding stub records to the ``changes`` table,
 
    which will in turn be used by the database synchronizer to manage change
 
    data propagation.
 
    """
 

	
 
    def __init__(self, ignore_role_changes=True):
 
        self.ignore_role_changes = ignore_role_changes
 

	
 
    def __call__(self, session, flush_context, instances):
 
        """
 
        Method invoked when session ``before_flush`` event occurs.
 
        """
 

	
 
        for instance in session.deleted:
 
            log.debug("ChangeRecorder: found deleted instance: {0}".format(repr(instance)))
 
            self.record_change(session, instance, deleted=True)
 
        for instance in session.new:
 
            log.debug("ChangeRecorder: found new instance: {0}".format(repr(instance)))
 
            self.record_change(session, instance)
 
        for instance in session.dirty:
 
            if session.is_modified(instance, passive=True):
 
                log.debug("ChangeRecorder: found dirty instance: {0}".format(repr(instance)))
 
                self.record_change(session, instance)
 

	
 
    def record_change(self, session, instance, deleted=False):
 
        """
 
        Record a change record in the database.
 

	
 
        If ``instance`` represents a change in which we are interested, then
 
        this method will create (or update) a :class:`rattail.db.model.Change`
 
        record.
 

	
 
        :returns: ``True`` if a change was recorded, or ``False`` if it was
 
           ignored.
 
        """
 

	
 
        # No need to record changes for changes.  Must not use `isinstance()`
 
        # here due to mocking in tests.
 
        if (hasattr(instance.__class__, '__tablename__')
 
            and instance.__class__.__tablename__ == 'changes'):
 
            return False
 

	
 
        # No need to record changes for batch data.
 
        if isinstance(instance, (Batch, BatchColumn, BatchRow)):
 
            return False
 

	
 
        # Ignore instances which don't use UUID.
 
        if not hasattr(instance, 'uuid'):
 
            return False
 

	
 
        # Ignore Role instances, if so configured.
 
        if self.ignore_role_changes and isinstance(instance, (Role, UserRole)):
 
            return False
 

	
 
        # Provide an UUID value, if necessary.
 
        self.ensure_uuid(instance)
 

	
 
        # Record the change.
 
        change = session.query(Change).get(
 
            (instance.__class__.__name__, instance.uuid))
 
        if not change:
 
            change = Change(
 
                class_name=instance.__class__.__name__,
 
                uuid=instance.uuid)
 
            session.add(change)
 
        change.deleted = deleted
 
        log.debug("ChangeRecorder.record_change: recorded change: %s" % repr(change))
 
        return True
 

	
 
    def ensure_uuid(self, instance):
 
        """
 
        Ensure the given instance has a UUID value.
 

	
 
        This uses the following logic:
 

	
 
        * If the instance already has a UUID, nothing will be done.
 

	
 
        * If the instance contains a foreign key to another table, then that
 
          relationship will be traversed and the foreign object's UUID will be used
 
          to populate that of the instance.
 

	
 
        * Otherwise, a new UUID will be generated for the instance.
 
        """
 

	
 
        if instance.uuid:
 
            return
 

	
 
        mapper = object_mapper(instance)
 
        if not mapper.columns['uuid'].foreign_keys:
 
            instance.uuid = get_uuid()
 
            return
 

	
 
        for prop in mapper.iterate_properties:
 
            if (isinstance(prop, RelationshipProperty)
 
                and len(prop.remote_side) == 1
 
                and list(prop.remote_side)[0].key == 'uuid'):
 

	
 
                foreign_instance = getattr(instance, prop.key)
 
                if foreign_instance:
 
                    self.ensure_uuid(foreign_instance)
 
                    instance.uuid = foreign_instance.uuid
 
                    return
 

	
 
        instance.uuid = get_uuid()
 
        log.error("ChangeRecorder.ensure_uuid: unexpected scenario; generated new UUID for instance: {0}".format(repr(instance)))
rattail/db/core.py
Show inline comments
 
new file 100644
 
#!/usr/bin/env python
 
# -*- coding: utf-8  -*-
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2012 Lance Edgar
 
#
 
#  This file is part of Rattail.
 
#
 
#  Rattail is free software: you can redistribute it and/or modify it under the
 
#  terms of the GNU Affero General Public License as published by the Free
 
#  Software Foundation, either version 3 of the License, or (at your option)
 
#  any later version.
 
#
 
#  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
 
#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 
#  FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for
 
#  more details.
 
#
 
#  You should have received a copy of the GNU Affero General Public License
 
#  along with Rattail.  If not, see <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 

	
 
"""
 
Core Data Stuff
 
"""
 

	
 
from sqlalchemy import Column, String
 
from ..core import get_uuid
 

	
 

	
 
__all__ = ['uuid_column']
 

	
 

	
 
def uuid_column(*args, **kwargs):
 
    """
 
    Returns a UUID column for use as a table's primary key.
 
    """
 

	
 
    kwargs.setdefault('primary_key', True)
 
    kwargs.setdefault('nullable', False)
 
    kwargs.setdefault('default', get_uuid)
 
    return Column(String(length=32), *args, **kwargs)
rattail/db/extension/model.py
Show inline comments
 
@@ -23,18 +23,18 @@
 
################################################################################
 

	
 
"""
 
``rattail.db.extension.model`` -- Schema Definition
 
"""
 

	
 
import re
 
import warnings
 
import logging
 

	
 
from sqlalchemy import Column, ForeignKey
 
from sqlalchemy import String, Integer, DateTime, Date, Boolean, Numeric, Text
 
from sqlalchemy import types
 
from sqlalchemy import and_
 
from sqlalchemy.orm import relationship, object_session
 
from sqlalchemy.ext.associationproxy import association_proxy
 
from sqlalchemy.ext.orderinglist import ordering_list
 

	
 
import edbob
 
@@ -143,12 +143,14 @@ class Batch(Base):
 
                           collection_class=ordering_list('ordinal'),
 
                           order_by=BatchColumn.ordinal,
 
                           cascade='save-update, merge, delete, delete-orphan')
 

	
 
    _rowclasses = {}
 

	
 
    sil_type_pattern = re.compile(r'^(CHAR|NUMBER)\((\d+(?:\,\d+)?)\)$')
 

	
 
    def __repr__(self):
 
        return "Batch(uuid={0})".format(repr(self.uuid))
 

	
 
    def __unicode__(self):
 
        return unicode(self.description or '')
 

	
 
@@ -168,25 +170,55 @@ class Batch(Base):
 
                '__tablename__': 'batch.%s' % self.uuid,
 
                'uuid': uuid_column(),
 
                'ordinal': Column(Integer, nullable=False),
 
                }
 

	
 
            for column in self.columns:
 
                data_type = sil.get_sqlalchemy_type(column.data_type)
 
                data_type = self.get_sqlalchemy_type(column.data_type)
 
                kwargs[column.name] = Column(data_type)
 
            rowclass = type('BatchRow_%s' % str(self.uuid), (Base, BatchRow), kwargs)
 

	
 
            batch_uuid = self.uuid
 
            def batch(self):
 
                return object_session(self).query(Batch).get(batch_uuid)
 
            rowclass.batch = property(batch)
 

	
 
            self._rowclasses[self.uuid] = rowclass
 

	
 
        return self._rowclasses[self.uuid]
 

	
 
    @classmethod
 
    def get_sqlalchemy_type(cls, sil_type):
 
        """
 
        Returns a SQLAlchemy data type according to a SIL data type.
 
        """
 

	
 
        if sil_type == 'GPC(14)':
 
            return GPCType()
 

	
 
        if sil_type == 'FLAG(1)':
 
            return Boolean()
 

	
 
        m = cls.sil_type_pattern.match(sil_type)
 
        if m:
 
            data_type, precision = m.groups()
 
            if precision.isdigit():
 
                precision = int(precision)
 
                scale = 0
 
            else:
 
                precision, scale = precision.split(',')
 
                precision = int(precision)
 
                scale = int(scale)
 
            if data_type == 'CHAR':
 
                assert not scale, "FIXME"
 
                return String(precision)
 
            if data_type == 'NUMBER':
 
                return Numeric(precision, scale)
 

	
 
        assert False, "FIXME"
 

	
 
    def add_column(self, sil_name=None, **kwargs):
 
        column = BatchColumn(sil_name, **kwargs)
 
        self.columns.append(column)
 

	
 
    def add_row(self, row, **kwargs):
 
        """
rattail/db/model.py
Show inline comments
 
@@ -23,10 +23,11 @@
 
################################################################################
 

	
 
"""
 
``rattail.db.model`` -- Data Model Namespace
 
"""
 

	
 
from edbob.db.model import Base
 
from edbob.db.model import *
 
from edbob.db.extensions.auth.model import *
 
from edbob.db.extensions.contact.model import *
 

	
 
from rattail.db.extension.model import *
rattail/sil/__init__.py
Show inline comments
 
@@ -29,8 +29,7 @@ Please see the `Standard Interchange Language Specifications
 
<http://productcatalog.gs1us.org/Store/tabid/86/CategoryID/21/List/1/catpageindex/2/Level/a/ProductID/46/Default.aspx>`_
 
for more information.
 
"""
 

	
 
from rattail.sil.columns import *
 
from rattail.sil.batches import *
 
from rattail.sil.sqlalchemy import *
 
from rattail.sil.writer import *
rattail/sil/sqlalchemy.py
Show inline comments
 
deleted file
tests/db/__init__.py
Show inline comments
 
@@ -19,11 +19,12 @@ activate_extension('rattail', engine)
 

	
 
class DataTestCase(unittest.TestCase):
 

	
 
    def setUp(self):
 
        self.session = Session()
 
        for table in reversed(Base.metadata.sorted_tables):
 
            self.session.execute(table.delete())
 
            if not table.name.startswith('batch.'):
 
                self.session.execute(table.delete())
 
        self.session.commit()
 

	
 
    def tearDown(self):
 
        self.session.close()
tests/db/test_changes.py
Show inline comments
 
new file 100644
 

	
 
from unittest import TestCase
 
from mock import patch, DEFAULT, Mock, MagicMock, call
 

	
 
from rattail.db import changes
 
from rattail.db.model import Change, Batch, BatchColumn, BatchRow, Role, UserRole, Product
 
from sqlalchemy.orm import RelationshipProperty
 

	
 

	
 
class TestChanges(TestCase):
 

	
 
    @patch.multiple('rattail.db.changes', listen=DEFAULT, ChangeRecorder=DEFAULT)
 
    def test_record_changes(self, listen, ChangeRecorder):
 
        session = Mock()
 
        ChangeRecorder.return_value = 'whatever'
 

	
 
        changes.record_changes(session)
 
        ChangeRecorder.assert_called_once_with(True)
 
        listen.assert_called_once_with(session, 'before_flush', 'whatever')
 

	
 
        ChangeRecorder.reset_mock()
 
        listen.reset_mock()
 
        changes.record_changes(session, ignore_role_changes=False)
 
        ChangeRecorder.assert_called_once_with(False)
 
        listen.assert_called_once_with(session, 'before_flush', 'whatever')
 

	
 

	
 
class TestChangeRecorder(TestCase):
 

	
 
    def test_init(self):
 
        recorder = changes.ChangeRecorder()
 
        self.assertTrue(recorder.ignore_role_changes)
 
        recorder = changes.ChangeRecorder(False)
 
        self.assertFalse(recorder.ignore_role_changes)
 

	
 
    def test_call(self):
 
        recorder = changes.ChangeRecorder()
 
        recorder.record_change = Mock()
 

	
 
        session = MagicMock()
 
        session.deleted.__iter__.return_value = ['deleted_instance']
 
        session.new.__iter__.return_value = ['new_instance']
 
        session.dirty.__iter__.return_value = ['dirty_instance']
 
        session.is_modified.return_value = True
 

	
 
        recorder(session, Mock(), Mock())
 
        self.assertEqual(recorder.record_change.call_count, 3)
 
        recorder.record_change.assert_has_calls([
 
                call(session, 'deleted_instance', deleted=True),
 
                call(session, 'new_instance'),
 
                call(session, 'dirty_instance'),
 
                ])
 

	
 
    def test_record_change(self):
 
        session = Mock()
 
        recorder = changes.ChangeRecorder()
 
        recorder.ensure_uuid = Mock()
 

	
 
        # don't record changes for changes
 
        self.assertFalse(recorder.record_change(session, Change()))
 

	
 
        # don't record changes for batch data
 
        self.assertFalse(recorder.record_change(session, Batch()))
 
        self.assertFalse(recorder.record_change(session, BatchColumn()))
 
        self.assertFalse(recorder.record_change(session, BatchRow()))
 

	
 
        # don't record changes for objects with no uuid attribute
 
        self.assertFalse(recorder.record_change(session, object()))
 

	
 
        # don't record changes for role data if so configured
 
        recorder.ignore_role_changes = True
 
        self.assertFalse(recorder.record_change(session, Role()))
 
        self.assertFalse(recorder.record_change(session, UserRole()))
 

	
 
        # none of the above should have involved a call to `ensure_uuid()`
 
        self.assertFalse(recorder.ensure_uuid.called)
 

	
 
        # make sure role data is *not* ignored if so configured
 
        recorder.ignore_role_changes = False
 
        self.assertTrue(recorder.record_change(session, Role()))
 
        self.assertTrue(recorder.record_change(session, UserRole()))
 

	
 
        # so far no *new* changes have been created
 
        self.assertFalse(session.add.called)
 

	
 
        # mock up session to force new change creation
 
        session.query.return_value = session
 
        session.get.return_value = None
 
        with patch('rattail.db.changes.Change') as MockChange:
 
            new_change = Mock()
 
            MockChange.return_value = new_change
 
            self.assertTrue(recorder.record_change(session, Product()))
 
            session.add.assert_called_once_with(new_change)
 

	
 
    @patch.multiple('rattail.db.changes', get_uuid=DEFAULT, object_mapper=DEFAULT)
 
    def test_ensure_uuid(self, get_uuid, object_mapper):
 
        recorder = changes.ChangeRecorder()
 
        uuid_column = Mock()
 
        object_mapper.return_value.columns.__getitem__.return_value = uuid_column
 

	
 
        # uuid already present
 
        product = Product(uuid='some_uuid')
 
        recorder.ensure_uuid(product)
 
        self.assertEqual(product.uuid, 'some_uuid')
 
        self.assertFalse(get_uuid.called)
 

	
 
        # no uuid yet, auto-generate
 
        uuid_column.foreign_keys = False
 
        get_uuid.return_value = 'another_uuid'
 
        product = Product()
 
        self.assertIsNone(product.uuid)
 
        recorder.ensure_uuid(product)
 
        get_uuid.assert_called_once_with()
 
        self.assertEqual(product.uuid, 'another_uuid')
 

	
 
        # some heavy mocking for following tests
 
        uuid_column.foreign_keys = True
 
        remote_side = MagicMock(key='uuid')
 
        prop = MagicMock(__class__=RelationshipProperty, key='foreign_thing')
 
        prop.remote_side.__len__.return_value = 1
 
        prop.remote_side.__iter__.return_value = [remote_side]
 
        object_mapper.return_value.iterate_properties.__iter__.return_value = [prop]
 
        
 
        # uuid fetched from existing foreign key object
 
        get_uuid.reset_mock()
 
        instance = Mock(uuid=None, foreign_thing=Mock(uuid='secondary_uuid'))
 
        recorder.ensure_uuid(instance)
 
        self.assertFalse(get_uuid.called)
 
        self.assertEqual(instance.uuid, 'secondary_uuid')
 

	
 
        # foreign key object doesn't exist; uuid generated as fallback
 
        get_uuid.return_value = 'fallback_uuid'
 
        instance = Mock(uuid=None, foreign_thing=None)
 
        recorder.ensure_uuid(instance)
 
        get_uuid.assert_called_once_with()
 
        self.assertEqual(instance.uuid, 'fallback_uuid')
 

	
 
    #     # uuid fetched from foreign key object
 
    #     product = Product(uuid='latest_uuid')
 
    #     product_ext = ProductExtTest(product=product)
 
    #     self.assertIsNone(product_ext.uuid)
 
    #     get_uuid.reset_mock()
 
    #     changes.ensure_uuid(product_ext)
 
    #     self.assertFalse(get_uuid.called)
 
    #     self.assertEqual(product_ext.uuid, 'latest_uuid')
 

	
 
    # @patch('rattail.db.changes.get_uuid')
 
    # def test_ensure_uuid(self, get_uuid):
 

	
 
    #     # fallback scenario.  "extended product" is expected to be associated
 
    #     # with a product, but if it isn't, a UUID must still be generated
 
    #     product_ext = ProductExtTest()
 
    #     self.assertIsNone(product_ext.uuid)
 
    #     changes.ensure_uuid(product_ext)
 
    #     get_uuid.assert_called_once_with()
 
    #     self.assertEqual(product_ext.uuid, 'some_uuid')
 

	
 

	
 
# class ProductExtTest(Base):
 
#     __tablename__ = 'products_extended_test'
 
#     uuid = uuid_column(ForeignKey('products.uuid'), default=None)
 
#     some_attr = Column(String(length=10))
 
#     product = relationship(Product)
 

	
 

	
 
# class TestChanges(DataTestCase):
 

	
 
#     def setUp(self):
 
#         Base.metadata.tables['products_extended_test'].create(bind=engine)
 
#         super(TestChanges, self).setUp()
 

	
 
#     def tearDown(self):
 
#         super(TestChanges, self).tearDown()
 
#         Base.metadata.tables['products_extended_test'].drop(bind=engine)
 

	
 
#     @patch('rattail.db.changes.listen')
 
#     def test_record_changes(self, listen):
 
#         session = Mock()
 

	
 
#         changes.record_changes(session)
 

	
 
#         # # changes not being recorded yet
 
#         # self.session.add(Product())
 
#         # self.session.commit()
 
#         # self.assertEqual(self.session.query(Change).count(), 0)
 

	
 
#         # # okay, record them and confirm
 
#         # changes.record_changes(self.session)
 
#         # self.session.add(Product())
 
#         # self.session.commit()
 
#         # self.assertEqual(self.session.query(Change).count(), 1)
 

	
 
#     @patch('rattail.db.changes.get_uuid')
 
#     def test_ensure_uuid(self, get_uuid):
 

	
 
#         # uuid already present
 
#         product = Product(uuid='whatever')
 
#         changes.ensure_uuid(product)
 
#         self.assertEqual(product.uuid, 'whatever')
 
#         self.assertFalse(get_uuid.called)
 

	
 
#         # no uuid yet, auto-generate
 
#         get_uuid.return_value = 'some_uuid'
 
#         product = Product()
 
#         self.assertIsNone(product.uuid)
 
#         changes.ensure_uuid(product)
 
#         get_uuid.assert_called_once_with()
 
#         self.assertEqual(product.uuid, 'some_uuid')
 

	
 
#         # uuid fetched from foreign key object
 
#         product = Product(uuid='latest_uuid')
 
#         product_ext = ProductExtTest(product=product)
 
#         self.assertIsNone(product_ext.uuid)
 
#         get_uuid.reset_mock()
 
#         changes.ensure_uuid(product_ext)
 
#         self.assertFalse(get_uuid.called)
 
#         self.assertEqual(product_ext.uuid, 'latest_uuid')
 

	
 
#         # fallback scenario.  "extended product" is expected to be associated
 
#         # with a product, but if it isn't, a UUID must still be generated
 
#         product_ext = ProductExtTest()
 
#         self.assertIsNone(product_ext.uuid)
 
#         changes.ensure_uuid(product_ext)
 
#         get_uuid.assert_called_once_with()
 
#         self.assertEqual(product_ext.uuid, 'some_uuid')
tests/db/test_core.py
Show inline comments
 
new file 100644
 

	
 
from unittest import TestCase
 

	
 
from rattail.db import core
 
from sqlalchemy import Column
 

	
 

	
 
class TestCore(TestCase):
 

	
 
    def test_uuid_column(self):
 
        column = core.uuid_column()
 
        self.assertIsInstance(column, Column)
 
        self.assertEqual(column.name, None)
 
        self.assertTrue(column.primary_key)
 
        self.assertFalse(column.nullable)
 
        self.assertIsNotNone(column.default)
 

	
 
    def test_uuid_column_no_default(self):
 
        column = core.uuid_column(default=None)
 
        self.assertIsNone(column.default)
 

	
 
    def test_uuid_column_nullable(self):
 
        column = core.uuid_column(nullable=True)
 
        self.assertTrue(column.nullable)
tests/db/test_model.py
Show inline comments
 

	
 
from unittest import TestCase
 
from . import DataTestCase
 
from mock import patch, DEFAULT, Mock, MagicMock
 

	
 
from rattail.db.extension import model
 
from sqlalchemy import String, Boolean, Numeric
 
from rattail.db.types import GPCType
 
from sqlalchemy.exc import IntegrityError
 

	
 

	
 
class TestBatch(TestCase):
 

	
 
    @patch('rattail.db.extension.model.object_session')
 
    def test_rowclass(self, object_session):
 
        object_session.return_value = object_session
 

	
 
        # no row classes to start with
 
        self.assertEqual(model.Batch._rowclasses, {})
 

	
 
        # basic, empty row class
 
        batch = model.Batch(uuid='some_uuid')
 
        batch.get_sqlalchemy_type = Mock(return_value='some_type')
 
        batch.columns = MagicMock()
 
        rowclass = batch.rowclass
 
        self.assertTrue(issubclass(rowclass, model.BatchRow))
 
        self.assertEqual(model.Batch._rowclasses.keys(), ['some_uuid'])
 
        self.assertIs(model.Batch._rowclasses['some_uuid'], rowclass)
 
        self.assertFalse(object_session.flush.called)
 

	
 
        # make sure rowclass.batch works
 
        object_session.query.return_value.get.return_value = batch
 
        self.assertIs(rowclass().batch, batch)
 
        object_session.query.return_value.get.assert_called_once_with('some_uuid')
 

	
 
        # row class with generated uuid and some columns
 
        batch = model.Batch(uuid=None)
 
        batch.columns = [model.BatchColumn(name='F01'), model.BatchColumn(name='F02')]
 
        model.Batch.get_sqlalchemy_type = Mock(return_value=String(length=20))
 
        def set_uuid():
 
            batch.uuid = 'fresh_uuid'
 
        object_session.flush.side_effect = set_uuid
 
        rowclass = batch.rowclass
 
        object_session.flush.assert_called_once_with()
 
        self.assertItemsEqual(model.Batch._rowclasses.keys(), ['some_uuid', 'fresh_uuid'])
 
        self.assertIs(model.Batch._rowclasses['fresh_uuid'], rowclass)
 

	
 
    def test_get_sqlalchemy_type(self):
 

	
 
        # gpc
 
        self.assertIsInstance(model.Batch.get_sqlalchemy_type('GPC(14)'), GPCType)
 

	
 
        # boolean
 
        self.assertIsInstance(model.Batch.get_sqlalchemy_type('FLAG(1)'), Boolean)
 

	
 
        # string
 
        type_ = model.Batch.get_sqlalchemy_type('CHAR(20)')
 
        self.assertIsInstance(type_, String)
 
        self.assertEqual(type_.length, 20)
 

	
 
        # numeric
 
        type_ = model.Batch.get_sqlalchemy_type('NUMBER(9,3)')
 
        self.assertIsInstance(type_, Numeric)
 
        self.assertEqual(type_.precision, 9)
 
        self.assertEqual(type_.scale, 3)
 

	
 
        # invalid
 
        self.assertRaises(AssertionError, model.Batch.get_sqlalchemy_type, 'CHAR(9,3)')
 
        self.assertRaises(AssertionError, model.Batch.get_sqlalchemy_type, 'OMGWTFBBQ')
 

	
 

	
 
class TestCustomer(DataTestCase):
 

	
 
    def test_repr(self):
 
        customer = model.Customer(uuid='whatever')
 
        self.assertEqual(repr(customer), "Customer(uuid='whatever')")
 

	
tests/test_core.py
Show inline comments
 
new file 100644
 

	
 
from unittest import TestCase
 

	
 
from rattail import core
 

	
 

	
 
class TestCore(TestCase):
 

	
 
    def test_get_uuid(self):
 
        uuid = core.get_uuid()
 
        self.assertIsInstance(uuid, str)
 
        self.assertEqual(len(uuid), 32)
0 comments (0 inline, 0 general)