From: Ed Page Date: Fri, 16 Oct 2009 02:24:40 +0000 (-0500) Subject: Implemented conversations with full testing, oh and got some addressbook stuff done X-Git-Url: http://git.maemo.org/git/?p=theonering;a=commitdiff_plain;h=fc96b200d858e66bbf02037b5002a07cd71c2654 Implemented conversations with full testing, oh and got some addressbook stuff done --- diff --git a/src/gvoice/backend.py b/src/gvoice/backend.py index 2163705..41d66a8 100755 --- a/src/gvoice/backend.py +++ b/src/gvoice/backend.py @@ -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 index 0000000..fad4b40 --- /dev/null +++ b/src/gvoice/conversations.py @@ -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 diff --git a/tests/test_addressbook.py b/tests/test_addressbook.py index aad0eee..00ca028 100644 --- a/tests/test_addressbook.py +++ b/tests/test_addressbook.py @@ -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 index 0000000..3444431 --- /dev/null +++ b/tests/test_conversations.py @@ -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"]