Merge lp:~jamesh/unity-scope-gdrive/previews into lp:~submarine/unity-scope-gdrive/libunity-7-compatible

Proposed by James Henstridge
Status: Merged
Approved by: Michal Hruby
Approved revision: 52
Merged at revision: 47
Proposed branch: lp:~jamesh/unity-scope-gdrive/previews
Merge into: lp:~submarine/unity-scope-gdrive/libunity-7-compatible
Diff against target: 534 lines (+290/-86)
6 files modified
debian/control (+7/-0)
debian/rules (+3/-0)
gdrive.scope (+1/-1)
test_scope.py (+142/-0)
unity-scope-gdrive.service (+1/-1)
unity_gdrive_daemon.py (+136/-84)
To merge this branch: bzr merge lp:~jamesh/unity-scope-gdrive/previews
Reviewer Review Type Date Requested Status
Michal Hruby (community) Approve
PS Jenkins bot (community) continuous-integration Approve
Review via email: mp+162505@code.launchpad.net

Commit message

Convert to the new scope API and add a preview.

Description of the change

Convert scope to new API, and add a simple preview showing author, modification date and whether the document has been starred or shared.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Michal Hruby (mhr3) wrote :

Could we get some tests? Or is that too complex with the authentication?

review: Needs Information
50. By James Henstridge

Use InfoHint to construct the preview.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
51. By James Henstridge

Add a test for the search functionality. Unfortunately we need to use a
stub document since GData.Parsable.new_from_xml() segfaults for
GData.Feed subtypes.

The preview test is also skipped because I can't initialise the metadata
dictionary.

52. By James Henstridge

Enable tests in packaging.

Revision history for this message
James Henstridge (jamesh) wrote :

I've added some basic tests for the search functionality, although it stubs out all the account discovery code and provides a fake DocumentsEntry class.

My original plan was to use GData.Parsable.new_from_xml() to build the feed from some sample XML, but that segfaults the interpreter. A quick poke with GDB shows that this wouldn't work from C either: it looks like you can only decode into GData.Feed subclasses from the query code path.

The previewer test is also disabled because I wasn't able to initialise Unity.ScopeResult.metadata from Python in a way that wouldn't cause it to segfault.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Michal Hruby (mhr3) wrote :

LGTM.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/control'
2--- debian/control 2013-03-15 10:09:23 +0000
3+++ debian/control 2013-05-09 07:42:25 +0000
4@@ -7,6 +7,13 @@
5 pkg-config,
6 python3,
7 python3-distutils-extra,
8+ python3-nose,
9+ python3-gi,
10+ gir1.2-glib-2.0,
11+ gir1.2-accounts-1.0,
12+ gir1.2-signon-1.0,
13+ gir1.2-gdata-0.0,
14+ gir1.2-unity-5.0 (>= 6.91),
15 Standards-Version: 3.9.3
16 Homepage: https://launchpad.net/unity-scope-gdrive
17 # If you aren't a member of ~online-accounts but need to upload packaging
18
19=== modified file 'debian/rules'
20--- debian/rules 2012-11-30 16:02:18 +0000
21+++ debian/rules 2013-05-09 07:42:25 +0000
22@@ -13,6 +13,9 @@
23 override_dh_auto_build:
24 python3 setup.py build
25
26+override_dh_auto_test:
27+ nosetests3
28+
29 override_dh_auto_install:
30 python3 setup.py install --root=$(CURDIR)/debian/unity-scope-gdrive --install-layout=deb
31
32
33=== modified file 'gdrive.scope'
34--- gdrive.scope 2013-05-01 17:52:08 +0000
35+++ gdrive.scope 2013-05-09 07:42:25 +0000
36@@ -4,7 +4,7 @@
37 Icon=/usr/share/icons/unity-icon-theme/places/svg/service-gdrive.svg
38 Keywords=gdrive;doc;drive;google;
39 Loader=/usr/share/unity-scopes/gdrive/unity_gdrive_daemon.py
40-RequiredMetadata=
41+RequiredMetadata=author[s];shared[b];starred[b];updated[i]
42 OptionalMetadata=
43 RemoteContent=true
44 Type=file
45
46=== added file 'test_scope.py'
47--- test_scope.py 1970-01-01 00:00:00 +0000
48+++ test_scope.py 2013-05-09 07:42:25 +0000
49@@ -0,0 +1,142 @@
50+import unittest
51+
52+from gi.repository import GData, GLib, Unity
53+
54+import unity_gdrive_daemon as gdrive
55+
56+
57+class ResultSet(Unity.ResultSet):
58+ def __init__(self):
59+ Unity.ResultSet.__init__(self)
60+ self.results = []
61+
62+ def do_add_result(self, result):
63+ self.results.append(dict(uri=result.uri,
64+ icon=result.icon_hint,
65+ category=result.category,
66+ result_type=result.result_type,
67+ mimetype=result.mimetype,
68+ title=result.title,
69+ comment=result.comment,
70+ dnd_uri=result.dnd_uri,
71+ author=result.metadata['author'].get_string(),
72+ shared=result.metadata['shared'].get_boolean(),
73+ starred=result.metadata['starred'].get_boolean(),
74+ updated=result.metadata['updated'].get_int32()))
75+
76+
77+class FakeDocumentsEntry:
78+ """A fake GData.DocumentsEntry"""
79+
80+ def get_resource_id(self):
81+ return 'spreadsheet:abcdefghij'
82+
83+ def look_up_link(self, rel):
84+ assert rel == GData.LINK_ALTERNATE
85+ return GData.Link.new("http://example.com/view", rel)
86+
87+ def get_updated(self):
88+ return 1000000000
89+
90+ def get_title(self):
91+ return "Fake title"
92+
93+ def get_authors(self):
94+ return [GData.Author.new('john.smith', '', 'john.smith@example.com')]
95+
96+ def get_categories(self):
97+ return [GData.Category.new(
98+ 'http://schemas.google.com/g/2005/labels#starred',
99+ 'http://schemas.google.com/g/2005/labels', 'starred')]
100+
101+ def get_content_uri(self):
102+ return "http://example.com/export"
103+
104+
105+class StubGDocsAccount(gdrive.GDocsAccount):
106+ def __init__(self):
107+ self._enabled = True
108+
109+ def get_doc_list(self, search, filters, is_global):
110+ # It would be nice if we could use
111+ # GData.Parsable.new_from_xml() to generate the feed from XML,
112+ # but it segfaults.
113+ return [FakeDocumentsEntry()]
114+
115+
116+def fake_setup_accounts(self):
117+ self._gdocs_accounts.append(StubGDocsAccount())
118+
119+
120+class GDriveTests(unittest.TestCase):
121+ def setUp(self):
122+ super(GDriveTests, self).setUp()
123+ self._orig_setup_accounts = gdrive.GDriveScope.setup_accounts
124+ gdrive.GDriveScope.setup_accounts = fake_setup_accounts
125+
126+ def tearDown(self):
127+ gdrive.GDriveScope.setup_accounts = self._orig_setup_accounts
128+ super(GDriveTests, self).tearDown()
129+
130+ def perform_query(self, query):
131+ scope = gdrive.GDriveScope()
132+ filter_set = scope.get_filters()
133+ result_set = ResultSet()
134+ context = Unity.SearchContext.create(
135+ query, 0, filter_set, None, result_set, None)
136+
137+ search = scope.create_search_for_query(context)
138+ search.run()
139+ return result_set.results
140+
141+ def test_get_categories(self):
142+ scope = gdrive.GDriveScope()
143+ category_set = scope.get_categories()
144+ self.assertEqual(
145+ [cat.props.id for cat in category_set.get_categories()],
146+ ['global', 'recent', 'downloads', 'folders'])
147+
148+ def test_get_filters(self):
149+ scope = gdrive.GDriveScope()
150+ filter_set = scope.get_filters()
151+ self.assertEqual(
152+ [filter.props.id for filter in filter_set.get_filters()],
153+ ['modified', 'type'])
154+
155+ def test_search(self):
156+ results = self.perform_query("foo")
157+ self.assertEqual(len(results), 1)
158+ self.assertEqual(results[0], dict(
159+ uri='http://example.com/view',
160+ icon='x-office-spreadsheet',
161+ category=1,
162+ result_type=Unity.ResultType.PERSONAL,
163+ mimetype='text/html',
164+ title='Fake title',
165+ comment='spreadsheet',
166+ dnd_uri='http://example.com/export',
167+ author='john.smith',
168+ shared=False,
169+ starred=True,
170+ updated=1000000000))
171+
172+ @unittest.skip("Can not set up metadata in ScopeResult")
173+ def test_preview(self):
174+ result = Unity.ScopeResult.create(
175+ uri='http://example.com/view',
176+ icon_hint='x-office-spreadsheet',
177+ category=1,
178+ result_type=Unity.ResultType.PERSONAL,
179+ mimetype='text/html',
180+ title='Fake title',
181+ comment='spreadsheet',
182+ dnd_uri='http://example.com/export',
183+ metadata={
184+ 'author': GLib.Variant('s', 'john.smith'),
185+ 'shared': GLib.Variant('b', False),
186+ 'starred': GLib.Variant('b', True),
187+ 'updated': GLib.Variant('i', 1000000000)})
188+ metadata = Unity.SearchMetadata()
189+ scope = gdrive.GDriveScope()
190+ previewer = scope.create_previewer(result, metadata)
191+ preview = previewer.run()
192
193=== modified file 'unity-scope-gdrive.service'
194--- unity-scope-gdrive.service 2013-03-22 15:25:03 +0000
195+++ unity-scope-gdrive.service 2013-05-09 07:42:25 +0000
196@@ -1,3 +1,3 @@
197 [D-BUS Service]
198 Name=com.canonical.Unity.Scope.File.Gdrive
199-Exec=/usr/share/unity-scopes/gdrive/unity_gdrive_daemon.py
200+Exec=/usr/bin/python3 /usr/share/unity-scopes/scope-runner-dbus.py /usr/share/unity-scopes/gdrive/unity_gdrive_daemon.py
201
202=== modified file 'unity_gdrive_daemon.py' (properties changed: +x to -x)
203--- unity_gdrive_daemon.py 2013-03-22 15:25:03 +0000
204+++ unity_gdrive_daemon.py 2013-05-09 07:42:25 +0000
205@@ -1,4 +1,5 @@
206 #! /usr/bin/python3
207+# -*- mode: python; python-indent: 2 -*-
208 #
209 # Copyright 2012 Canonical Ltd.
210 #
211@@ -7,27 +8,26 @@
212 # GPLv3
213 #
214
215+from datetime import datetime, timedelta
216+import gettext
217+import locale
218 import sys
219+import time
220+
221 from gi.repository import GLib, GObject, Gio
222 from gi.repository import Accounts, Signon
223 from gi.repository import GData
224-from gi.repository import Unity, UnityExtras
225+from gi.repository import Unity
226
227-from datetime import datetime, timedelta
228-import gettext
229-import time
230
231 APP_NAME = "unity-scope-gdrive"
232 LOCAL_PATH = "/usr/share/locale/"
233
234+locale.setlocale(locale.LC_ALL, '')
235 gettext.bindtextdomain(APP_NAME, LOCAL_PATH)
236 gettext.textdomain(APP_NAME)
237 _ = gettext.gettext
238
239-#
240-# The primary bus name we grab *must* match what we specify in our .scope file
241-#
242-BUS_NAME = "com.canonical.Unity.Scope.File.Gdrive"
243 # Map Google Docs types to the values of the "type" filter
244 TYPE_MAP = {
245 "document": "documents",
246@@ -38,34 +38,34 @@
247 }
248 THEME = "/usr/share/icons/unity-icon-theme/places/svg/"
249
250-class Daemon:
251-
252- def __init__ (self):
253- self._scope = Unity.DeprecatedScope.new ("/com/canonical/unity/scope/file/gdrive", "gdrive")
254- self._scope.search_in_global = True;
255- self._preferences = Unity.PreferencesManager.get_default()
256- self._preferences.connect ("notify::remote-content-search",
257- self._on_filters_or_preferences_changed);
258+
259+class GDriveScope(Unity.AbstractScope):
260+ __g_type_name__ = "GDriveScope"
261+
262+ def __init__(self):
263+ super(GDriveScope, self).__init__()
264+ self.search_in_global = True;
265
266 self._gdocs_accounts = []
267- try:
268- self._account_manager = Accounts.Manager.new_for_service_type("documents")
269- except TypeError as e:
270- print ("Couldn't start account manager, not initialising: %s" % e)
271- sys.exit(0)
272- self._account_manager.connect("enabled-event", self._on_enabled_event);
273- for account in self._account_manager.get_enabled_account_services():
274- self.add_account_service(account)
275-
276- # Listen for changes and requests
277- self._scope.connect ("search-changed", self._on_search_changed)
278-
279- # This allows us to re-do the search if any parameter on a filter has changed
280- # Though it's possible to connect to a more-specific changed signal on each
281- # Fitler, as we re-do the search anyway, this catch-all signal is perfect for
282- # us.
283- self._scope.connect ("notify::filtering", self._on_filters_or_preferences_changed);
284-
285+ self.setup_accounts()
286+
287+ def do_get_group_name(self):
288+ # The primary bus name we grab *must* match what we specify in our
289+ # .scope file
290+ return "com.canonical.Unity.Scope.File.Gdrive"
291+
292+ def do_get_unique_name(self):
293+ return "/com/canonical/unity/scope/file/gdrive"
294+
295+ def do_get_schema(self):
296+ schema = Unity.Schema.new()
297+ schema.add_field('author', 's', Unity.SchemaFieldType.REQUIRED)
298+ schema.add_field('shared', 'b', Unity.SchemaFieldType.REQUIRED)
299+ schema.add_field('starred', 'b', Unity.SchemaFieldType.REQUIRED)
300+ schema.add_field('updated', 'i', Unity.SchemaFieldType.REQUIRED)
301+ return schema
302+
303+ def do_get_filters(self):
304 filters = Unity.FilterSet.new()
305 f = Unity.RadioOptionFilter.new ("modified", _("Last modified"), Gio.ThemedIcon.new("input-keyboard-symbolic"), False)
306 f.add_option ("last-7-days", _("Last 7 days"), None)
307@@ -81,7 +81,9 @@
308 f2.add_option ("presentations", _("Presentations"), None)
309 f2.add_option ("other", _("Other"), None)
310 filters.add (f2)
311- self._scope.props.filters = filters
312+ return filters
313+
314+ def do_get_categories(self):
315 cats = Unity.CategorySet.new()
316 cats.add (Unity.Category.new ('global',
317 _("Files & Folders"),
318@@ -99,8 +101,26 @@
319 _("Folders"),
320 Gio.ThemedIcon.new(THEME + "group-folders.svg"),
321 Unity.CategoryRenderer.VERTICAL_TILE))
322- self._scope.props.categories = cats
323- self._scope.export()
324+ return cats
325+
326+ def do_create_search_for_query(self, search_context):
327+ return GDriveScopeSearch(search_context, self._gdocs_accounts)
328+
329+ def do_create_previewer(self, result, metadata):
330+ previewer = GDriveScopePreviewer()
331+ previewer.set_scope_result(result)
332+ previewer.set_search_metadata(metadata)
333+ return previewer
334+
335+ def setup_accounts(self):
336+ try:
337+ self._account_manager = Accounts.Manager.new_for_service_type("documents")
338+ except TypeError as e:
339+ print ("Couldn't start account manager, not initialising: %s" % e)
340+ sys.exit(0)
341+ self._account_manager.connect("enabled-event", self._on_enabled_event);
342+ for account in self._account_manager.get_enabled_account_services():
343+ self.add_account_service(account)
344
345 def _on_enabled_event (self, account_manager, account_id):
346 account = self._account_manager.get_account(account_id)
347@@ -113,32 +133,49 @@
348 for gdocs_account in self._gdocs_accounts:
349 if gdocs_account.get_account_service() == account_service:
350 return
351- gdocs_account = GDocsAccount(self._scope, account_service);
352+ gdocs_account = GDocsAccount(account_service);
353 self._gdocs_accounts.append(gdocs_account)
354
355- def _on_search_changed (self, scope, search, search_type, cancellable):
356- search_string = search.props.search_string
357- results = search.props.results_model
358- results.clear()
359-
360- if self._preferences.props.remote_content_search != Unity.PreferencesManagerRemoteContent.ALL:
361- search.emit("finished")
362- return
363-
364- if search_type == Unity.SearchType.GLOBAL:
365- is_global = True
366- else:
367- is_global = False
368-
369- print("Search changed to: '%s'" % search_string)
370-
371+
372+class GDriveScopeSearch(Unity.ScopeSearchBase):
373+ __g_type_name__ = "GDriveScopeSearch"
374+
375+ def __init__(self, search_context, accounts):
376+ super(GDriveScopeSearch, self).__init__()
377+ self.set_search_context(search_context)
378+ self._gdocs_accounts = accounts
379+
380+ def do_run(self):
381+ print("Search changed to: '%s'" % self.search_context.search_query)
382 for gdocs_account in self._gdocs_accounts:
383- gdocs_account.update_results_model (search_string, results, search, is_global)
384- search.emit("finished")
385-
386- def _on_filters_or_preferences_changed (self, *_):
387- self._scope.queue_search_changed(Unity.SearchType.DEFAULT)
388-
389+ gdocs_account.search(self.search_context)
390+
391+
392+class GDriveScopePreviewer(Unity.ResultPreviewer):
393+ __g_type_name__ = "GDriveScopePreviewer"
394+
395+ def do_run(self):
396+ icon = Gio.ThemedIcon.new(self.result.icon_hint)
397+ preview = Unity.GenericPreview.new(self.result.title, '', icon)
398+ author = self.result.metadata['author'].get_string()
399+ modified = datetime.fromtimestamp(
400+ self.result.metadata['updated'].get_int32())
401+ shared = self.result.metadata['shared'].get_boolean()
402+ starred = self.result.metadata['starred'].get_boolean()
403+ preview.props.subtitle = _("By %s") % author
404+ preview.add_info(Unity.InfoHint.new(
405+ "format", _("Format"), None, self.result.comment))
406+ preview.add_info(Unity.InfoHint.new(
407+ "modified", _("Modified"), None, modified.strftime('%x, %X')))
408+ preview.add_info(Unity.InfoHint.new(
409+ "shared", _("Shared"), None, _('yes') if shared else _('no')))
410+ preview.add_info(Unity.InfoHint.new(
411+ "starred", _("Starred"), None, _('yes') if starred else _('no')))
412+
413+ action = Unity.PreviewAction.new("open", _("Open"), None)
414+ preview.add_action(action)
415+ return preview
416+
417
418 class SignOnAuthorizer(GObject.Object, GData.Authorizer):
419 __g_type_name__ = "SignOnAuthorizer"
420@@ -195,8 +232,7 @@
421
422 # Encapsulates searching a single user's GDocs
423 class GDocsAccount:
424- def __init__ (self, scope, account_service):
425- self._scope = scope
426+ def __init__ (self, account_service):
427 self._account_service = account_service
428 self._account_service.connect("enabled", self._on_account_enabled)
429 self._enabled = self._account_service.get_enabled()
430@@ -212,12 +248,15 @@
431 print("account %s, enabled %s" % (account, enabled))
432 self._enabled = enabled
433
434- def update_results_model (self, search, model, s, is_global=False):
435+ def search (self, context):
436 if not self._enabled:
437 return
438
439 # Get the list of documents
440- feed = self.get_doc_list(search, s, is_global);
441+ is_global = context.search_type == Unity.SearchType.GLOBAL
442+ feed = self.get_doc_list(
443+ context.search_query, context.filter_state, is_global)
444+ result_set = context.result_set
445 for entry in feed:
446 rtype = entry.get_resource_id().split(":")[0]
447
448@@ -229,22 +268,38 @@
449 else:
450 category = 1
451
452- model.append(uri=entry.look_up_link(GData.LINK_ALTERNATE).get_uri(),
453- icon_hint=self.icon_for_type(rtype),
454- category=category,
455- mimetype="text/html",
456- title=entry.props.title,
457- comment=rtype,
458- dnd_uri=entry.props.content_uri,
459- result_type=Unity.ResultType.PERSONAL);
460+ authors = sorted([author.get_name() for author in entry.get_authors()])
461+ shared = False
462+ starred = False
463+ for cat in entry.get_categories():
464+ if cat.get_scheme() != 'http://schemas.google.com/g/2005/labels':
465+ continue
466+ if cat.get_label() == 'shared':
467+ shared = True
468+ elif cat.get_label() == 'starred':
469+ starred = True
470+
471+ result_set.add_result(
472+ uri=entry.look_up_link(GData.LINK_ALTERNATE).get_uri(),
473+ icon=self.icon_for_type(rtype),
474+ category=category,
475+ result_type=Unity.ResultType.PERSONAL,
476+ mimetype="text/html",
477+ title=entry.get_title(),
478+ comment=rtype,
479+ dnd_uri=entry.get_content_uri(),
480+ author=GLib.Variant('s', ', '.join(authors)),
481+ shared=GLib.Variant('b', shared),
482+ starred=GLib.Variant('b', starred),
483+ updated=GLib.Variant('i', entry.get_updated()))
484
485 # This is where we do the actual search for documents
486- def get_doc_list (self, search, s, is_global):
487+ def get_doc_list (self, search, filters, is_global):
488 query = GData.DocumentsQuery(q=search)
489
490 # We do not want filters to effect global results
491 if not is_global:
492- self.apply_filters(query, s)
493+ self.apply_filters(query, filters)
494
495 print("Searching for: " + query.props.q)
496
497@@ -258,11 +313,11 @@
498 return []
499
500 if not is_global:
501- feed = self.filter_results(feed, s)
502+ feed = self.filter_results(feed, filters)
503 return feed
504
505- def apply_filters (self, query, s):
506- f = s.get_filter("modified")
507+ def apply_filters (self, query, filters):
508+ f = filters.get_filter_by_id("modified")
509 if f != None:
510 o = f.get_active_option()
511 if o != None:
512@@ -277,8 +332,8 @@
513 last_time = datetime.now() - timedelta(age)
514 query.set_updated_min(time.mktime(last_time.timetuple()))
515
516- def filter_results (self, feed, s):
517- f = s.get_filter("type")
518+ def filter_results (self, feed, filters):
519+ f = filters.get_filter_by_id("type")
520 if not f: return feed
521 if not f.props.filtering:
522 return feed
523@@ -313,9 +368,6 @@
524
525 return ret;
526
527-if __name__ == '__main__':
528- daemon = UnityExtras.dbus_own_name(BUS_NAME, Daemon, None)
529- if daemon:
530- GLib.unix_signal_add(0, 2, lambda x: daemon.quit(), None)
531- daemon.run([])
532
533+def load_scope():
534+ return GDriveScope()

Subscribers

People subscribed via source and target branches

to all changes: