summaryrefslogtreecommitdiff
path: root/service
diff options
context:
space:
mode:
authorDuda Dornelles <ddornell@thoughtworks.com>2014-11-20 13:43:12 -0200
committerDuda Dornelles <ddornell@thoughtworks.com>2014-11-24 08:21:59 -0200
commit4f484e4dd2a40c4b3c71cd3d241785fb3a7b2eaf (patch)
tree09d494d82ac812e87e45c1b1ccc6ff2693b49a62 /service
parentd4b29e22f51c986e4b8306f1782ef3603248d0d5 (diff)
Adding contacts controller and basic contact search
Diffstat (limited to 'service')
-rw-r--r--service/pixelated/adapter/search.py34
-rw-r--r--service/pixelated/config/app_factory.py32
-rw-r--r--service/pixelated/config/routes.py26
-rw-r--r--service/pixelated/controllers/__init__.py1
-rw-r--r--service/pixelated/controllers/contacts_controller.py31
-rw-r--r--service/pixelated/controllers/features_controller.py2
-rw-r--r--service/test/integration/contacts_test.py22
-rw-r--r--service/test/support/integration/app_test_client.py8
-rw-r--r--service/test/support/integration/soledad_test_base.py4
9 files changed, 121 insertions, 39 deletions
diff --git a/service/pixelated/adapter/search.py b/service/pixelated/adapter/search.py
index 6c62ccc3..fd4e7c36 100644
--- a/service/pixelated/adapter/search.py
+++ b/service/pixelated/adapter/search.py
@@ -13,10 +13,12 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
from pixelated.support.encrypted_file_storage import EncryptedFileStorage
import os
from pixelated.adapter.status import Status
+from pixelated.support.functional import flatten
from whoosh.index import FileIndex
from whoosh.fields import *
from whoosh.qparser import QueryParser
@@ -44,7 +46,8 @@ class SearchEngine(object):
continue
if not tags.get(tag):
- tags[tag] = {'ident': tag, 'name': tag, 'default': False, 'counts': {'total': 0, 'read': 0}, 'mails': []}
+ tags[tag] = {'ident': tag, 'name': tag, 'default': False, 'counts': {'total': 0, 'read': 0},
+ 'mails': []}
tags[tag]['counts'][count_type] += count
def _search_tag_groups(self, is_filtering_tags):
@@ -91,9 +94,9 @@ class SearchEngine(object):
return Schema(
ident=ID(stored=True, unique=True),
sender=ID(stored=False),
- to=ID(stored=False),
- cc=ID(stored=False),
- bcc=ID(stored=False),
+ to=KEYWORD(stored=False, commas=True),
+ cc=KEYWORD(stored=False, commas=True),
+ bcc=KEYWORD(stored=False, commas=True),
subject=TEXT(stored=False),
date=NUMERIC(stored=False, sortable=True, bits=64, signed=False),
body=TEXT(stored=False),
@@ -119,9 +122,9 @@ class SearchEngine(object):
'sender': unicode(header.get('from', '')),
'subject': unicode(header.get('subject', '')),
'date': milliseconds(header.get('date', '')),
- 'to': unicode(header.get('to', '')),
- 'cc': unicode(header.get('cc', '')),
- 'bcc': unicode(header.get('bcc', '')),
+ 'to': u','.join(header.get('to', [''])),
+ 'cc': u','.join(header.get('cc', [''])),
+ 'bcc': u','.join(header.get('bcc', [''])),
'tag': u','.join(unique(tags)),
'body': unicode(mdict['body']),
'ident': unicode(mdict['ident']),
@@ -179,3 +182,20 @@ class SearchEngine(object):
writer.delete_by_term('ident', mail_id)
finally:
writer.commit()
+
+ def contacts(self, query):
+ if query:
+ to = QueryParser('to', self._index.schema)
+ cc = QueryParser('cc', self._index.schema)
+ bcc = QueryParser('bcc', self._index.schema)
+ with self._index.searcher() as searcher:
+ to = searcher.search(to.parse("*%s*" % query), limit=None,
+ groupedby=sorting.FieldFacet('to', allow_overlap=True)).groups()
+ cc = searcher.search(cc.parse("*%s*" % query), limit=None,
+ groupedby=sorting.FieldFacet('cc', allow_overlap=True)).groups()
+ bcc = searcher.search(bcc.parse("*%s*" % query), limit=None,
+ groupedby=sorting.FieldFacet('bcc', allow_overlap=True)).groups()
+ return flatten([to, cc, bcc])
+
+ return []
+
diff --git a/service/pixelated/config/app_factory.py b/service/pixelated/config/app_factory.py
index 497ab205..eb31a271 100644
--- a/service/pixelated/config/app_factory.py
+++ b/service/pixelated/config/app_factory.py
@@ -16,6 +16,7 @@
import sys
from twisted.internet import reactor
+from pixelated.config.routes import setup_routes
from pixelated.adapter.mail_service import MailService
from pixelated.adapter.mail import InputMail
from pixelated.adapter.mail_sender import MailSender
@@ -57,32 +58,6 @@ def update_info_sync_and_index_partial(sync_info_controller, search_engine, mail
return wrapper
-def _setup_routes(app, home_controller, mails_controller, tags_controller, features_controller, sync_info_controller,
- attachments_controller):
- # mails
- app.route('/mails', methods=['GET'])(mails_controller.mails)
- app.route('/mail/<mail_id>/read', methods=['POST'])(mails_controller.mark_mail_as_read)
- app.route('/mail/<mail_id>/unread', methods=['POST'])(mails_controller.mark_mail_as_unread)
- app.route('/mails/unread', methods=['POST'])(mails_controller.mark_many_mail_unread)
- app.route('/mails/read', methods=['POST'])(mails_controller.mark_many_mail_read)
- app.route('/mail/<mail_id>', methods=['GET'])(mails_controller.mail)
- app.route('/mail/<mail_id>', methods=['DELETE'])(mails_controller.delete_mail)
- app.route('/mails', methods=['DELETE'])(mails_controller.delete_mails)
- app.route('/mails', methods=['POST'])(mails_controller.send_mail)
- app.route('/mail/<mail_id>/tags', methods=['POST'])(mails_controller.mail_tags)
- app.route('/mails', methods=['PUT'])(mails_controller.update_draft)
- # tags
- app.route('/tags', methods=['GET'])(tags_controller.tags)
- # features
- app.route('/features', methods=['GET'])(features_controller.features)
- # sync info
- app.route('/sync_info', methods=['GET'])(sync_info_controller.sync_info)
- # attachments
- app.route('/attachment/<attachment_id>', methods=['GET'])(attachments_controller.attachment)
- # static
- app.route('/', methods=['GET'], branch=True)(home_controller.home)
-
-
def init_leap_session(app):
try:
leap_session = LeapSession.open(app.config['LEAP_USERNAME'],
@@ -126,6 +101,7 @@ def init_app(app):
draft_service=draft_service,
search_engine=search_engine)
tags_controller = TagsController(search_engine=search_engine)
+ contacts_controller = ContactsController(search_engine=search_engine)
sync_info_controller = SyncInfoController()
attachments_controller = AttachmentsController(soledad_querier)
@@ -141,8 +117,8 @@ def init_app(app):
register(signal=proto.SOLEDAD_DONE_DATA_SYNC, uid=CREATE_KEYS_IF_KEYS_DONT_EXISTS_CALLBACK,
callback=look_for_user_key_and_create_if_cant_find(leap_session))
- _setup_routes(app, home_controller, mails_controller, tags_controller, features_controller,
- sync_info_controller, attachments_controller)
+ setup_routes(app, home_controller, mails_controller, tags_controller, features_controller,
+ sync_info_controller, attachments_controller, contacts_controller)
def create_app(app, bind_address, bind_port):
diff --git a/service/pixelated/config/routes.py b/service/pixelated/config/routes.py
new file mode 100644
index 00000000..6bad8a17
--- /dev/null
+++ b/service/pixelated/config/routes.py
@@ -0,0 +1,26 @@
+def setup_routes(app, home_controller, mails_controller, tags_controller, features_controller, sync_info_controller,
+ attachments_controller, contacts_controller):
+ # mails
+ app.route('/mails', methods=['GET'])(mails_controller.mails)
+ app.route('/mail/<mail_id>/read', methods=['POST'])(mails_controller.mark_mail_as_read)
+ app.route('/mail/<mail_id>/unread', methods=['POST'])(mails_controller.mark_mail_as_unread)
+ app.route('/mails/unread', methods=['POST'])(mails_controller.mark_many_mail_unread)
+ app.route('/mails/read', methods=['POST'])(mails_controller.mark_many_mail_read)
+ app.route('/mail/<mail_id>', methods=['GET'])(mails_controller.mail)
+ app.route('/mail/<mail_id>', methods=['DELETE'])(mails_controller.delete_mail)
+ app.route('/mails', methods=['DELETE'])(mails_controller.delete_mails)
+ app.route('/mails', methods=['POST'])(mails_controller.send_mail)
+ app.route('/mail/<mail_id>/tags', methods=['POST'])(mails_controller.mail_tags)
+ app.route('/mails', methods=['PUT'])(mails_controller.update_draft)
+ # tags
+ app.route('/tags', methods=['GET'])(tags_controller.tags)
+ # contacts
+ app.route('/contacts', methods=['GET'])(contacts_controller.contacts)
+ # features
+ app.route('/features', methods=['GET'])(features_controller.features)
+ # sync info
+ app.route('/sync_info', methods=['GET'])(sync_info_controller.sync_info)
+ # attachments
+ app.route('/attachment/<attachment_id>', methods=['GET'])(attachments_controller.attachment)
+ # static
+ app.route('/', methods=['GET'], branch=True)(home_controller.home) \ No newline at end of file
diff --git a/service/pixelated/controllers/__init__.py b/service/pixelated/controllers/__init__.py
index e1c13515..33da02b8 100644
--- a/service/pixelated/controllers/__init__.py
+++ b/service/pixelated/controllers/__init__.py
@@ -37,3 +37,4 @@ from tags_controller import TagsController
from features_controller import FeaturesController
from sync_info_controller import SyncInfoController
from attachments_controller import AttachmentsController
+from contacts_controller import ContactsController
diff --git a/service/pixelated/controllers/contacts_controller.py b/service/pixelated/controllers/contacts_controller.py
new file mode 100644
index 00000000..f9b0628b
--- /dev/null
+++ b/service/pixelated/controllers/contacts_controller.py
@@ -0,0 +1,31 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated 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.
+#
+# Pixelated 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 Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from pixelated.controllers import respond_json_deferred
+from twisted.internet.threads import deferToThread
+
+
+class ContactsController:
+
+ def __init__(self, search_engine):
+ self._search_engine = search_engine
+
+ def contacts(self, request):
+ query = request.args.get('q', [''])[0]
+ d = deferToThread(lambda: self._search_engine.contacts(query))
+ d.addCallback(lambda tags: respond_json_deferred(tags, request))
+
+ return d \ No newline at end of file
diff --git a/service/pixelated/controllers/features_controller.py b/service/pixelated/controllers/features_controller.py
index 4d375683..f3fa9dff 100644
--- a/service/pixelated/controllers/features_controller.py
+++ b/service/pixelated/controllers/features_controller.py
@@ -19,7 +19,7 @@ import os
class FeaturesController:
- DISABLED_FEATURES = ['draftReply', 'signatureStatus', 'encryptionStatus', 'contacts']
+ DISABLED_FEATURES = ['draftReply', 'signatureStatus', 'encryptionStatus']
def __init__(self):
pass
diff --git a/service/test/integration/contacts_test.py b/service/test/integration/contacts_test.py
new file mode 100644
index 00000000..b1db485f
--- /dev/null
+++ b/service/test/integration/contacts_test.py
@@ -0,0 +1,22 @@
+from nose.twistedtools import deferred
+from test.support.integration import SoledadTestBase, MailBuilder
+
+
+class SearchTest(SoledadTestBase):
+
+ def setUp(self):
+ SoledadTestBase.setUp(self)
+
+ @deferred(timeout=SoledadTestBase.DEFERRED_TIMEOUT)
+ def test_that_tags_returns_all_tags(self):
+ input_mail = MailBuilder().with_tags(['important']).build_input_mail()
+ self.client.add_mail_to_inbox(input_mail)
+
+ d = self.get_contacts(query='recipient')
+
+ def _assert(contacts):
+ self.assertTrue('recipient@to.com' in contacts)
+ self.assertTrue('recipient@cc.com' in contacts)
+ self.assertTrue('recipient@bcc.com' in contacts)
+ d.addCallback(_assert)
+ return d \ No newline at end of file
diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py
index dea59399..229d32d6 100644
--- a/service/test/support/integration/app_test_client.py
+++ b/service/test/support/integration/app_test_client.py
@@ -17,6 +17,7 @@ import json
import multiprocessing
import shutil
import time
+from pixelated.config.routes import setup_routes
from klein.test_resource import requestMock, _render
from leap.mail.imap.account import SoledadBackedAccount
@@ -30,7 +31,7 @@ from pixelated.adapter.soledad_querier import SoledadQuerier
from pixelated.adapter.tag_service import TagService
from pixelated.config import app_factory
from pixelated.controllers import FeaturesController, HomeController, MailsController, TagsController, \
- SyncInfoController, AttachmentsController
+ SyncInfoController, AttachmentsController, ContactsController
import pixelated.runserver
from pixelated.adapter.mail import PixelatedMail
from pixelated.adapter.search import SearchEngine
@@ -70,11 +71,12 @@ class AppTestClient:
draft_service=self.draft_service,
search_engine=self.search_engine)
tags_controller = TagsController(search_engine=self.search_engine)
+ contacts_controller = ContactsController(search_engine=self.search_engine)
sync_info_controller = SyncInfoController()
attachments_controller = AttachmentsController(self.soledad_querier)
- app_factory._setup_routes(self.app, home_controller, mails_controller, tags_controller,
- features_controller, sync_info_controller, attachments_controller)
+ setup_routes(self.app, home_controller, mails_controller, tags_controller,
+ features_controller, sync_info_controller, attachments_controller, contacts_controller)
def _render(self, request, as_json=True):
def get_request_written_data(_=None):
diff --git a/service/test/support/integration/soledad_test_base.py b/service/test/support/integration/soledad_test_base.py
index 2221679f..a9663144 100644
--- a/service/test/support/integration/soledad_test_base.py
+++ b/service/test/support/integration/soledad_test_base.py
@@ -81,3 +81,7 @@ class SoledadTestBase(unittest.TestCase):
def mark_many_as_read(self, idents):
res, req = self.client.post('/mails/read', json.dumps({'idents': idents}))
return req
+
+ def get_contacts(self, query):
+ res, req = self.client.get('/contacts', get_args={'q': query})
+ return res \ No newline at end of file