From 4f484e4dd2a40c4b3c71cd3d241785fb3a7b2eaf Mon Sep 17 00:00:00 2001 From: Duda Dornelles Date: Thu, 20 Nov 2014 13:43:12 -0200 Subject: Adding contacts controller and basic contact search --- service/pixelated/adapter/search.py | 34 +++++++++++++++++----- service/pixelated/config/app_factory.py | 32 +++----------------- service/pixelated/config/routes.py | 26 +++++++++++++++++ service/pixelated/controllers/__init__.py | 1 + .../pixelated/controllers/contacts_controller.py | 31 ++++++++++++++++++++ .../pixelated/controllers/features_controller.py | 2 +- service/test/integration/contacts_test.py | 22 ++++++++++++++ .../test/support/integration/app_test_client.py | 8 +++-- .../test/support/integration/soledad_test_base.py | 4 +++ 9 files changed, 121 insertions(+), 39 deletions(-) create mode 100644 service/pixelated/config/routes.py create mode 100644 service/pixelated/controllers/contacts_controller.py create mode 100644 service/test/integration/contacts_test.py (limited to 'service') 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 . + 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//read', methods=['POST'])(mails_controller.mark_mail_as_read) - app.route('/mail//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/', methods=['GET'])(mails_controller.mail) - app.route('/mail/', 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//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/', 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//read', methods=['POST'])(mails_controller.mark_mail_as_read) + app.route('/mail//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/', methods=['GET'])(mails_controller.mail) + app.route('/mail/', 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//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/', 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 . + +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 -- cgit v1.2.3