===========
MongoObject
===========

A MongoObject can get stored independent from anything else in a MongoDB. Such
MongoObject can get used together with a field property called
MongoOjectProperty. The field property is responsible for set and get such
MongoObject to and from MongoDB. A persistent item which provides such a 
MongoObject within a MongoObjectProperty only has to provide an oid attribute
with a unique value. You can use the m01.oid package for such a unique oid
or implement an own pattern.

The MongoObject uses the __parent__._moid and the attribute (field) name as
it's unique MongoDB key.  

Note, this test uses a fake MongoDB server setup. But this fake server is far
away from beeing complete. We will add more feature to this fake server if we
need them in other projects. See testing.py for more information.


Condition
---------

Befor we start testing, check if our thread local cache is empty or if we have
let over some junk from previous tests:

  >>> from m01.mongo.testing import pprint
  >>> from m01.mongo import LOCAL
  >>> pprint(LOCAL.__dict__)
  {}


Setup
-----

First import some components:

  >>> import datetime
  >>> import transaction
  >>> from ZODB.DB import DB
  >>> from ZODB.DemoStorage import DemoStorage
  >>> from m01.mongo import interfaces
  >>> from m01.mongo import pool
  >>> from m01.mongo import testing

And set up a zope database:

  >>> db = DB(DemoStorage())

First, we need to setup a persistent object:

  >>> content = testing.Content(42)
  >>> content._moid
  42

And add them to the ZODB:

  >>> conn = db.open()
  >>> conn.root()['content'] = content
  >>> transaction.commit()
  >>> conn.close()

And check our content with a new zope database connection:

  >>> conn = db.open()
  >>> content = conn.root()['content']
  >>> content
  <Content 42>


MongoObject
-----------

Now let's add a MongoObject instance to our sample content object:

  >>> data = {'title': u'Mongo Object Title',
  ...         'description': u'A Description',
  ...         'item': {'text':u'Item'},
  ...         'date': datetime.date(2010, 2, 28).toordinal(),
  ...         'numbers': [1,2,3],
  ...         'comments': [{'text':u'Comment 1'}, {'text':u'Comment 2'}]}
  >>> obj = testing.SampleMongoObject(data)
  >>> obj._id
  ObjectId('...')

  obj.title
  u'Mongo Object Title'

  >>> obj.description
  u'A Description'

  >>> obj.item
  <SampleSubItem u'...'>

  >>> obj.item.text
  u'Item'

  >>> obj.numbers
  [1, 2, 3]

  >>> obj.comments
  [<SampleSubItem u'...'>, <SampleSubItem u'...'>]

  >>> tuple(obj.comments)[0].text
  u'Comment 1'

  >>> tuple(obj.comments)[1].text
  u'Comment 2'

Our MongoObject doesn't provide a _aprent__ or __name__ right now:

  >>> obj.__parent__ is None
  True

  >>> obj.__name__ is None
  True

But after adding the mongo object to our content which uses a
MongoObjectProperty, the mongo object get located and becomes the attribute
name as _field value. If the object didn't provide a __name__, the same value
will also get applied for __name__:

  >>> content.obj = obj
  >>> obj.__parent__
  <Content 42>

  >>> obj.__name__
  u'obj'

  >>> obj.__name__
  u'obj'

After adding our mongo object, there should be a reference in our thread local
cache:

  >>> pprint(LOCAL.__dict__)
  {u'42:obj': <SampleMongoObject u'obj'>,
   'MongoTransactionDataManager': <m01.mongo.tm.MongoTransactionDataManager object at ...>}

A MongoObject provides a _oid attribute which is used as the MongoDB key. This
value uses the __parent__._moid and the mongo objects attribute name:

  >>> obj._oid == '%s:%s' % (content._moid, obj.__name__)
  True

  >>> obj._oid
  u'42:obj'

Now check if we can get the mongo object again and if we still get the same
values:

  >>> obj = content.obj
  >>> obj.title
  u'Mongo Object Title'

  >>> obj.description
  u'A Description'

  >>> obj.item
  <SampleSubItem u'...'>

  >>> obj.item.text
  u'Item'

  >>> obj.numbers
  [1, 2, 3]

  >>> obj.comments
  [<SampleSubItem u'...'>, <SampleSubItem u'...'>]

  >>> tuple(obj.comments)[0].text
  u'Comment 1'

  >>> tuple(obj.comments)[1].text
  u'Comment 2'

Now let's commit the transaction which will store the obj in our fake mongo DB:

  >>> transaction.commit()
  >>> conn.close()

After we commited to the MongoDB, the mongo object and our transaction data
manger reference should be gone in the thread local cache:

  >>> pprint(LOCAL.__dict__)
  {}

Now lets get a new ZODB connection and check our mongo object values again:

  >>> conn = db.open()
  >>> content = conn.root()['content']
  >>> content
  <Content 42>
  
  >>> obj = content.obj
  >>> obj
  <SampleMongoObject u'obj'>
  
  >>> obj.title
  u'Mongo Object Title'

  >>> obj.description
  u'A Description'

  >>> obj.item
  <SampleSubItem u'...'>

  >>> obj.item.text
  u'Item'

  >>> obj.numbers
  [1, 2, 3]

  >>> obj.comments
  [<SampleSubItem u'...'>, <SampleSubItem u'...'>]

  >>> tuple(obj.comments)[0].text
  u'Comment 1'

  >>> tuple(obj.comments)[1].text
  u'Comment 2'
  
  >>> pprint(obj.dump())
  {'__name__': u'obj',
   '_field': u'obj',
   '_id': ObjectId('...'),
   '_oid': u'42:obj',
   '_type': u'SampleMongoObject',
   '_version': 1,
   'comments': [{'_id': ObjectId('...'),
                 '_type': u'SampleSubItem',
                 'created': datetime.datetime(...),
                 'text': u'Comment 1'},
                {'_id': ObjectId('...'),
                 '_type': u'SampleSubItem',
                 'created': datetime.datetime(...),
                 'text': u'Comment 2'}],
   'created': datetime.datetime(...),
   'date': 733831,
   'description': u'A Description',
   'item': {'_id': ObjectId('...'),
            '_type': u'SampleSubItem',
            'created': datetime.datetime(...),
            'text': u'Item'},
   'modified': datetime.datetime(...),
   'numbers': [1, 2, 3],
   'removed': False,
   'title': u'Mongo Object Title'}

  >>> transaction.commit()
  >>> conn.close()

  >>> pprint(LOCAL.__dict__)
  {}

Now let's replace the existing item with a new one and add another item to
the item lists. Also make sure we can use append instead of re-apply the full
list like zope widgets do:

  >>> conn = db.open()
  >>> content = conn.root()['content']
  >>> obj = content.obj

  >>> obj.item = testing.SampleSubItem({'text': u'New Item'})

  >>> newItem = testing.SampleSubItem({'text': u'New List Item'})
  >>> obj.comments.append(newItem)

  >>> obj.numbers.append(4)

  >>> transaction.commit()
  >>> conn.close()

check again:

  >>> conn = db.open()
  >>> content = conn.root()['content']
  >>> obj = content.obj
  
  >>> obj.title
  u'Mongo Object Title'

  >>> obj.description
  u'A Description'

  >>> obj.item
  <SampleSubItem u'...'>

  >>> obj.item.text
  u'New Item'

  >>> obj.numbers
  [1, 2, 3, 4]

  >>> obj.comments
  [<SampleSubItem u'...'>, <SampleSubItem u'...'>]

  >>> tuple(obj.comments)[0].text
  u'Comment 1'

  >>> tuple(obj.comments)[1].text
  u'Comment 2'

And now re-apply a full list of values to the list field:

  >>> comOne = testing.SampleSubItem({'text': u'First List Item'})
  >>> comTwo = testing.SampleSubItem({'text': u'Second List Item'})
  >>> comments = [comOne, comTwo]
  >>> obj.comments = comments
  >>> obj.numbers = [1,2,3,4,5]
  >>> transaction.commit()
  >>> conn.close()

check again:

  >>> conn = db.open()
  >>> content = conn.root()['content']
  >>> obj = content.obj

  >>> len(obj.comments)
  2

  >>> obj.comments
  [<SampleSubItem u'...'>, <SampleSubItem u'...'>]

  >>> len(obj.numbers)
  5

  >>> obj.numbers
  [1, 2, 3, 4, 5]

Also check if we can remove list items:

  >>> obj.numbers.remove(1)
  >>> obj.numbers.remove(2)

  >>> obj.comments.remove(comTwo)

  >>> transaction.commit()
  >>> conn.close()

check again:

  >>> conn = db.open()
  >>> content = conn.root()['content']
  >>> obj = content.obj

  >>> len(obj.comments)
  1

  >>> obj.comments
  [<SampleSubItem u'...'>]

  >>> len(obj.numbers)
  3

  >>> obj.numbers
  [3, 4, 5]

  >>> transaction.commit()
  >>> conn.close()

We can also remove items from the item list by it's __name__:

  >>> conn = db.open()
  >>> content = conn.root()['content']
  >>> obj = content.obj

  >>> del obj.comments[comOne.__name__]

  >>> transaction.commit()
  >>> conn.close()

check again:

  >>> conn = db.open()
  >>> content = conn.root()['content']
  >>> obj = content.obj

  >>> len(obj.comments)
  0

  >>> obj.comments
  []

  >>> transaction.commit()
  >>> conn.close()

Or we can add items to the item list by name:

  >>> conn = db.open()
  >>> content = conn.root()['content']
  >>> obj = content.obj

  >>> obj.comments[comOne.__name__] = comOne

  >>> transaction.commit()
  >>> conn.close()

check again:

  >>> conn = db.open()
  >>> content = conn.root()['content']
  >>> obj = content.obj

  >>> len(obj.comments)
  1

  >>> obj.comments
  [<SampleSubItem u'...'>]

  >>> transaction.commit()
  >>> conn.close()


Coverage
--------

Our items list also provides the following methods:

  >>> obj.comments.__contains__(comOne.__name__)
  True

  >>> comOne.__name__ in obj.comments
  True

  >>> obj.comments.get(comOne.__name__)
  <SampleSubItem u'...'>

  >>> obj.comments.keys() == [comOne.__name__]
  True

  >>> obj.comments.values()
  <generator object ...>

  >>> tuple(obj.comments.values())
  (<SampleSubItem u'...'>,)

  >>> obj.comments.items()
  <generator object ...>

  >>> tuple(obj.comments.items())
  ((u'...', <SampleSubItem u'...'>),)

  >>> obj.comments == obj.comments
  True

Let's test some internals for increase coverage:

  >>> obj.comments._m_changed
  Traceback (most recent call last):
  ...
  AttributeError: _m_changed is a write only property

  >>> obj.comments._m_changed = False
  Traceback (most recent call last):
  ...
  ValueError: Can only dispatch True to __parent__

  >>> obj.comments.locate(42)

Our simple value typ list also provides the following methods:

  >>> obj.numbers.__contains__(3)
  True

  >>> 3 in obj.numbers
  True

  >>> obj.numbers == obj.numbers
  True

  >>> obj.numbers.pop()
  5

  >>> del obj.numbers[0]

  >>> obj.numbers[0] = 42

  >>> obj.numbers._m_changed
  Traceback (most recent call last):
  ...
  AttributeError: _m_changed is a write only property

  >>> obj.numbers._m_changed = False
  Traceback (most recent call last):
  ...
  ValueError: Can only dispatch True to __parent__

Check our thread local cache before we leave this test:

  >>> pprint(LOCAL.__dict__)
  {}
