Implemented conversations with full testing, oh and got some addressbook stuff done
authorEd Page <eopage@byu.net>
Fri, 16 Oct 2009 02:24:40 +0000 (21:24 -0500)
committerEd Page <eopage@byu.net>
Fri, 16 Oct 2009 02:24:40 +0000 (21:24 -0500)
src/gvoice/backend.py
src/gvoice/conversations.py [new file with mode: 0644]
tests/test_addressbook.py
tests/test_conversations.py [new file with mode: 0644]

index 2163705..41d66a8 100755 (executable)
@@ -595,6 +595,7 @@ def decorate_recent(recentCallData):
        """
        @returns (personsName, phoneNumber, date, action)
        """
+       contactId = recentCallData["contactId"]
        if recentCallData["name"]:
                header = recentCallData["name"]
        elif recentCallData["prettyNumber"]:
@@ -607,10 +608,11 @@ def decorate_recent(recentCallData):
        number = recentCallData["number"]
        relTime = recentCallData["relTime"]
        action = recentCallData["action"]
-       return header, number, relTime, action
+       return contactId, header, number, relTime, action
 
 
 def decorate_message(messageData):
+       contactId = messageData["contactId"]
        exactTime = messageData["time"]
        if messageData["name"]:
                header = messageData["name"]
@@ -632,7 +634,7 @@ def decorate_message(messageData):
                        for messagePart in messageParts
                ]
 
-       decoratedResults = header, number, relativeTime, messages
+       decoratedResults = contactId, header, number, relativeTime, messages
        return decoratedResults
 
 
diff --git a/src/gvoice/conversations.py b/src/gvoice/conversations.py
new file mode 100644 (file)
index 0000000..fad4b40
--- /dev/null
@@ -0,0 +1,110 @@
+#!/usr/bin/python
+
+
+import logging
+
+import util.coroutines as coroutines
+
+import backend
+
+
+_moduleLogger = logging.getLogger("gvoice.conversations")
+
+
+class Conversations(object):
+
+       def __init__(self, backend):
+               self._backend = backend
+               self._conversations = {}
+
+               self.updateSignalHandler = coroutines.CoTee()
+               self.update()
+
+       def update(self, force=False):
+               if not force and self._conversations:
+                       return
+
+               oldConversationIds = set(self._conversations.iterkeys())
+
+               updateConversationIds = set()
+               messages = self._backend.get_messages()
+               sortedMessages = backend.sort_messages(messages)
+               for messageData in sortedMessages:
+                       key = messageData["contactId"], messageData["number"]
+                       try:
+                               conversation = self._conversations[key]
+                               isNewConversation = False
+                       except KeyError:
+                               conversation = Conversation(self._backend, messageData)
+                               self._conversations[key] = conversation
+                               isNewConversation = True
+
+                       if isNewConversation:
+                               # @todo see if this has issues with a user marking a item as unread/unarchive?
+                               isConversationUpdated = True
+                       else:
+                               isConversationUpdated = conversation.merge_conversation(messageData)
+
+                       if isConversationUpdated:
+                               updateConversationIds.add(key)
+
+               if updateConversationIds:
+                       message = (self, updateConversationIds, )
+                       self.updateSignalHandler.stage.send(message)
+
+       def get_conversations(self):
+               return self._conversations.iterkeys()
+
+       def get_conversation(self, key):
+               return self._conversations[key]
+
+
+class Conversation(object):
+
+       def __init__(self, backend, data):
+               self._backend = backend
+               self._data = dict((key, value) for (key, value) in data.iteritems())
+
+               # confirm we have a list
+               self._data["messageParts"] = list(
+                       self._append_time(message, self._data["time"])
+                       for message in self._data["messageParts"]
+               )
+
+       def __getitem__(self, key):
+               return self._data[key]
+
+       def merge_conversation(self, moreData):
+               """
+               @returns True if there was content to merge (new messages arrived
+               rather than being a duplicate)
+
+               @warning This assumes merges are done in chronological order
+               """
+               for constantField in ("contactId", "number"):
+                       assert self._data[constantField] == moreData[constantField], "Constant field changed, soemthing is seriously messed up: %r v %r" % (self._data, moreData)
+
+               if moreData["time"] < self._data["time"]:
+                       # If its older, assuming it has nothing new to report
+                       return False
+
+               for preferredMoreField in ("id", "name", "time", "relTime", "prettyNumber", "location"):
+                       preferredFieldValue = moreData[preferredMoreField]
+                       if preferredFieldValue:
+                               self._data[preferredMoreField] = preferredFieldValue
+
+               messageAppended = False
+
+               messageParts = self._data["messageParts"]
+               for message in moreData["messageParts"]:
+                       messageWithTimestamp = self._append_time(message, moreData["time"])
+                       if messageWithTimestamp not in messageParts:
+                               messageParts.append(messageWithTimestamp)
+                               messageAppended = True
+
+               return messageAppended
+
+       @staticmethod
+       def _append_time(message, exactWhen):
+               whoFrom, message, when = message
+               return whoFrom, message, when, exactWhen
index aad0eee..00ca028 100644 (file)
@@ -71,6 +71,12 @@ def test_one_contact_no_details():
        book.updateSignalHandler.register_sink(callback)
        assert len(callbackData) == 0, "%r" % callbackData
 
+       contacts = list(book.get_contacts())
+       assert len(contacts) == 1
+       id = contacts[0]
+       name = book.get_contact_name(id)
+       assert name == backend.contactsData[id]["name"]
+
        book.update()
        assert len(callbackData) == 0, "%r" % callbackData
 
@@ -101,6 +107,12 @@ def test_one_contact_with_details():
        book.updateSignalHandler.register_sink(callback)
        assert len(callbackData) == 0, "%r" % callbackData
 
+       contacts = list(book.get_contacts())
+       assert len(contacts) == 1
+       id = contacts[0]
+       name = book.get_contact_name(id)
+       assert name == backend.contactsData[id]["name"]
+
        book.update()
        assert len(callbackData) == 0, "%r" % callbackData
 
diff --git a/tests/test_conversations.py b/tests/test_conversations.py
new file mode 100644 (file)
index 0000000..3444431
--- /dev/null
@@ -0,0 +1,209 @@
+from __future__ import with_statement
+
+import datetime
+import logging
+
+import sys
+sys.path.append("../src")
+
+import util.coroutines as coroutines
+
+import gvoice
+
+
+logging.basicConfig(level=logging.DEBUG)
+
+
+class MockBackend(object):
+
+       def __init__(self, conversationsData):
+               self.conversationsData = conversationsData
+
+       def get_messages(self):
+               return self.conversationsData
+
+
+def generate_update_callback(callbackData):
+
+       @coroutines.func_sink
+       @coroutines.expand_positional
+       def callback(conversations, updatedIds):
+               callbackData.append((conversations, updatedIds))
+
+       return callback
+
+
+def test_no_conversations():
+       callbackData = []
+       callback = generate_update_callback(callbackData)
+
+       backend = MockBackend([])
+       conversings = gvoice.conversations.Conversations(backend)
+       conversings.updateSignalHandler.register_sink(callback)
+       assert len(callbackData) == 0, "%r" % callbackData
+
+       conversings.update()
+       assert len(callbackData) == 0, "%r" % callbackData
+
+       conversings.update(force=True)
+       assert len(callbackData) == 0, "%r" % callbackData
+
+       contacts = list(conversings.get_conversations())
+       assert len(contacts) == 0
+
+
+def test_a_conversation():
+       callbackData = []
+       callback = generate_update_callback(callbackData)
+
+       backend = MockBackend([
+               {
+                       "id": "conv1",
+                       "contactId": "con1",
+                       "name": "Con Man",
+                       "time": datetime.datetime(2000, 1, 1),
+                       "relTime": "Sometime back",
+                       "prettyNumber": "(555) 555-1224",
+                       "number": "5555551224",
+                       "location": "",
+                       "messageParts": [
+                               ("Innocent Man", "Body of Message", "Forever ago")
+                       ],
+               },
+       ])
+       conversings = gvoice.conversations.Conversations(backend)
+       conversings.updateSignalHandler.register_sink(callback)
+       assert len(callbackData) == 0, "%r" % callbackData
+
+       cons = list(conversings.get_conversations())
+       assert len(cons) == 1
+       assert cons[0] == ("con1", "5555551224"), cons
+
+       conversings.update()
+       assert len(callbackData) == 0, "%r" % callbackData
+
+       conversings.update(force=True)
+       assert len(callbackData) == 0, "%r" % callbackData
+
+
+def test_adding_a_conversation():
+       callbackData = []
+       callback = generate_update_callback(callbackData)
+
+       backend = MockBackend([
+               {
+                       "id": "conv1",
+                       "contactId": "con1",
+                       "name": "Con Man",
+                       "time": datetime.datetime(2000, 1, 1),
+                       "relTime": "Sometime back",
+                       "prettyNumber": "(555) 555-1224",
+                       "number": "5555551224",
+                       "location": "",
+                       "messageParts": [
+                               ("Innocent Man", "Body of Message", "Forever ago")
+                       ],
+               },
+       ])
+       conversings = gvoice.conversations.Conversations(backend)
+       conversings.updateSignalHandler.register_sink(callback)
+       assert len(callbackData) == 0, "%r" % callbackData
+
+       cons = list(conversings.get_conversations())
+       assert len(cons) == 1
+       assert cons[0] == ("con1", "5555551224"), cons
+
+       conversings.update()
+       assert len(callbackData) == 0, "%r" % callbackData
+
+       conversings.update(force=True)
+       assert len(callbackData) == 0, "%r" % callbackData
+
+       backend.conversationsData.append(
+               {
+                       "id": "conv2",
+                       "contactId": "con2",
+                       "name": "Pretty Man",
+                       "time": datetime.datetime(2003, 1, 1),
+                       "relTime": "Somewhere over the rainbow",
+                       "prettyNumber": "(555) 555-2244",
+                       "number": "5555552244",
+                       "location": "",
+                       "messageParts": [
+                               ("Con Man", "Body of Message somewhere", "Maybe")
+                       ],
+               },
+       )
+
+       conversings.update()
+       assert len(callbackData) == 0, "%r" % callbackData
+
+       conversings.update(force=True)
+       assert len(callbackData) == 1, "%r" % callbackData
+       idsOnly = callbackData[0][1]
+       assert ("con2", "5555552244") in idsOnly, idsOnly
+
+       cons = list(conversings.get_conversations())
+       assert len(cons) == 2
+       assert ("con1", "5555551224") in cons, cons
+       assert ("con2", "5555552244") in cons, cons
+
+
+def test_merging_a_conversation():
+       callbackData = []
+       callback = generate_update_callback(callbackData)
+
+       backend = MockBackend([
+               {
+                       "id": "conv1",
+                       "contactId": "con1",
+                       "name": "Con Man",
+                       "time": datetime.datetime(2000, 1, 1),
+                       "relTime": "Sometime back",
+                       "prettyNumber": "(555) 555-1224",
+                       "number": "5555551224",
+                       "location": "",
+                       "messageParts": [
+                               ("Innocent Man", "Body of Message", "Forever ago")
+                       ],
+               },
+       ])
+       conversings = gvoice.conversations.Conversations(backend)
+       conversings.updateSignalHandler.register_sink(callback)
+       assert len(callbackData) == 0, "%r" % callbackData
+
+       cons = list(conversings.get_conversations())
+       assert len(cons) == 1
+       assert cons[0] == ("con1", "5555551224"), cons
+
+       conversings.update()
+       assert len(callbackData) == 0, "%r" % callbackData
+
+       conversings.update(force=True)
+       assert len(callbackData) == 0, "%r" % callbackData
+
+       backend.conversationsData.append(
+               {
+                       "id": "conv1",
+                       "contactId": "con1",
+                       "name": "Con Man",
+                       "time": datetime.datetime(2003, 1, 1),
+                       "relTime": "Sometime back",
+                       "prettyNumber": "(555) 555-1224",
+                       "number": "5555551224",
+                       "location": "",
+                       "messageParts": [
+                               ("Innocent Man", "Mwahahaah", "somewhat closer")
+                       ],
+               },
+       )
+
+       conversings.update()
+       assert len(callbackData) == 0, "%r" % callbackData
+
+       conversings.update(force=True)
+       assert len(callbackData) == 1, "%r" % callbackData
+       idsOnly = callbackData[0][1]
+       assert ("con1", "5555551224") in idsOnly, idsOnly
+       convseration = conversings.get_conversation(idsOnly.pop())
+       assert len(convseration["messageParts"]) == 2, convseration["messageParts"]